From f7c0c6e5e4a2e08f0eaff3d3b4da8efb506465f3 Mon Sep 17 00:00:00 2001 From: Frikky Date: Sun, 21 Jan 2024 22:11:10 +0100 Subject: [PATCH] Added more management tools for workflow running and stat management --- frontend/src/components/Billing.jsx | 1 + frontend/src/components/BillingStats.jsx | 374 +++++++++++++++--- frontend/src/components/RuntimeDebugger.jsx | 3 +- .../src/components/WorkflowTemplatePopup.jsx | 4 +- frontend/src/views/Admin.jsx | 209 ++++++++-- frontend/src/views/AngularWorkflow.jsx | 39 +- frontend/src/views/AppCreator.jsx | 228 +++++++---- frontend/src/views/Apps.jsx | 85 ++-- 8 files changed, 752 insertions(+), 191 deletions(-) diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 2327a089..4152ce2a 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -1230,6 +1230,7 @@ const Billing = (props) => { { const [hovered, setHovered] = useState(""); const inputdata = keys.data === undefined ? keys : keys.data return ( -
+
{inputname} @@ -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 = (
- All Stat widgets are monthly and gathered from Your Organization Statistics. - This is a feature to help give you more insight into Shuffle, and will be populating over time. + >Your Organization Statistics + This is a feature to help give you more insight into Shuffle, and to understand your utilization of the Shuffle platform. The billing tracker is in Beta, and is always calculated manually before being invoiced. - {statistics !== undefined ? -
- - - {statistics.monthly_workflow_executions} - - - Workflow Runs - - - - - {statistics.monthly_app_executions} - - - App Runs - - -
- : null} + +
+ {filteredStatistics !== undefined ? +
+ + 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}. + + }> + + + ${apprunCost} + + + Period Cost + + + + + App runs in the selected period + + }> + + + {filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions} + + + App Runs + + + + + Workflow runs in the selected period + + }> + + + {filteredStatistics.monthly_workflow_executions === null || filteredStatistics.monthly_workflow_executions === undefined ? 0 : filteredStatistics.monthly_workflow_executions} + + + Workflow Runs + + + + + 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}. + + }> + + + ${monthTotalCost} + + + Estimated cost + + + +
+ : null} + + +
+ } + /> + } + /> +
+
+ +
{appRuns === undefined ? null @@ -267,6 +533,12 @@ const AppStats = (defaultprops) => { : } + + {/*appRunCosts === undefined ? + null + : + + */}
) diff --git a/frontend/src/components/RuntimeDebugger.jsx b/frontend/src/components/RuntimeDebugger.jsx index 5810cbab..570778df 100644 --- a/frontend/src/components/RuntimeDebugger.jsx +++ b/frontend/src/components/RuntimeDebugger.jsx @@ -830,6 +830,7 @@ const RuntimeDebugger = (props) => { ); }} /> + { onChange={handleEndTimeChange} renderInput={(params) => } /> - + + + + : null} ); @@ -2658,7 +2815,7 @@ If you're interested, please let me know a time that works for you, or set up a /> - Licensing + Billing & Stats /> )} - Cloud sync features + Features - 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. - + {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 = { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 579ca8d1..650c2e9d 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -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) => {
: null + const RightsideBar = () => { + const [hovered, setHovered] = useState(false) + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + onClick={() => { + setExecutionModalOpen(true); + getWorkflowExecution(props.match.params.key, ""); + }} + > + + + Explore runs + + {/* */} +
+ ) + } + const BottomCytoscapeBar = () => { if (workflow.id === undefined || workflow.id === null || (!workflow.public && apps.length === 0)) { return null; @@ -15069,7 +15103,7 @@ const AngularWorkflow = (defaultprops) => { >

- All Workflow Runs: { workflowExecutionCount } + All Workflow Runs

{ {showErrors} + }
diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index d2a74d71..33184cc0 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -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 = ( - { - 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); }} > - +
New action
- + { id: "method-option", }} > - {actionNonBodyRequest.map((data, index) => { + + // Add actionBodyRequest to actionNonBodyRequest + {actionNonBodyRequest.concat(actionBodyRequest).map((data, index) => { + const backgroundColor = getBackgroundColor(data); return ( - {data} + ); })} - {actionBodyRequest.map((data, index) => ( - - {data} - - ))}
URL path / Curl statement @@ -4222,19 +4264,11 @@ const AppCreator = (defaultprops) => { /> {exampleResponse} - - +
- + +
-
+ ); @@ -6103,20 +6145,68 @@ const AppCreator = (defaultprops) => { {testView} */} - - - {errorCode.length > 0 ? `Error: ${errorCode}` : null} - +
+ {appDownloadData.length > 0 ? + + { + 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); + }} + > + + + + : null} + + {appDownloadData.length > 0 ? +
+ : null} +
+ + + {errorCode.length > 0 ? `Upload Error: ${errorCode}` : null} + +
); diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 0acface3..6234b535 100755 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -921,6 +921,7 @@ const Apps = (props) => { : null + console.log("Sharing config: ", sharingConfiguration); const activateButton = selectedApp.generated && !selectedApp.activated ? (
@@ -943,6 +944,7 @@ const Apps = (props) => { onClick={() => { setDeleteModalOpen(true); }} + disabled={sharingConfiguration === undefined || sharingConfiguration === null || sharingConfiguration == "public"} > @@ -957,7 +959,7 @@ const Apps = (props) => { (selectedApp.downloaded !== undefined && selectedApp.downloaded == true) || !selectedApp.generated) && activateButton === null ? ( - + @@ -1240,48 +1243,50 @@ const Apps = (props) => { {isCloud && (selectedApp.sharing === true || selectedApp.public === true || creatorProfile.github_avatar !== undefined) && !internalIds.includes(selectedApp.name.toLowerCase()) ? - + activateApp(selectedApp.id, true) + }} + style={{ height: 35, marginTop: 0, marginLeft: 10, }} + > + Deactivate + + : null}
) : null}