Added more management tools for workflow running and stat management

This commit is contained in:
Frikky
2024-01-21 22:11:10 +01:00
parent a08c9c0bf3
commit f7c0c6e5e4
8 changed files with 752 additions and 191 deletions
+183 -26
View File
@@ -88,7 +88,6 @@ import Priorities from "../components/Priorities.jsx";
import Branding from "../components/Branding.jsx";
import Files from "../components/Files.jsx";
import { display, style } from "@mui/system";
//import EnvironmentStats from "../components/EnvironmentStats.jsx";
const useStyles = makeStyles({
notchedOutline: {
@@ -181,10 +180,12 @@ const Admin = (props) => {
const [secret2FA, setSecret2FA] = React.useState("");
const [show2faSetup, setShow2faSetup] = useState(false);
const [adminTab, setAdminTab] = React.useState(2);
const [showApiKey, setShowApiKey] = useState(false);
const [adminTab, setAdminTab] = React.useState(3);
const [showApiKey, setShowApiKey] = useState(false);
const [billingInfo, setBillingInfo] = React.useState({});
const [selectedStatus, setSelectedStatus] = React.useState([]);
const [selectedStatus, setSelectedStatus] = React.useState([]);
const [, forceUpdate] = React.useState();
useEffect(() => {
getUsers()
@@ -2254,35 +2255,124 @@ If you're interested, please let me know a time that works for you, or set up a
const GridItem = (props) => {
const [expanded, setExpanded] = React.useState(false);
const [showEdit, setShowEdit] = React.useState(false);
const [newValue, setNewValue] = React.useState(-100);
const primary = props.data.primary;
const secondary = props.data.secondary;
const primaryIcon = props.data.icon;
const secondaryIcon = props.data.active ? (
const secondaryIcon = props.data.active ?
<CheckCircleIcon style={{ color: "green" }} />
) : (
:
<CloseIcon style={{ color: "red" }} />
)
const submitFeatureEdit = (sync_features) => {
if (!userdata.support) {
console.log("User does not have support access and can't edit features");
return
}
sync_features.editing = true
const data = {
org_id: selectedOrganization.id,
sync_features: sync_features,
};
const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`;
fetch(url, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast("Failed updating org: ", responseJson.reason);
} else {
toast("Successfully edited org!");
}
})
)
.catch((error) => {
toast("Err: " + error.toString());
});
}
const enableFeature = () => {
console.log("Enabling "+primary)
console.log(selectedOrganization.sync_features)
// Check if primary is in sync_features
var tmpprimary = primary.replaceAll(" ", "_")
if (!(tmpprimary in selectedOrganization.sync_features)) {
console.log("Primary not in sync_features: "+tmpprimary)
return
}
if (props.data.active) {
selectedOrganization.sync_features[tmpprimary].active = false
} else {
selectedOrganization.sync_features[tmpprimary].active = true
}
setSelectedOrganization(selectedOrganization)
forceUpdate(Math.random())
submitFeatureEdit(selectedOrganization.sync_features)
}
const submitEdit = (e) => {
e.preventDefault();
e.stopPropagation();
// Check if primary is in sync_features
var tmpprimary = primary.replaceAll(" ", "_")
if (!(tmpprimary in selectedOrganization.sync_features)) {
console.log("Primary not in sync_features: "+tmpprimary)
return
}
// Make it into a number
var tmp = parseInt(newValue)
if (isNaN(tmp)) {
console.log("Not a number: "+newValue)
return
}
selectedOrganization.sync_features[tmpprimary].limit = tmp
setSelectedOrganization(selectedOrganization)
forceUpdate(Math.random())
submitFeatureEdit(selectedOrganization.sync_features)
}
return (
<Grid
item
xs={4}
style={{ cursor: "pointer" }}
onClick={() => {
setExpanded(!expanded);
}}
>
<Card
style={{
margin: 4,
backgroundColor: theme.palette.surfaceColor,
backgroundColor: theme.palette.platformColor,
borderRadius: theme.palette.borderRadius,
border: "1px solid rgba(255,255,255,0.3)",
color: "white",
minHeight: expanded ? 250 : "inherit",
maxHeight: expanded ? 250 : "inherit",
maxHeight: expanded ? 300 : "inherit",
}}
>
<ListItem>
<ListItem
style={{cursor: "pointer", }}
onClick={() => {
setExpanded(!expanded);
}}
>
<ListItemAvatar>
<Avatar>{primaryIcon}</Avatar>
</ListItemAvatar>
@@ -2290,9 +2380,42 @@ If you're interested, please let me know a time that works for you, or set up a
style={{ textTransform: "capitalize" }}
primary={primary}
/>
{secondaryIcon}
{isCloud && userdata.support === true ?
<Tooltip title="Edit features (support users only)">
<EditIcon
color="secondary"
style={{marginRight: 10, cursor: "pointer", }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showEdit) {
setShowEdit(false)
return
}
console.log("Edit")
setShowEdit(true)
}}
/>
</Tooltip>
: null}
<Tooltip title={props.data.active ? "Disable feature" : "Enable feature"}>
<span
style={{cursor: "pointer", marginTop: 5, }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
enableFeature()
}}
>
{secondaryIcon}
</span>
</Tooltip>
</ListItem>
{expanded ? (
{expanded ?
<div style={{ padding: 15 }}>
<Typography>
<b>Usage:&nbsp;</b>
@@ -2309,7 +2432,41 @@ If you're interested, please let me know a time that works for you, or set up a
</Typography>*/}
<Typography style={{maxHeight: 150, overflowX: "hidden", overflowY: "auto"}}><b>Description:</b> {secondary}</Typography>
</div>
) : null}
: null}
{showEdit ?
<FormControl fullWidth onSubmit={(e) => {
console.log("Submit")
submitEdit(e)
}}>
<span style={{display: "flex", }}>
<TextField
style={{flex: 3, }}
color="primary"
label={"Edit value"}
defaultValue={props.data.limit}
style={{
}}
onChange={(event) => {
setNewValue(event.target.value)
}}
/>
<Button
style={{flex: 1, }}
variant="contained"
disabled={newValue < -1}
onClick={(e) => {
console.log("Submit 2")
submitEdit(e)
}}
>
Submit
</Button>
</span>
</FormControl>
: null}
</Card>
</Grid>
);
@@ -2658,7 +2815,7 @@ If you're interested, please let me know a time that works for you, or set up a
/>
<Tab
label=<span>
Licensing
Billing & Stats
</span>
/>
<Tab
@@ -2858,12 +3015,12 @@ If you're interested, please let me know a time that works for you, or set up a
</div>
)}
<Typography variant="h6" style={{ marginLeft: 5, marginTop: 40, marginBottom: 5 }}>
Cloud sync features
Features
</Typography>
<Typography variant="body2" color="textSecondary" style={{marginBottom: 10, marginLeft: 5, }}>
If not otherwise specified, Usage will reset monthly
Features and Limitations that are currently available to you in your Cloud or Hybrid Organization. App Executions (App Runs) reset monthly. If the organization is a customer or in a trial, these features limitations are not always enforced.
</Typography>
<Grid container style={{ width: "100%", marginBottom: 15 }}>
<Grid container style={{ width: "100%", marginBottom: 15, paddingBottom: 150, }}>
{selectedOrganization.sync_features === undefined ||
selectedOrganization.sync_features === null
@@ -2872,15 +3029,15 @@ If you're interested, please let me know a time that works for you, or set up a
key,
index
) {
// unnecessary parts
if (key === "schedule" || key === "apps" || key === "updates") {
if (key === "schedule" || key === "apps" || key === "updates" || key === "editing") {
return null;
}
const item = selectedOrganization.sync_features[key];
if (item === null) {
return null
}
if (item === null) {
return null
}
const newkey = key.replaceAll("_", " ");
const griditem = {
+37 -2
View File
@@ -1022,7 +1022,7 @@ const AngularWorkflow = (defaultprops) => {
})
.then((responseJson) => {
console.log("GOT A RESPONSE??")
getWorkflowExecutionCount(id);
// getWorkflowExecutionCount(id);
if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null) {
// - means it's opposite
@@ -13758,6 +13758,40 @@ const AngularWorkflow = (defaultprops) => {
</div>
: null
const RightsideBar = () => {
const [hovered, setHovered] = useState(false)
return (
<div
style={{
position: "fixed", right: -5, top: "40%", width: 70, height: 235, border: "1px solid #f85a3e", cursor: "pointer", borderRadius: theme.palette.borderRadius,
padding: 10,
backgroundColor: hovered ? theme.palette.surfaceColor : theme.palette.platformColor,
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={() => {
setExecutionModalOpen(true);
getWorkflowExecution(props.match.params.key, "");
}}
>
<ArrowLeftIcon style={{marginTop: 10, marginLeft: 10, marginBottom: 10, }}/>
<Typography
variant="h6"
style={{
writingMode: "vertical-rl",
textOrientation: "mixed",
marginLeft: 10,
fontWeight: "bold",
}}
>
Explore runs
</Typography>
{/*<ArrowLeftIcon style={{marginTop: 10, marginLeft: 10, marginBottom: 10, }}/> */}
</div>
)
}
const BottomCytoscapeBar = () => {
if (workflow.id === undefined || workflow.id === null || (!workflow.public && apps.length === 0)) {
return null;
@@ -15069,7 +15103,7 @@ const AngularWorkflow = (defaultprops) => {
>
<h2 style={{ color: "rgba(255,255,255,0.5)" }}>
<DirectionsRunIcon style={{ marginRight: 10 }} />
All Workflow Runs: { workflowExecutionCount }
All Workflow Runs
</h2>
</Breadcrumbs>
<Tooltip
@@ -16514,6 +16548,7 @@ const AngularWorkflow = (defaultprops) => {
{showErrors}
<BottomCytoscapeBar />
<TopCytoscapeBar />
<RightsideBar />
</span>
}
</div>
+159 -69
View File
@@ -20,6 +20,7 @@ import {
TextField,
Tooltip,
Breadcrumbs,
Drawer,
CircularProgress,
Chip,
IconButton,
@@ -43,6 +44,7 @@ import {
Loop as LoopIcon,
AddPhotoAlternate as AddPhotoAlternateIcon,
CallMerge as CallMergeIcon,
CloudDownload as CloudDownloadIcon,
} from "@mui/icons-material";
import { v4 as uuidv4 } from "uuid";
@@ -448,6 +450,8 @@ const AppCreator = (defaultprops) => {
const [openApiData, setOpenApiData] = React.useState("");
const [openApiModal, setOpenApiModal] = React.useState(false);
const [appDownloadData, setAppDownloadData] = React.useState("");
useEffect(() => {
console.log("In useEffect for openApiData: ", openApiData)
}, [openApiData]);
@@ -900,8 +904,9 @@ const AppCreator = (defaultprops) => {
}
if (newaction.url !== undefined && newaction.url !== null && newaction.url.includes("_shuffle_replace_")) {
const regex = /_shuffle_replace_\d/i;
//console.log("NEW: ",
//const regex = /_shuffle_replace_\d/i;
const regex = /_shuffle_replace_\d+/i
newaction.url = newaction.url.replaceAll(new RegExp(regex, 'g'), "")
}
@@ -1205,9 +1210,9 @@ const AppCreator = (defaultprops) => {
);
}
}
} catch (e) {
console.log("Param Error: ", e, path)
}
} catch (e) {
console.log("Param Error: ", e, path)
}
}
}
}
@@ -2569,6 +2574,8 @@ const AppCreator = (defaultprops) => {
}
}
setAppDownloadData(JSON.stringify(data, null, 4))
fetch(globalUrl + "/api/v1/verify_openapi", {
method: "POST",
headers: {
@@ -3687,48 +3694,76 @@ const AppCreator = (defaultprops) => {
return errormessage;
};
const getBackgroundColor = (data) => {
var bgColor = "#61afee";
if (data === "POST") {
bgColor = "#49cc90";
} else if (data === "PUT") {
bgColor = "#fca130";
} else if (data === "PATCH") {
bgColor = "#50e3c2";
} else if (data === "DELETE") {
bgColor = "#f93e3e";
} else if (data === "HEAD") {
bgColor = "#9012fe";
}
return bgColor;
}
const newActionModal = (
<Dialog
<Drawer
anchor={"right"}
open={actionsModalOpen}
fullWidth
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: 550,
maxWidth: 550,
maxHeight: 750,
minWidth: 700,
maxWidth: 700,
},
}}
onClose={() => {
setUrlPath("");
setCurrentAction({
name: "",
description: "",
url: "",
file_field: "",
headers: "",
paths: [],
queries: [],
body: "",
errors: [],
method: actionNonBodyRequest[0],
action_label: "No Label",
required_bodyfields: [],
});
setCurrentActionMethod(apikeySelection[0]);
setUrlPathQueries([]);
setActionsModalOpen(false);
setFileUploadEnabled(false);
console.log("Closing modal");
// Old: This had some issue with arrays
//setUrlPath("");
//setCurrentAction({
// name: "",
// description: "",
// url: "",
// file_field: "",
// headers: "",
// paths: [],
// queries: [],
// body: "",
// errors: [],
// method: actionNonBodyRequest[0],
// action_label: "No Label",
// required_bodyfields: [],
//});
//setCurrentActionMethod(apikeySelection[0]);
//setUrlPathQueries([]);
//setActionsModalOpen(false);
//setFileUploadEnabled(false);
console.log(currentAction);
const errors = getActionErrors();
addActionToView(errors);
setActionsModalOpen(false);
setUrlPathQueries([]);
setUrlPath("");
setFileUploadEnabled(false);
}}
>
<FormControl style={{ backgroundColor: surfaceColor, color: "white" }}>
<DialogTitle>
<DialogTitle style={{marginTop: 30, }}>
<div style={{ color: "white" }}>New action</div>
</DialogTitle>
<DialogContent>
<DialogContent style={{paddingBottom: 100, }}>
<a
target="_blank"
href="https://shuffler.io/docs/app_creation#actions"
@@ -3826,26 +3861,33 @@ const AppCreator = (defaultprops) => {
id: "method-option",
}}
>
{actionNonBodyRequest.map((data, index) => {
// Add actionBodyRequest to actionNonBodyRequest
{actionNonBodyRequest.concat(actionBodyRequest).map((data, index) => {
const backgroundColor = getBackgroundColor(data);
return (
<MenuItem
key={index}
style={{ backgroundColor: inputColor, color: "white" }}
style={{}}
value={data}
>
{data}
<Chip
style={{
color: "white",
borderRadius: 5,
minWidth: 80,
marginRight: 10,
marginTop: 2,
cursor: "pointer",
fontSize: 14,
fontWeight: "bold",
backgroundColor: backgroundColor,
}}
label={data}
/>
</MenuItem>
);
})}
{actionBodyRequest.map((data, index) => (
<MenuItem
key={index}
style={{ backgroundColor: inputColor, color: "white" }}
value={data}
>
{data}
</MenuItem>
))}
</Select>
<div style={{ marginTop: "15px" }} />
URL path / Curl statement
@@ -4222,19 +4264,11 @@ const AppCreator = (defaultprops) => {
/>
{exampleResponse}
</DialogContent>
<DialogActions>
<Button
style={{ borderRadius: "0px" }}
onClick={() => {
setActionsModalOpen(false);
}}
>
Cancel
</Button>
<div style={{position: "fixed", backgroundColor: theme.palette.surfaceColor, bottom: 0, width: "100%", padding: 25, borderTop: "1px solid rgba(255,255,255,0.3)", }}>
<Button
color="primary"
variant={urlPath.length > 0 ? "contained" : "outlined"}
style={{ borderRadius: "0px" }}
style={{ }}
onClick={() => {
//console.log(urlPathQueries)
//console.log(urlPath)
@@ -4249,9 +4283,17 @@ const AppCreator = (defaultprops) => {
>
Submit
</Button>
</DialogActions>
<Button
style={{ marginLeft: 10, }}
onClick={() => {
setActionsModalOpen(false);
}}
>
Cancel
</Button>
</div>
</FormControl>
</Dialog>
</Drawer>
);
@@ -6103,20 +6145,68 @@ const AppCreator = (defaultprops) => {
{testView}
*/}
<Button
disabled={appBuilding}
color="primary"
variant="contained"
style={{ borderRadius: "0px", marginTop: "30px", height: "50px" }}
onClick={() => {
submitApp();
}}
>
{appBuilding ? <CircularProgress /> : "Save"}
</Button>
<Typography style={{ marginTop: 5 }}>
{errorCode.length > 0 ? `Error: ${errorCode}` : null}
</Typography>
<div style={{display: "flex", marginTop: 35, }}>
{appDownloadData.length > 0 ?
<Tooltip title="Download the OpenAPI specification for the App" placement="bottom">
<IconButton
style={{marginRight: 25, }}
onClick={() => {
toast(`Downloading OpenAPI JSON data for for ${name}`)
// Download as file
var blob = new Blob([appDownloadData], {
type: "application/octet-stream",
});
var url = URL.createObjectURL(blob);
var link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `${name}.json`);
var event = document.createEvent("MouseEvents");
event.initMouseEvent(
"click",
true,
true,
window,
1,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
);
link.dispatchEvent(event);
}}
>
<CloudDownloadIcon />
</IconButton>
</Tooltip>
: null}
<Button
disabled={appBuilding}
color="primary"
variant="contained"
fullWidth
style={{ height: "50px", flex: 1, }}
onClick={() => {
submitApp();
}}
>
{appBuilding ? <CircularProgress /> : "Save"}
</Button>
{appDownloadData.length > 0 ?
<div style={{width: 50, }}/>
: null}
</div>
<Typography style={{ marginTop: 25, textAlign: "center", }}>
{errorCode.length > 0 ? `Upload Error: ${errorCode}` : null}
</Typography>
</Paper>
</div>
);
+45 -40
View File
@@ -921,6 +921,7 @@ const Apps = (props) => {
</Link>
: null
console.log("Sharing config: ", sharingConfiguration);
const activateButton =
selectedApp.generated && !selectedApp.activated ? (
<div>
@@ -943,6 +944,7 @@ const Apps = (props) => {
onClick={() => {
setDeleteModalOpen(true);
}}
disabled={sharingConfiguration === undefined || sharingConfiguration === null || sharingConfiguration == "public"}
>
<DeleteIcon />
</Button>
@@ -957,7 +959,7 @@ const Apps = (props) => {
(selectedApp.downloaded !== undefined && selectedApp.downloaded == true) ||
!selectedApp.generated) &&
activateButton === null ? (
<Tooltip title={"Delete app"}>
<Tooltip title={"Delete app (confirm box will show)"}>
<Button
variant="outlined"
component="label"
@@ -966,6 +968,7 @@ const Apps = (props) => {
onClick={() => {
setDeleteModalOpen(true);
}}
disabled={sharingConfiguration === undefined || sharingConfiguration === null || sharingConfiguration == "public"}
>
<DeleteIcon />
</Button>
@@ -1240,48 +1243,50 @@ const Apps = (props) => {
</Select>
{isCloud && (selectedApp.sharing === true || selectedApp.public === true || creatorProfile.github_avatar !== undefined) && !internalIds.includes(selectedApp.name.toLowerCase()) ?
<Button
variant="contained"
component="label"
color="primary"
onClick={() => {
const tmpurl = new URL(window.location.href);
const searchParams = tmpurl.searchParams;
const queryID = searchParams.get("queryID");
<Tooltip title="Deactivates this app for the current organisation. This means the app will not be usable again until you re-activate it." placement="top">
<Button
variant="contained"
component="label"
color="primary"
onClick={() => {
const tmpurl = new URL(window.location.href);
const searchParams = tmpurl.searchParams;
const queryID = searchParams.get("queryID");
if (queryID !== undefined && queryID !== null) {
aa("init", {
appId: "JNSS5CFDZZ",
apiKey: "db08e40265e2941b9a7d8f644b6e5240",
});
if (queryID !== undefined && queryID !== null) {
aa("init", {
appId: "JNSS5CFDZZ",
apiKey: "db08e40265e2941b9a7d8f644b6e5240",
});
const timestamp = new Date().getTime();
aa("sendEvents", [
{
eventType: "conversion",
eventName: "Public App Activated",
index: "appsearch",
objectIDs: [selectedApp.id],
timestamp: timestamp,
queryID: queryID,
userToken:
userdata === undefined ||
userdata === null ||
userdata.id === undefined
? "unauthenticated"
: userdata.id,
},
]);
} else {
console.log("No query to handle when activating");
}
const timestamp = new Date().getTime();
aa("sendEvents", [
{
eventType: "conversion",
eventName: "Public App Activated",
index: "appsearch",
objectIDs: [selectedApp.id],
timestamp: timestamp,
queryID: queryID,
userToken:
userdata === undefined ||
userdata === null ||
userdata.id === undefined
? "unauthenticated"
: userdata.id,
},
]);
} else {
console.log("No query to handle when activating");
}
activateApp(selectedApp.id, true)
}}
style={{ height: 35, marginTop: 0, marginLeft: 10, }}
>
Deactivate
</Button>
activateApp(selectedApp.id, true)
}}
style={{ height: 35, marginTop: 0, marginLeft: 10, }}
>
Deactivate
</Button>
</Tooltip>
: null}
</div>
) : null}