diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py
index a29853d8..b1d7bac3 100755
--- a/backend/app_sdk/app_base.py
+++ b/backend/app_sdk/app_base.py
@@ -3213,7 +3213,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 (
+
+
+