diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 3c683c6b..3937b052 100755 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -3214,7 +3214,7 @@ class AppBase: self.action_result["result"] = json.dumps({ "success": False, "reason": f"Function {actionname} doesn't exist, or the App is out of date.", - "details": "If this persists, please restart delete the Docker image locally, restart your Orborus instance and then try again to force-download the latest version. Contact support@shuffler.io with this data if the issue persists.", + "details": "If this persists, please delete the Docker image locally, then restart your Orborus instance before trying again. This will force-download the latest version. Contact support@shuffler.io with this data if the issue persists.", }) elif callable(func): try: diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index a91c275a..e4354db0 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -3,6 +3,8 @@ import React, { useState, useEffect } from 'react'; import theme from '../theme.jsx'; import classNames from "classnames"; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' +import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid' +import { toast } from "react-toastify" import { DatePicker, @@ -11,6 +13,12 @@ import { } from '@mui/x-date-pickers' import { + OpenInNew as OpenInNewIcon, +} from '@mui/icons-material'; + +import { + CircularProgress, + Link, Tooltip, TextField, IconButton, @@ -24,39 +32,8 @@ import { import { BarChart, - RadialBarChart, - RadialAreaChart, - RadialAxis, - StackedBarSeries, - TooltipArea, - ChartTooltip, - TooltipTemplate, - RadialAreaSeries, - RadialPointSeries, - RadialArea, - RadialLine, - TreeMap, - TreeMapSeries, - TreeMapLabel, - TreeMapRect, - Line, - LineChart, - LineSeries, - LinearYAxis, - LinearXAxis, - LinearYAxisTickSeries, - LinearXAxisTickSeries, - Area, - AreaChart, - AreaSeries, - AreaSparklineChart, - PointSeries, GridlineSeries, Gridline, - Stripes, - Gradient, - GradientStop, - LinearXAxisTickLabel, } from 'reaviz'; import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; @@ -84,7 +61,7 @@ const LineChartWrapper = ({keys, inputname, height, width}) => { const AppStats = (defaultprops) => { - const { globalUrl, selectedOrganization, userdata, isCloud, } = defaultprops; + const { globalUrl, selectedOrganization, userdata, isCloud, inputWorkflows, } = defaultprops; const [keys, setKeys] = useState([]) const [searches, setSearches] = useState([]); @@ -102,8 +79,154 @@ const AppStats = (defaultprops) => { const [monthToDateCost, setMonthToDateCost] = useState(0) const [monthTotalCost, setMonthTotalCost] = useState(0) + const [workflows, setWorkflows] = useState(inputWorkflows === undefined ? [] : inputWorkflows) + const [resultRows, setResultRows] = useState([]) + const [resultLoading, setResultLoading] = useState(true) + const includedExecutions = selectedOrganization.sync_features.app_executions !== undefined ? selectedOrganization.sync_features.app_executions.limit : 0 + useEffect(() => { + if (workflows === undefined || workflows === null || workflows.length === 0) { + getAvailableWorkflows() + } + }, []) + + + + const getWorkflowStats = async (workflow, startTime, endTime) => { + if (!userdata.support) { + return workflow + } + + if (workflow.id === undefined || workflow.id === null || workflow.id === "") { + return workflow + } + + var starttime = "" + var endtime = "" + try { + starttime = startTime === undefined || startTime === null || startTime === "" ? "" : new Date(startTime).toISOString() + endtime = endTime === undefined || endTime === null || endTime == "" ? "" : new Date(endTime).toISOString() + } catch(err) { + console.log("Error converting start/end time", err) + toast("Bad start/endtime. Please try again") + return + } + + console.log("START TIME", starttime, endtime) + + var url = `${globalUrl}/api/v1/workflows/${workflow.id}/executions/count` + + if (starttime !== "") { + url += `?start_time=${starttime}` + } + + if (endtime !== "") { + if (starttime !== "") { + url += `&end_time=${endtime}` + } else { + url += `?end_time=${endtime}` + } + } + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + + if (response.status !== 200) { + console.log("Status not 200 for workflow stats URL: " + url); + return workflow + } + + const data = await response.json(); + + if (data === undefined || data === null) { + console.log("No data for workflow stats URL: " + url); + return workflow + } + + if (data.success === false) { + console.log("No success for workflow stats URL: " + url, data.reason); + return workflow + } + + workflow.runcount = data.count + return workflow + } + + const loadWorkflowStats = (foundWorkflows, startTime, endTime) => { + if (!userdata.support) { + console.log("Not support") + + return + } + + if (foundWorkflows === undefined || foundWorkflows === null || foundWorkflows.length === 0) { + console.log("Not workflows") + return + } + + // Only do latest 20 + setResultLoading(true) + const promises = foundWorkflows.slice(0, 50).map(wf => getWorkflowStats(wf, startTime, endTime)); + + const allData = Promise.all(promises); + + allData.then((data) => { + console.log("IN ALL DATA") + + var total = 0 + for (var i = 0; i < data.length; i++) { + if (data[i].runcount !== undefined) { + total += data[i].runcount + } else { + data[i].runcount = 0 + } + } + + data[0].runcount = total + // Sort data by runcount + data.sort((a, b) => (a.runcount < b.runcount) ? 1 : -1) + setResultRows(data) + setResultLoading(false) + }) + } + + const getAvailableWorkflows = () => { + fetch(globalUrl + "/api/v1/workflows", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + return; + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson !== undefined) { + var foundWorkflows = [{"id": "", "name": "All Workflows",}] + foundWorkflows.push(...responseJson) + setWorkflows(foundWorkflows) + + loadWorkflowStats(foundWorkflows) + } + }) + .catch((error) => { + console.log("Error getting workflows: " + error); + }) + } + // Cost in old contracts: 0.0009 // Old contracts also always included 150.000 executions const invocationCost = includedExecutions === 150000 || includedExecutions === 250000 ? 0.0009 : typecost_single @@ -242,6 +365,15 @@ const AppStats = (defaultprops) => { setFilteredStatistics(tmpstats) handleDataSetting(tmpstats, "day") + + if (workflows !== undefined && workflows !== null && workflows.length > 0) { + var foundWorkflows = [{"id": "", "name": "All Workflows",}] + var tmpworkflows = workflows.filter((workflow) => workflow.id !== undefined && workflow.id !== null && workflow.id !== "") + foundWorkflows.push(...tmpworkflows) + + loadWorkflowStats(foundWorkflows, startTime, endTime) + } + }, [statistics, startTime, endTime]) const handleStartTimeChange = (date) => { @@ -400,6 +532,112 @@ const AppStats = (defaultprops) => { maxWidth: 300, } + const columns: GridColDef[] = [ + { + field: 'workflow.name', + headerName: 'Workflow Name', + width: 350, + renderCell: (params) => { + + return ( + { + }}> + {params.row.name} + + ) + } + }, + { + field: 'workflow.runcount', + headerName: 'Workflow Runs in selected period', + width: 250, + renderCell: (params) => { + + return ( + { + }}> + {params.row.runcount} + + ) + } + }, + { + field: 'triggers', + headerName: 'Triggers', + width: 100, + renderCell: (params) => { + if (params.row.id === "") { + return null + } + + const cnt = params.row.triggers === undefined || params.row.triggers === null ? 0 : params.row.triggers.length + + return ( + { + }}> + {cnt} + + ) + } + }, + { + field: 'actions', + headerName: 'Actions', + width: 100, + renderCell: (params) => { + if (params.row.id === "") { + return null + } + + const cnt = params.row.actions === undefined || params.row.actions === null ? 0 : params.row.actions.length + + return ( + { + }}> + {cnt} + + ) + } + }, + /*{ + field: 'last editor', + headerName: 'Last Editor', + width: 100, + renderCell: (params) => { + if (params.row.id === "") { + return null + } + + const lastEditor = params.row.lasteditor === undefined || params.row.lasteditor === null ? "" : params.row.lasteditor + + return ( + { + }}> + {lastEditor} + + ) + } + },*/ + { + field: 'explore', + headerName: 'Explore', + width: 100, + renderCell: (params) => { + if (params.row.id === "") { + return null + } + + return ( + + + + + + ) + } + }, + ] + const data = (
@@ -421,7 +659,7 @@ const AppStats = (defaultprops) => { }> - {selectedOrganization.lead_info.customer === false && selectedOrganization.lead_info.pov === false ? + ${selectedOrganization.lead_info.customer === false && selectedOrganization.lead_info.pov === false ? 0 : apprunCost @@ -543,6 +781,57 @@ const AppStats = (defaultprops) => { : */} + + +
+ {resultLoading ? +
+ + Loading usage for selected period (may take a while) + + +
+ : + { + //setRowsPerPage(newPageSize) + //submitSearch(workflowId, status, startTime, endTime, rowCursor, newPageSize) + }} + // event for when clicking next page + // Hide page changer + onPageChange={(params) => { + console.log("page params: ", params) + }} + onSelectionModelChange={(newSelection) => { + console.log("newSelection: ", newSelection) + //console.log("newSelection: ", newSelection) + //setSelectedWorkflowExecutionsIndexes(newSelection) + //var found = [] + //for (var i = 0; i < newSelection.length; i++) { + // // Find the workflow in the resultRows + // var selected = resultRows.find((workflow) => { + // return workflow.id === newSelection[i] + // }) + + // if (selected === undefined || selected === null) { + // continue + // } + + // found.push(selected) + //} + + //setSelectedWorkflowExecutions(found) + }} + // Track which items are selected + /> + } +
) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 92a57baa..f22ee36b 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -15083,6 +15083,10 @@ const AngularWorkflow = (defaultprops) => { open={executionModalOpen} onClose={() => { setExecutionModalOpen(false) + + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) }} style={{ resize: "both", overflow: "auto", }} hideBackdrop={false}