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> </Typography>
</div> </div>
<BillingStats <BillingStats
isCloud={isCloud}
globalUrl={globalUrl} globalUrl={globalUrl}
selectedOrganization={selectedOrganization} selectedOrganization={selectedOrganization}
userdata={userdata} userdata={userdata}
+323 -51
View File
@@ -1,7 +1,14 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import classNames from "classnames";
import theme from '../theme.jsx'; 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 { import {
Tooltip, Tooltip,
@@ -52,12 +59,14 @@ import {
LinearXAxisTickLabel, LinearXAxisTickLabel,
} from 'reaviz'; } from 'reaviz';
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
const LineChartWrapper = ({keys, inputname, height, width}) => { const LineChartWrapper = ({keys, inputname, height, width}) => {
const [hovered, setHovered] = useState(""); const [hovered, setHovered] = useState("");
const inputdata = keys.data === undefined ? keys : keys.data const inputdata = keys.data === undefined ? keys : keys.data
return ( 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, }}> <Typography variant="h6" style={{marginBotton: 15, }}>
{inputname} {inputname}
</Typography> </Typography>
@@ -75,16 +84,174 @@ const LineChartWrapper = ({keys, inputname, height, width}) => {
const AppStats = (defaultprops) => { const AppStats = (defaultprops) => {
const { globalUrl, selectedOrganization, userdata, } = defaultprops; const { globalUrl, selectedOrganization, userdata, isCloud, } = defaultprops;
const [keys, setKeys] = useState([]) const [keys, setKeys] = useState([])
const [searches, setSearches] = 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 [appRuns, setAppruns] = useState(undefined);
const [appRunCosts, setApprunCosts] = useState(undefined);
const [workflowRuns, setWorkflowRuns] = useState(undefined); const [workflowRuns, setWorkflowRuns] = useState(undefined);
const [subflowRuns, setSubflowRuns] = 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) => { const handleDataSetting = (inputdata, grouping) => {
if (inputdata === undefined || inputdata === null) { if (inputdata === undefined || inputdata === null) {
return return
@@ -95,8 +262,6 @@ const AppStats = (defaultprops) => {
return return
} }
console.log("Looking at daily data: ", inputdata)
var appRuns = { var appRuns = {
"key": "App Runs", "key": "App Runs",
"data": [] "data": []
@@ -112,6 +277,11 @@ const AppStats = (defaultprops) => {
"data": [] "data": []
} }
var appcostRuns = {
"key": "Cost of App Runs",
"data": []
}
for (let key in dailyStats) { for (let key in dailyStats) {
// Always skips first one as it has accumulated data in it // Always skips first one as it has accumulated data in it
if (key === 0) { if (key === 0) {
@@ -119,7 +289,6 @@ const AppStats = (defaultprops) => {
} }
const item = dailyStats[key] const item = dailyStats[key]
if (item["date"] === undefined) { if (item["date"] === undefined) {
console.log("No date: ", item) console.log("No date: ", item)
continue continue
@@ -131,6 +300,12 @@ const AppStats = (defaultprops) => {
key: new Date(item["date"]), key: new Date(item["date"]),
data: item["app_executions"] 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 // Check if workflow_executions key in item
@@ -150,12 +325,16 @@ const AppStats = (defaultprops) => {
} }
// Adds data for today // Adds data for today
console.log("Inputdata: ", inputdata)
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({ appRuns["data"].push({
key: new Date(), key: new Date(),
data: inputdata["daily_app_executions"] 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) { if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
@@ -175,6 +354,7 @@ const AppStats = (defaultprops) => {
setSubflowRuns(subflowRuns) setSubflowRuns(subflowRuns)
setWorkflowRuns(workflowRuns) setWorkflowRuns(workflowRuns)
setAppruns(appRuns) setAppruns(appRuns)
setApprunCosts(appcostRuns)
} }
const getStats = () => { const getStats = () => {
@@ -186,25 +366,25 @@ const AppStats = (defaultprops) => {
}, },
credentials: "include", credentials: "include",
}) })
.then((response) => { .then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status); console.log("Status not 200 for workflows :O!: ", response.status);
return; return;
} }
return response.json(); return response.json();
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson["success"] === false) { if (responseJson["success"] === false) {
return return
} }
setStatistics(responseJson) setStatistics(responseJson)
handleDataSetting(responseJson, "day") handleDataSetting(responseJson, "day")
}) })
.catch((error) => { .catch((error) => {
console.log("error: ", error) console.log("error: ", error)
}); });
} }
useEffect(() => { useEffect(() => {
@@ -215,40 +395,126 @@ const AppStats = (defaultprops) => {
textAlign: "center", textAlign: "center",
padding: 40, padding: 40,
margin: 5, margin: 5,
backgroundColor: theme.palette.surfaceColor, backgroundColor: theme.palette.platformColor,
border: "1px solid rgba(255,255,255,0.3)",
maxWidth: 300, maxWidth: 300,
} }
const data = ( const data = (
<div className="content" style={{width: "100%", margin: "auto", }}> <div className="content" style={{width: "100%", margin: "auto", }}>
<Typography variant="body1" style={{margin: "auto", marginLeft: 10, marginBottom: 20, }}> <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`} href={`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`}
target="_blank" target="_blank"
style={{ textDecoration: "none", color: "#f85a3e",}} style={{ textDecoration: "none", color: "#f85a3e",}}
>Your Organization Statistics. </a> >Your Organization Statistics </a>
This is a feature to help give you more insight into Shuffle, and will be populating over time. 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> </Typography>
{statistics !== undefined ?
<div style={{display: "flex", textAlign: "center",}}> <div style={{display: "flex", textAlign: "center",}}>
<Paper style={paperStyle}> {filteredStatistics !== undefined ?
<Typography variant="h4"> <div style={{flex: 1, display: "flex", textAlign: "center",}}>
{statistics.monthly_workflow_executions} <Tooltip title={
</Typography> <Typography variant="body1" style={{padding: 10, }}>
<Typography variant="h6"> 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}.
Workflow Runs </Typography>
</Typography> }>
</Paper> <Paper style={paperStyle}>
<Paper style={paperStyle}> <Typography variant="h4">
<Typography variant="h4"> ${apprunCost}
{statistics.monthly_app_executions} </Typography>
</Typography> <Typography variant="h6">
<Typography variant="h6"> Period Cost
App Runs </Typography>
</Typography> </Paper>
</Paper> </Tooltip>
</div> <Tooltip title={
: null} <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 ? {appRuns === undefined ?
null null
@@ -267,6 +533,12 @@ const AppStats = (defaultprops) => {
: :
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"}/> <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> </div>
) )
+2 -1
View File
@@ -830,6 +830,7 @@ const RuntimeDebugger = (props) => {
); );
}} }}
/> />
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker <DateTimePicker
sx={{ sx={{
@@ -859,8 +860,8 @@ const RuntimeDebugger = (props) => {
onChange={handleEndTimeChange} onChange={handleEndTimeChange}
renderInput={(params) => <TextField {...params} />} renderInput={(params) => <TextField {...params} />}
/> />
</LocalizationProvider> </LocalizationProvider>
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
@@ -448,7 +448,7 @@ const WorkflowTemplatePopup = (props) => {
return ( 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 /> <ModalView />
<div <div
// variant={isActive === 1 ? "contained" : "outlined"} // variant={isActive === 1 ? "contained" : "outlined"}
@@ -522,7 +522,7 @@ const WorkflowTemplatePopup = (props) => {
<div style={{width: 50, }} /> <div style={{width: 50, }} />
} }
</div> </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)"}} > <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} {parsedTitle}
</Typography> </Typography>
+183 -26
View File
@@ -88,7 +88,6 @@ import Priorities from "../components/Priorities.jsx";
import Branding from "../components/Branding.jsx"; import Branding from "../components/Branding.jsx";
import Files from "../components/Files.jsx"; import Files from "../components/Files.jsx";
import { display, style } from "@mui/system"; import { display, style } from "@mui/system";
//import EnvironmentStats from "../components/EnvironmentStats.jsx";
const useStyles = makeStyles({ const useStyles = makeStyles({
notchedOutline: { notchedOutline: {
@@ -181,10 +180,12 @@ const Admin = (props) => {
const [secret2FA, setSecret2FA] = React.useState(""); const [secret2FA, setSecret2FA] = React.useState("");
const [show2faSetup, setShow2faSetup] = useState(false); const [show2faSetup, setShow2faSetup] = useState(false);
const [adminTab, setAdminTab] = React.useState(2); const [adminTab, setAdminTab] = React.useState(3);
const [showApiKey, setShowApiKey] = useState(false); const [showApiKey, setShowApiKey] = useState(false);
const [billingInfo, setBillingInfo] = React.useState({}); const [billingInfo, setBillingInfo] = React.useState({});
const [selectedStatus, setSelectedStatus] = React.useState([]); const [selectedStatus, setSelectedStatus] = React.useState([]);
const [, forceUpdate] = React.useState();
useEffect(() => { useEffect(() => {
getUsers() 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 GridItem = (props) => {
const [expanded, setExpanded] = React.useState(false); const [expanded, setExpanded] = React.useState(false);
const [showEdit, setShowEdit] = React.useState(false);
const [newValue, setNewValue] = React.useState(-100);
const primary = props.data.primary; const primary = props.data.primary;
const secondary = props.data.secondary; const secondary = props.data.secondary;
const primaryIcon = props.data.icon; const primaryIcon = props.data.icon;
const secondaryIcon = props.data.active ? ( const secondaryIcon = props.data.active ?
<CheckCircleIcon style={{ color: "green" }} /> <CheckCircleIcon style={{ color: "green" }} />
) : ( :
<CloseIcon style={{ color: "red" }} /> <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 ( return (
<Grid <Grid
item item
xs={4} xs={4}
style={{ cursor: "pointer" }}
onClick={() => {
setExpanded(!expanded);
}}
> >
<Card <Card
style={{ style={{
margin: 4, 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", color: "white",
minHeight: expanded ? 250 : "inherit", minHeight: expanded ? 250 : "inherit",
maxHeight: expanded ? 250 : "inherit", maxHeight: expanded ? 300 : "inherit",
}} }}
> >
<ListItem> <ListItem
style={{cursor: "pointer", }}
onClick={() => {
setExpanded(!expanded);
}}
>
<ListItemAvatar> <ListItemAvatar>
<Avatar>{primaryIcon}</Avatar> <Avatar>{primaryIcon}</Avatar>
</ListItemAvatar> </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" }} style={{ textTransform: "capitalize" }}
primary={primary} 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> </ListItem>
{expanded ? ( {expanded ?
<div style={{ padding: 15 }}> <div style={{ padding: 15 }}>
<Typography> <Typography>
<b>Usage:&nbsp;</b> <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>*/}
<Typography style={{maxHeight: 150, overflowX: "hidden", overflowY: "auto"}}><b>Description:</b> {secondary}</Typography> <Typography style={{maxHeight: 150, overflowX: "hidden", overflowY: "auto"}}><b>Description:</b> {secondary}</Typography>
</div> </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> </Card>
</Grid> </Grid>
); );
@@ -2658,7 +2815,7 @@ If you're interested, please let me know a time that works for you, or set up a
/> />
<Tab <Tab
label=<span> label=<span>
Licensing Billing & Stats
</span> </span>
/> />
<Tab <Tab
@@ -2858,12 +3015,12 @@ If you're interested, please let me know a time that works for you, or set up a
</div> </div>
)} )}
<Typography variant="h6" style={{ marginLeft: 5, marginTop: 40, marginBottom: 5 }}> <Typography variant="h6" style={{ marginLeft: 5, marginTop: 40, marginBottom: 5 }}>
Cloud sync features Features
</Typography> </Typography>
<Typography variant="body2" color="textSecondary" style={{marginBottom: 10, marginLeft: 5, }}> <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> </Typography>
<Grid container style={{ width: "100%", marginBottom: 15 }}> <Grid container style={{ width: "100%", marginBottom: 15, paddingBottom: 150, }}>
{selectedOrganization.sync_features === undefined || {selectedOrganization.sync_features === undefined ||
selectedOrganization.sync_features === null 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, key,
index index
) { ) {
// unnecessary parts
if (key === "schedule" || key === "apps" || key === "updates") { if (key === "schedule" || key === "apps" || key === "updates" || key === "editing") {
return null; return null;
} }
const item = selectedOrganization.sync_features[key]; const item = selectedOrganization.sync_features[key];
if (item === null) { if (item === null) {
return null return null
} }
const newkey = key.replaceAll("_", " "); const newkey = key.replaceAll("_", " ");
const griditem = { const griditem = {
+37 -2
View File
@@ -1022,7 +1022,7 @@ const AngularWorkflow = (defaultprops) => {
}) })
.then((responseJson) => { .then((responseJson) => {
console.log("GOT A RESPONSE??") console.log("GOT A RESPONSE??")
getWorkflowExecutionCount(id); // getWorkflowExecutionCount(id);
if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null) { if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null) {
// - means it's opposite // - means it's opposite
@@ -13758,6 +13758,40 @@ const AngularWorkflow = (defaultprops) => {
</div> </div>
: null : 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 = () => { const BottomCytoscapeBar = () => {
if (workflow.id === undefined || workflow.id === null || (!workflow.public && apps.length === 0)) { if (workflow.id === undefined || workflow.id === null || (!workflow.public && apps.length === 0)) {
return null; return null;
@@ -15069,7 +15103,7 @@ const AngularWorkflow = (defaultprops) => {
> >
<h2 style={{ color: "rgba(255,255,255,0.5)" }}> <h2 style={{ color: "rgba(255,255,255,0.5)" }}>
<DirectionsRunIcon style={{ marginRight: 10 }} /> <DirectionsRunIcon style={{ marginRight: 10 }} />
All Workflow Runs: { workflowExecutionCount } All Workflow Runs
</h2> </h2>
</Breadcrumbs> </Breadcrumbs>
<Tooltip <Tooltip
@@ -16514,6 +16548,7 @@ const AngularWorkflow = (defaultprops) => {
{showErrors} {showErrors}
<BottomCytoscapeBar /> <BottomCytoscapeBar />
<TopCytoscapeBar /> <TopCytoscapeBar />
<RightsideBar />
</span> </span>
} }
</div> </div>
+159 -69
View File
@@ -20,6 +20,7 @@ import {
TextField, TextField,
Tooltip, Tooltip,
Breadcrumbs, Breadcrumbs,
Drawer,
CircularProgress, CircularProgress,
Chip, Chip,
IconButton, IconButton,
@@ -43,6 +44,7 @@ import {
Loop as LoopIcon, Loop as LoopIcon,
AddPhotoAlternate as AddPhotoAlternateIcon, AddPhotoAlternate as AddPhotoAlternateIcon,
CallMerge as CallMergeIcon, CallMerge as CallMergeIcon,
CloudDownload as CloudDownloadIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
@@ -448,6 +450,8 @@ const AppCreator = (defaultprops) => {
const [openApiData, setOpenApiData] = React.useState(""); const [openApiData, setOpenApiData] = React.useState("");
const [openApiModal, setOpenApiModal] = React.useState(false); const [openApiModal, setOpenApiModal] = React.useState(false);
const [appDownloadData, setAppDownloadData] = React.useState("");
useEffect(() => { useEffect(() => {
console.log("In useEffect for openApiData: ", openApiData) console.log("In useEffect for openApiData: ", openApiData)
}, [openApiData]); }, [openApiData]);
@@ -900,8 +904,9 @@ const AppCreator = (defaultprops) => {
} }
if (newaction.url !== undefined && newaction.url !== null && newaction.url.includes("_shuffle_replace_")) { if (newaction.url !== undefined && newaction.url !== null && newaction.url.includes("_shuffle_replace_")) {
const regex = /_shuffle_replace_\d/i; //const regex = /_shuffle_replace_\d/i;
//console.log("NEW: ", const regex = /_shuffle_replace_\d+/i
newaction.url = newaction.url.replaceAll(new RegExp(regex, 'g'), "") newaction.url = newaction.url.replaceAll(new RegExp(regex, 'g'), "")
} }
@@ -1205,9 +1210,9 @@ const AppCreator = (defaultprops) => {
); );
} }
} }
} catch (e) { } catch (e) {
console.log("Param Error: ", e, path) 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", { fetch(globalUrl + "/api/v1/verify_openapi", {
method: "POST", method: "POST",
headers: { headers: {
@@ -3687,48 +3694,76 @@ const AppCreator = (defaultprops) => {
return errormessage; 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 = ( const newActionModal = (
<Dialog <Drawer
anchor={"right"}
open={actionsModalOpen} open={actionsModalOpen}
fullWidth fullWidth
PaperProps={{ PaperProps={{
style: { style: {
backgroundColor: surfaceColor, backgroundColor: surfaceColor,
color: "white", color: "white",
minWidth: 550, minWidth: 700,
maxWidth: 550, maxWidth: 700,
maxHeight: 750,
}, },
}} }}
onClose={() => { onClose={() => {
setUrlPath(""); console.log("Closing modal");
setCurrentAction({
name: "", // Old: This had some issue with arrays
description: "", //setUrlPath("");
url: "", //setCurrentAction({
file_field: "", // name: "",
headers: "", // description: "",
paths: [], // url: "",
queries: [], // file_field: "",
body: "", // headers: "",
errors: [], // paths: [],
method: actionNonBodyRequest[0], // queries: [],
action_label: "No Label", // body: "",
required_bodyfields: [], // errors: [],
}); // method: actionNonBodyRequest[0],
setCurrentActionMethod(apikeySelection[0]); // action_label: "No Label",
setUrlPathQueries([]); // required_bodyfields: [],
setActionsModalOpen(false); //});
setFileUploadEnabled(false); //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" }}> <FormControl style={{ backgroundColor: surfaceColor, color: "white" }}>
<DialogTitle> <DialogTitle style={{marginTop: 30, }}>
<div style={{ color: "white" }}>New action</div> <div style={{ color: "white" }}>New action</div>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent style={{paddingBottom: 100, }}>
<a <a
target="_blank" target="_blank"
href="https://shuffler.io/docs/app_creation#actions" href="https://shuffler.io/docs/app_creation#actions"
@@ -3826,26 +3861,33 @@ const AppCreator = (defaultprops) => {
id: "method-option", id: "method-option",
}} }}
> >
{actionNonBodyRequest.map((data, index) => {
// Add actionBodyRequest to actionNonBodyRequest
{actionNonBodyRequest.concat(actionBodyRequest).map((data, index) => {
const backgroundColor = getBackgroundColor(data);
return ( return (
<MenuItem <MenuItem
key={index} key={index}
style={{ backgroundColor: inputColor, color: "white" }} style={{}}
value={data} 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> </MenuItem>
); );
})} })}
{actionBodyRequest.map((data, index) => (
<MenuItem
key={index}
style={{ backgroundColor: inputColor, color: "white" }}
value={data}
>
{data}
</MenuItem>
))}
</Select> </Select>
<div style={{ marginTop: "15px" }} /> <div style={{ marginTop: "15px" }} />
URL path / Curl statement URL path / Curl statement
@@ -4222,19 +4264,11 @@ const AppCreator = (defaultprops) => {
/> />
{exampleResponse} {exampleResponse}
</DialogContent> </DialogContent>
<DialogActions> <div style={{position: "fixed", backgroundColor: theme.palette.surfaceColor, bottom: 0, width: "100%", padding: 25, borderTop: "1px solid rgba(255,255,255,0.3)", }}>
<Button
style={{ borderRadius: "0px" }}
onClick={() => {
setActionsModalOpen(false);
}}
>
Cancel
</Button>
<Button <Button
color="primary" color="primary"
variant={urlPath.length > 0 ? "contained" : "outlined"} variant={urlPath.length > 0 ? "contained" : "outlined"}
style={{ borderRadius: "0px" }} style={{ }}
onClick={() => { onClick={() => {
//console.log(urlPathQueries) //console.log(urlPathQueries)
//console.log(urlPath) //console.log(urlPath)
@@ -4249,9 +4283,17 @@ const AppCreator = (defaultprops) => {
> >
Submit Submit
</Button> </Button>
</DialogActions> <Button
style={{ marginLeft: 10, }}
onClick={() => {
setActionsModalOpen(false);
}}
>
Cancel
</Button>
</div>
</FormControl> </FormControl>
</Dialog> </Drawer>
); );
@@ -6103,20 +6145,68 @@ const AppCreator = (defaultprops) => {
{testView} {testView}
*/} */}
<Button <div style={{display: "flex", marginTop: 35, }}>
disabled={appBuilding} {appDownloadData.length > 0 ?
color="primary" <Tooltip title="Download the OpenAPI specification for the App" placement="bottom">
variant="contained" <IconButton
style={{ borderRadius: "0px", marginTop: "30px", height: "50px" }} style={{marginRight: 25, }}
onClick={() => { onClick={() => {
submitApp(); toast(`Downloading OpenAPI JSON data for for ${name}`)
}} // Download as file
> var blob = new Blob([appDownloadData], {
{appBuilding ? <CircularProgress /> : "Save"} type: "application/octet-stream",
</Button> });
<Typography style={{ marginTop: 5 }}>
{errorCode.length > 0 ? `Error: ${errorCode}` : null} var url = URL.createObjectURL(blob);
</Typography> 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> </Paper>
</div> </div>
); );
+45 -40
View File
@@ -921,6 +921,7 @@ const Apps = (props) => {
</Link> </Link>
: null : null
console.log("Sharing config: ", sharingConfiguration);
const activateButton = const activateButton =
selectedApp.generated && !selectedApp.activated ? ( selectedApp.generated && !selectedApp.activated ? (
<div> <div>
@@ -943,6 +944,7 @@ const Apps = (props) => {
onClick={() => { onClick={() => {
setDeleteModalOpen(true); setDeleteModalOpen(true);
}} }}
disabled={sharingConfiguration === undefined || sharingConfiguration === null || sharingConfiguration == "public"}
> >
<DeleteIcon /> <DeleteIcon />
</Button> </Button>
@@ -957,7 +959,7 @@ const Apps = (props) => {
(selectedApp.downloaded !== undefined && selectedApp.downloaded == true) || (selectedApp.downloaded !== undefined && selectedApp.downloaded == true) ||
!selectedApp.generated) && !selectedApp.generated) &&
activateButton === null ? ( activateButton === null ? (
<Tooltip title={"Delete app"}> <Tooltip title={"Delete app (confirm box will show)"}>
<Button <Button
variant="outlined" variant="outlined"
component="label" component="label"
@@ -966,6 +968,7 @@ const Apps = (props) => {
onClick={() => { onClick={() => {
setDeleteModalOpen(true); setDeleteModalOpen(true);
}} }}
disabled={sharingConfiguration === undefined || sharingConfiguration === null || sharingConfiguration == "public"}
> >
<DeleteIcon /> <DeleteIcon />
</Button> </Button>
@@ -1240,48 +1243,50 @@ const Apps = (props) => {
</Select> </Select>
{isCloud && (selectedApp.sharing === true || selectedApp.public === true || creatorProfile.github_avatar !== undefined) && !internalIds.includes(selectedApp.name.toLowerCase()) ? {isCloud && (selectedApp.sharing === true || selectedApp.public === true || creatorProfile.github_avatar !== undefined) && !internalIds.includes(selectedApp.name.toLowerCase()) ?
<Button <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">
variant="contained" <Button
component="label" variant="contained"
color="primary" component="label"
onClick={() => { color="primary"
const tmpurl = new URL(window.location.href); onClick={() => {
const searchParams = tmpurl.searchParams; const tmpurl = new URL(window.location.href);
const queryID = searchParams.get("queryID"); const searchParams = tmpurl.searchParams;
const queryID = searchParams.get("queryID");
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa("init", { aa("init", {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "db08e40265e2941b9a7d8f644b6e5240", apiKey: "db08e40265e2941b9a7d8f644b6e5240",
}); });
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
aa("sendEvents", [ aa("sendEvents", [
{ {
eventType: "conversion", eventType: "conversion",
eventName: "Public App Activated", eventName: "Public App Activated",
index: "appsearch", index: "appsearch",
objectIDs: [selectedApp.id], objectIDs: [selectedApp.id],
timestamp: timestamp, timestamp: timestamp,
queryID: queryID, queryID: queryID,
userToken: userToken:
userdata === undefined || userdata === undefined ||
userdata === null || userdata === null ||
userdata.id === undefined userdata.id === undefined
? "unauthenticated" ? "unauthenticated"
: userdata.id, : userdata.id,
}, },
]); ]);
} else { } else {
console.log("No query to handle when activating"); console.log("No query to handle when activating");
} }
activateApp(selectedApp.id, true) activateApp(selectedApp.id, true)
}} }}
style={{ height: 35, marginTop: 0, marginLeft: 10, }} style={{ height: 35, marginTop: 0, marginLeft: 10, }}
> >
Deactivate Deactivate
</Button> </Button>
</Tooltip>
: null} : null}
</div> </div>
) : null} ) : null}