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
+1
View File
@@ -1230,6 +1230,7 @@ const Billing = (props) => {
</Typography>
</div>
<BillingStats
isCloud={isCloud}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
+323 -51
View File
@@ -1,7 +1,14 @@
import React, { useState, useEffect } from 'react';
import classNames from "classnames";
import theme from '../theme.jsx';
import classNames from "classnames";
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import {
DatePicker,
DateTimePicker,
LocalizationProvider,
} from '@mui/x-date-pickers'
import {
Tooltip,
@@ -52,12 +59,14 @@ import {
LinearXAxisTickLabel,
} from 'reaviz';
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
const LineChartWrapper = ({keys, inputname, height, width}) => {
const [hovered, setHovered] = useState("");
const inputdata = keys.data === undefined ? keys : keys.data
return (
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, padding: 30, marginTop: 15, }}>
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, padding: 30, marginTop: 15, backgroundColor: theme.palette.platformColor, overflow: "hidden", }}>
<Typography variant="h6" style={{marginBotton: 15, }}>
{inputname}
</Typography>
@@ -75,16 +84,174 @@ const LineChartWrapper = ({keys, inputname, height, width}) => {
const AppStats = (defaultprops) => {
const { globalUrl, selectedOrganization, userdata, } = defaultprops;
const { globalUrl, selectedOrganization, userdata, isCloud, } = defaultprops;
const [keys, setKeys] = useState([])
const [searches, setSearches] = useState([]);
const [clickData, setClickData] = useState(undefined);
const [conversionData, setConversionData] = useState(undefined);
const [statistics, setStatistics] = useState(undefined);
const [appRuns, setAppruns] = useState(undefined);
const [appRunCosts, setApprunCosts] = useState(undefined);
const [workflowRuns, setWorkflowRuns] = useState(undefined);
const [subflowRuns, setSubflowRuns] = useState(undefined);
const [endTime, setEndTime] = useState("")
const [startTime, setStartTime] = useState("")
const [statistics, setStatistics] = useState(undefined);
const [filteredStatistics, setFilteredStatistics] = useState(undefined);
const [apprunCost, setApprunCost] = useState(0)
const [monthToDateCost, setMonthToDateCost] = useState(0)
const [monthTotalCost, setMonthTotalCost] = useState(0)
const includedExecutions = selectedOrganization.sync_features.app_executions !== undefined ? selectedOrganization.sync_features.app_executions.limit : 0
// Cost in old contracts: 0.0009
// Old contracts also always included 150.000 executions
const invocationCost = includedExecutions === 150000 || includedExecutions === 250000 ? 0.0009 : typecost_single
const defaultAmount = 10000
useEffect(() => {
if (statistics === undefined || statistics === null) {
return
}
if (statistics["daily_statistics"] === undefined || statistics["daily_statistics"] === null) {
setFilteredStatistics(statistics)
return
}
// Calculate month to date cost
var mtd_cost = 0
for (let key in statistics["daily_statistics"]) {
const item = statistics["daily_statistics"][key]
if (item["date"] === undefined) {
continue
}
const date = new Date(item["date"])
const today = new Date()
if (date.getMonth() === today.getMonth()) {
mtd_cost += (item["app_executions"] * invocationCost)
}
}
if (isCloud && mtd_cost !== monthToDateCost) {
// Find how many days there have been in the current month
const today = new Date()
const daysInMonth = new Date(today.getFullYear(), today.getMonth()+1, 0).getDate()
// Find what day we are on
const day = today.getDate()
// Find how many days are left in the month
const daysLeft = daysInMonth - day
// Calculate the cost of the entire month
var monthTotalCost = mtd_cost/day*daysInMonth
monthTotalCost -= defaultAmount*invocationCost
monthTotalCost -= includedExecutions*invocationCost
// Remove included amount
//const defaultAmount = 10000
mtd_cost -= defaultAmount*invocationCost
mtd_cost -= includedExecutions*invocationCost
if (monthTotalCost > 0) {
setMonthTotalCost(monthTotalCost.toFixed(2))
}
if (mtd_cost > 0) {
setMonthToDateCost(mtd_cost.toFixed(2))
}
}
// Make a date at the 1st of the current month
var foundstarttime = (new Date())
foundstarttime.setDate(1)
if (startTime !== "" && startTime !== undefined && startTime !== null) {
foundstarttime = startTime
}
// Set to tomorrow by default
var foundendtime = (new Date())
foundendtime.setDate(foundendtime.getDate() + 1)
// Check if endtime is after the daily statistics["date"] string
if (endTime !== "" && endTime !== undefined && endTime !== null) {
foundendtime = endTime
}
// Check if start time is before the daily statistics["date"] string
var newlist = []
for (let key in statistics["daily_statistics"]) {
const item = statistics["daily_statistics"][key]
if (item["date"] === undefined) {
continue
}
const date = new Date(item["date"])
if (date >= foundstarttime) {
if (date <= foundendtime) {
newlist.push(item)
}
}
}
// If newlist is empty, set the timestamp to 1 year back and check if there are any statistics there
// If foundstarttime is more than 30 days back, don't do this
/*
if (newlist.length === 0 && foundstarttime.getDate() > 30) {
// Set the timestamp to be back
foundstarttime.setFullYear(foundstarttime.getFullYear() - 1)
setStartTime(foundstarttime)
console.log("IN HERE")
}
*/
var tmpstats = JSON.parse(JSON.stringify(statistics))
var workflowexecutions = 0
var appexecutions = 0
var estimatedcost = 0
if (newlist.length > 0) {
tmpstats["daily_statistics"] = newlist
for (let key in newlist) {
const item = newlist[key]
if (item["workflow_executions"] === undefined) {
continue
}
workflowexecutions += item["workflow_executions"]
appexecutions += item["app_executions"]
estimatedcost += (item["app_executions"] * invocationCost)
}
tmpstats["monthly_workflow_executions"] = workflowexecutions
tmpstats["monthly_app_executions"] = appexecutions
}
// Make estimatedcost have max 2 decimals
if (isCloud) {
// Exclude includedExecutions*month
// const includedExecutions = 150000
//estimatedcost -= (includedExecutions * invocationCost)
setApprunCost(estimatedcost.toFixed(2))
}
setFilteredStatistics(tmpstats)
handleDataSetting(tmpstats, "day")
}, [statistics, startTime, endTime])
const handleStartTimeChange = (date) => {
setStartTime(date)
}
const handleEndTimeChange = (date) => {
setEndTime(date)
}
const handleDataSetting = (inputdata, grouping) => {
if (inputdata === undefined || inputdata === null) {
return
@@ -95,8 +262,6 @@ const AppStats = (defaultprops) => {
return
}
console.log("Looking at daily data: ", inputdata)
var appRuns = {
"key": "App Runs",
"data": []
@@ -112,6 +277,11 @@ const AppStats = (defaultprops) => {
"data": []
}
var appcostRuns = {
"key": "Cost of App Runs",
"data": []
}
for (let key in dailyStats) {
// Always skips first one as it has accumulated data in it
if (key === 0) {
@@ -119,7 +289,6 @@ const AppStats = (defaultprops) => {
}
const item = dailyStats[key]
if (item["date"] === undefined) {
console.log("No date: ", item)
continue
@@ -131,6 +300,12 @@ const AppStats = (defaultprops) => {
key: new Date(item["date"]),
data: item["app_executions"]
})
// Add number
appcostRuns["data"].push({
key: new Date(item["date"]),
data: (item["app_executions"] * invocationCost).toFixed(2)
})
}
// Check if workflow_executions key in item
@@ -150,12 +325,16 @@ const AppStats = (defaultprops) => {
}
// Adds data for today
console.log("Inputdata: ", inputdata)
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date(),
data: inputdata["daily_app_executions"]
})
appcostRuns["data"].push({
key: new Date(),
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
@@ -175,6 +354,7 @@ const AppStats = (defaultprops) => {
setSubflowRuns(subflowRuns)
setWorkflowRuns(workflowRuns)
setAppruns(appRuns)
setApprunCosts(appcostRuns)
}
const getStats = () => {
@@ -186,25 +366,25 @@ const AppStats = (defaultprops) => {
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status);
return;
}
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status);
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
return
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
return
}
setStatistics(responseJson)
handleDataSetting(responseJson, "day")
})
.catch((error) => {
console.log("error: ", error)
});
setStatistics(responseJson)
handleDataSetting(responseJson, "day")
})
.catch((error) => {
console.log("error: ", error)
});
}
useEffect(() => {
@@ -215,40 +395,126 @@ const AppStats = (defaultprops) => {
textAlign: "center",
padding: 40,
margin: 5,
backgroundColor: theme.palette.surfaceColor,
backgroundColor: theme.palette.platformColor,
border: "1px solid rgba(255,255,255,0.3)",
maxWidth: 300,
}
const data = (
<div className="content" style={{width: "100%", margin: "auto", }}>
<Typography variant="body1" style={{margin: "auto", marginLeft: 10, marginBottom: 20, }}>
All Stat widgets are monthly and gathered from <a
All shown statistics are gathered from <a
href={`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e",}}
>Your Organization Statistics. </a>
This is a feature to help give you more insight into Shuffle, and will be populating over time.
>Your Organization Statistics </a>
This is a feature to help give you more insight into Shuffle, and to understand your utilization of the Shuffle platform. <b>The billing tracker is in Beta, and is always calculated manually before being invoiced.</b>
</Typography>
{statistics !== undefined ?
<div style={{display: "flex", textAlign: "center",}}>
<Paper style={paperStyle}>
<Typography variant="h4">
{statistics.monthly_workflow_executions}
</Typography>
<Typography variant="h6">
Workflow Runs
</Typography>
</Paper>
<Paper style={paperStyle}>
<Typography variant="h4">
{statistics.monthly_app_executions}
</Typography>
<Typography variant="h6">
App Runs
</Typography>
</Paper>
</div>
: null}
<div style={{display: "flex", textAlign: "center",}}>
{filteredStatistics !== undefined ?
<div style={{flex: 1, display: "flex", textAlign: "center",}}>
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}>
The cost of app runs in the selected period based on {filteredStatistics.monthly_app_executions} App Runs. These numbers do not exclude your included 10.000/month or {includedExecutions} App Runs per month. App Run cost: ${invocationCost}.
</Typography>
}>
<Paper style={paperStyle}>
<Typography variant="h4">
${apprunCost}
</Typography>
<Typography variant="h6">
Period Cost
</Typography>
</Paper>
</Tooltip>
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}>
App runs in the selected period
</Typography>
}>
<Paper style={paperStyle}>
<Typography variant="h4">
{filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions}
</Typography>
<Typography variant="h6">
App Runs
</Typography>
</Paper>
</Tooltip>
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}>
Workflow runs in the selected period
</Typography>
}>
<Paper style={paperStyle}>
<Typography variant="h4">
{filteredStatistics.monthly_workflow_executions === null || filteredStatistics.monthly_workflow_executions === undefined ? 0 : filteredStatistics.monthly_workflow_executions}
</Typography>
<Typography variant="h6">
Workflow Runs
</Typography>
</Paper>
</Tooltip>
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}>
Estimated cost to be billed at the end of the current month. Subtracted contractually included app runs. Actual cost month to date: ${monthToDateCost}. App Run cost: ${invocationCost}.
</Typography>
}>
<Paper style={{
textAlign: "center",
padding: 40,
margin: 5,
marginLeft: 90,
backgroundColor: theme.palette.platformColor,
border: "1px solid rgba(255,255,255,0.3)",
maxWidth: 300,
}}>
<Typography variant="h4">
${monthTotalCost}
</Typography>
<Typography variant="h6">
Estimated cost
</Typography>
</Paper>
</Tooltip>
</div>
: null}
<LocalizationProvider dateAdapter={AdapterDayjs} style={{flex: 1, }}>
<div style={{display: "flex", flexDirection: "column", }}>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
minWidth: 240,
maxWidth: 240,
}}
ampm={false}
label="Search from"
format="YYYY-MM-DD HH:mm:ss"
value={startTime}
onChange={handleStartTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
minWidth: 240,
maxWidth: 240,
}}
ampm={false}
label="Search until"
format="YYYY-MM-DD HH:mm:ss"
value={endTime}
onChange={handleEndTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
</div>
</LocalizationProvider>
</div>
{appRuns === undefined ?
null
@@ -267,6 +533,12 @@ const AppStats = (defaultprops) => {
:
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"}/>
}
{/*appRunCosts === undefined ?
null
:
<LineChartWrapper keys={appRunCosts} height={300} width={"100%"} inputname={"Apprun cost - Cost per day"}/>
*/}
</div>
)
+2 -1
View File
@@ -830,6 +830,7 @@ const RuntimeDebugger = (props) => {
);
}}
/>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
sx={{
@@ -859,8 +860,8 @@ const RuntimeDebugger = (props) => {
onChange={handleEndTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
</LocalizationProvider>
<Button
variant="outlined"
color="primary"
@@ -448,7 +448,7 @@ const WorkflowTemplatePopup = (props) => {
return (
<div style={{ display: "flex", maxWidth: isCloud ? 470 : isMobile? 345: 450, minWidth: isCloud ? 470 : isMobile? null: 450, height: 78, borderRadius: 8, }}>
<div style={{ display: "flex", maxWidth: isCloud ? isMobile ? null : 470 : isMobile? 345: 450, minWidth: isCloud ? isMobile ? null : 470 : isMobile? null: 450, height: 78, borderRadius: 8, justifyContent: isMobile ? null : "center" }}>
<ModalView />
<div
// variant={isActive === 1 ? "contained" : "outlined"}
@@ -522,7 +522,7 @@ const WorkflowTemplatePopup = (props) => {
<div style={{width: 50, }} />
}
</div>
<div style={{ flex: 3, marginLeft: 20, }}>
<div style={{ flex: 3, marginLeft: 20, maxHeight: 50, overflow: "hidden", }}>
<Typography variant="body1" style={{ marginTop: parsedDescription.length === 0 ? 10 : 0, fontSize: isMobile ? 13 : 16,fontWeight: isHomePage? 600 : null,textTransform: 'capitalize', color: isHomePage ? "var(--White-text, #F1F1F1)" :"rgba(241, 241, 241, 1)"}} >
{parsedTitle}
</Typography>
+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>
+158 -68
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: {
@@ -3688,47 +3695,75 @@ const AppCreator = (defaultprops) => {
};
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}