Cloud sync fixes

This commit is contained in:
Frikky
2025-06-02 18:15:01 +02:00
parent 02fd54430b
commit 86ae5dc59d
7 changed files with 358 additions and 274 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ require (
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/h2non/filetype v1.1.3 github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.8.71 github.com/shuffle/shuffle-shared v0.8.72
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.37.0
google.golang.org/api v0.228.0 google.golang.org/api v0.228.0
google.golang.org/grpc v1.71.1 google.golang.org/grpc v1.71.1
+73 -42
View File
@@ -11,17 +11,18 @@ import (
"crypto/md5" "crypto/md5"
"strconv" "strconv"
"os"
"io"
"log"
"fmt"
"errors"
"net/url"
"os/exec"
"net/http"
"io/ioutil"
"math/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@@ -60,7 +61,8 @@ var baseDockerName = "frikky/shuffle"
var registryName = "registry.hub.docker.com" var registryName = "registry.hub.docker.com"
var runningEnvironment = "onprem" var runningEnvironment = "onprem"
var syncUrl = "https://shuffler.io" //var syncUrl = "https://shuffler.io"
var syncUrl = "http://localhost:5002"
type retStruct struct { type retStruct struct {
Success bool `json:"success"` Success bool `json:"success"`
@@ -3805,32 +3807,55 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error {
} }
} }
if org.SyncConfig.WorkflowBackup { shouldBackupData := false
workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser, 250, "") randomNumber := rand.Intn(20)
if err != nil { if randomNumber == 0 {
log.Printf("[ERROR] Failed getting backup workflows for org %s: %s", org.Id, err) shouldBackupData = true
} else {
backupJob.Workflows = workflows
}
} }
if org.SyncConfig.AppBackup && len(org.Users) > 0 { // Check if it's 1/20 times (600 seconds - 10 min on average)
// Just to prevent it from spamming large outbound requests
apps, err := shuffle.GetPrioritizedApps(ctx, foundUser) if shouldBackupData {
if err != nil { if org.SyncConfig.WorkflowBackup {
log.Printf("[ERROR] Failed getting backup apps for org %s: %s", org.Id, err) workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser, 250, "")
} else { if err != nil {
backupJob.Apps = apps log.Printf("[ERROR] Failed getting backup workflows for org %s: %s", org.Id, err)
} else {
backupJob.Workflows = workflows
}
} }
}
// Send stats once every 10 times or so..? if org.SyncConfig.AppBackup && len(org.Users) > 0 {
// For now, just send every time foundUser.ActiveOrg.Id = org.Id
info, err := shuffle.GetOrgStatistics(ctx, org.Id) apps, err := shuffle.GetPrioritizedApps(ctx, foundUser)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed getting org statistics backup for org %s: %s", org.Id, err) log.Printf("[ERROR] Failed getting backup apps for org %s: %s", org.Id, err)
} else { } else {
backupJob.Stats = *info parsedApps := []shuffle.WorkflowApp{}
for _, app := range apps {
if len(app.Actions) == 0 {
continue
}
if !app.Generated {
continue
}
parsedApps = append(parsedApps, app)
}
backupJob.Apps = parsedApps
}
}
// Send stats once every 10 times or so..?
// For now, just send every time
info, err := shuffle.GetOrgStatistics(ctx, org.Id)
if err != nil {
log.Printf("[ERROR] Failed getting org statistics backup for org %s: %s", org.Id, err)
} else {
backupJob.Stats = *info
}
} }
backupJobData, err := json.Marshal(backupJob) backupJobData, err := json.Marshal(backupJob)
@@ -3867,6 +3892,7 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error {
//log.Printf("[ERROR] Failed cloud sync job controller run for '%s': %s", respBody, err) //log.Printf("[ERROR] Failed cloud sync job controller run for '%s': %s", respBody, err)
return err return err
} }
return nil return nil
} }
@@ -4004,6 +4030,8 @@ func runInitEs(ctx context.Context) {
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
} }
// FIXME: This should ONLY run on one backend instance
schedules, err := shuffle.GetAllSchedules(ctx, "ALL") schedules, err := shuffle.GetAllSchedules(ctx, "ALL")
if err != nil { if err != nil {
log.Printf("[WARNING] Failed getting schedules during service init: %s", err) log.Printf("[WARNING] Failed getting schedules during service init: %s", err)
@@ -4147,7 +4175,7 @@ func runInitEs(ctx context.Context) {
} }
//interval := int(org.SyncConfig.Interval) //interval := int(org.SyncConfig.Interval)
interval := 15 interval := 30
if interval == 0 { if interval == 0 {
log.Printf("[WARNING] Skipping org %s because sync isn't set (0).", org.Id) log.Printf("[WARNING] Skipping org %s because sync isn't set (0).", org.Id)
continue continue
@@ -4249,17 +4277,17 @@ func runInitEs(ctx context.Context) {
continue continue
} }
if newresp.StatusCode != 200 {
log.Printf("[WARNING] Failed stopping runs in environment %s. Status code: %d", environment, newresp.StatusCode) respBody, err := ioutil.ReadAll(newresp.Body)
if err != nil {
log.Printf("[ERROR] Failed setting respbody %s for execution stop. Status: %d", err, newresp.StatusCode)
continue continue
} }
//respBody, err := ioutil.ReadAll(newresp.Body) if newresp.StatusCode != 200 {
//if err != nil { log.Printf("[WARNING] Failed stopping runs in environment %s. Status code: %d. Body: %s", environment, newresp.StatusCode, string(respBody))
// log.Printf("[ERROR] Failed setting respbody %s", err) continue
// continue }
//}
//log.Printf("[DEBUG] Successfully ran workflow cleanup request for %s. Body: %s", environment, string(respBody))
url = fmt.Sprintf("http://localhost:%s/api/v1/environments/%s/rerun", backendPort, environment) url = fmt.Sprintf("http://localhost:%s/api/v1/environments/%s/rerun", backendPort, environment)
req, err = http.NewRequest( req, err = http.NewRequest(
@@ -4677,7 +4705,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
// If you want to disable cloud sync, see previous section. // If you want to disable cloud sync, see previous section.
if org.CloudSync { if org.CloudSync {
log.Printf("[WARNING] Org %s is already syncing. Skip", org.Id) log.Printf("[WARNING] Org %s is already syncing. Skip", org.Id)
resp.WriteHeader(401) resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Your org is already syncing. Nothing to set up."}`))) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Your org is already syncing. Nothing to set up."}`)))
return return
} }
@@ -4754,6 +4782,9 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
org.SyncConfig = shuffle.SyncConfig{ org.SyncConfig = shuffle.SyncConfig{
Apikey: responseData.SessionKey, Apikey: responseData.SessionKey,
Interval: responseData.IntervalSeconds, Interval: responseData.IntervalSeconds,
WorkflowBackup: true,
AppBackup: true,
} }
interval := int(responseData.IntervalSeconds) interval := int(responseData.IntervalSeconds)
+47 -26
View File
@@ -2599,40 +2599,50 @@ const Billing = memo((props) => {
Utilization & Stats Utilization & Stats
</Typography> </Typography>
</div> </div>
{isChildOrg ? ( <span>
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
/>
): (
<span>
<Tabs <Tabs
value={currentTab} value={currentTab}
onChange={(event, newValue) => setCurrentTab(newValue)} onChange={(event, newValue) => {
setCurrentTab(-1)
// Force re-render
setTimeout(() => {
setCurrentTab(newValue)
}, 100);
}}
style={{ marginTop: 20 }} style={{ marginTop: 20 }}
TabIndicatorProps={{ TabIndicatorProps={{
style: { style: {
height: 3, height: 3,
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
marginLeft: 12, marginLeft: 12,
marginRight: 12, marginRight: 12,
} }
}} }}
> >
<Tab <Tab
label="Parent Organization" label="Parent Organization"
style={{ textTransform: 'none', fontSize: 16, minWidth: 'auto', paddingLeft: 12, paddingRight: 12 }} style={{ textTransform: 'none',}}
/> value={0}
<Tab />
label="Child Organization Stats"
style={{ textTransform: 'none', fontSize: 16, minWidth: 'auto', paddingLeft: 12, paddingRight: 12 }} {isCloud ?
/> <Tab
label="Cloud-Synced Stats"
style={{ textTransform: 'none', }}
value={1}
/>
: null}
<Tab
label="Child Organization Stats"
disabled={isChildOrg}
style={{ textTransform: 'none', }}
value={2}
/>
</Tabs> </Tabs>
<div style={{paddingBottom: 200, }}> <div style={{paddingBottom: 200, minHeight: 750, }}>
{currentTab === 0 ? {currentTab === 0 ?
<div style={{ marginTop: 30,}}> <div style={{ marginTop: 30,}}>
<BillingStats <BillingStats
@@ -2643,6 +2653,18 @@ const Billing = memo((props) => {
userdata={userdata} userdata={userdata}
/> />
</div> </div>
: currentTab === 1 ?
<div style={{ marginTop: 30,}}>
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
syncStats={true}
/>
</div>
: :
<BillingStatsChildOrg <BillingStatsChildOrg
isCloud={isCloud} isCloud={isCloud}
@@ -2657,8 +2679,7 @@ const Billing = memo((props) => {
/> />
} }
</div> </div>
</span> </span>
)}
</div> </div>
</Wrapper> </Wrapper>
) )
+126 -102
View File
@@ -84,7 +84,15 @@ const LineChartWrapper = ({keys, inputname, height, width}) => {
const AppStats = (defaultprops) => { const AppStats = (defaultprops) => {
const { globalUrl, selectedOrganization, userdata, isCloud, inputWorkflows,clickedFromOrgTab } = defaultprops; const {
globalUrl,
selectedOrganization,
userdata,
isCloud,
inputWorkflows,
clickedFromOrgTab,
syncStats,
} = defaultprops;
const [keys, setKeys] = useState([]) const [keys, setKeys] = useState([])
const [searches, setSearches] = useState([]); const [searches, setSearches] = useState([]);
@@ -119,9 +127,6 @@ const AppStats = (defaultprops) => {
const getWorkflowStats = async (workflow, startTime, endTime) => { const getWorkflowStats = async (workflow, startTime, endTime) => {
if (!userdata.support) {
return workflow
}
if (workflow.id === undefined || workflow.id === null || workflow.id === "") { if (workflow.id === undefined || workflow.id === null || workflow.id === "") {
return workflow return workflow
@@ -186,12 +191,8 @@ const AppStats = (defaultprops) => {
} }
const loadWorkflowStats = (foundWorkflows, startTime, endTime) => { const loadWorkflowStats = (foundWorkflows, startTime, endTime) => {
if (!userdata.support) {
return
}
if (foundWorkflows === undefined || foundWorkflows === null || foundWorkflows.length === 0) { if (foundWorkflows === undefined || foundWorkflows === null || foundWorkflows.length === 0) {
console.log("Not workflows") setResultLoading(false)
return return
} }
@@ -200,6 +201,9 @@ const AppStats = (defaultprops) => {
const promises = foundWorkflows.slice(0, 50).map(wf => getWorkflowStats(wf, startTime, endTime)); const promises = foundWorkflows.slice(0, 50).map(wf => getWorkflowStats(wf, startTime, endTime));
const allData = Promise.all(promises); const allData = Promise.all(promises);
if (allData === undefined || allData === null) {
setResultLoading(false)
}
allData.then((data) => { allData.then((data) => {
var total = 0 var total = 0
@@ -259,15 +263,16 @@ const AppStats = (defaultprops) => {
return return
} }
if (statistics["daily_statistics"] === undefined || statistics["daily_statistics"] === null) { const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
if (statistics[statKey] === undefined || statistics[statKey] === null) {
setFilteredStatistics(statistics) setFilteredStatistics(statistics)
return return
} }
// Calculate month to date cost // Calculate month to date cost
var mtd_cost = 0 var mtd_cost = 0
for (let key in statistics["daily_statistics"]) { for (let key in statistics[statKey]) {
const item = statistics["daily_statistics"][key] const item = statistics[statKey][key]
if (item["date"] === undefined) { if (item["date"] === undefined) {
continue continue
} }
@@ -325,8 +330,8 @@ const AppStats = (defaultprops) => {
// Check if start time is before the daily statistics["date"] string // Check if start time is before the daily statistics["date"] string
var newlist = [] var newlist = []
for (let key in statistics["daily_statistics"]) { for (let key in statistics[statKey]) {
const item = statistics["daily_statistics"][key] const item = statistics[statKey][key]
if (item["date"] === undefined) { if (item["date"] === undefined) {
continue continue
} }
@@ -357,7 +362,7 @@ const AppStats = (defaultprops) => {
var appexecutions = 0 var appexecutions = 0
var estimatedcost = 0 var estimatedcost = 0
if (newlist.length > 0) { if (newlist.length > 0) {
tmpstats["daily_statistics"] = newlist tmpstats[statKey] = newlist
for (let key in newlist) { for (let key in newlist) {
const item = newlist[key] const item = newlist[key]
@@ -411,7 +416,8 @@ const AppStats = (defaultprops) => {
return return
} }
const dailyStats = inputdata.daily_statistics const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
const dailyStats = inputdata[statKey]
if (dailyStats === undefined || dailyStats === null) { if (dailyStats === undefined || dailyStats === null) {
return return
} }
@@ -705,44 +711,57 @@ const AppStats = (defaultprops) => {
style={{ textDecoration: "none", color: theme.palette.linkColor,}} style={{ textDecoration: "none", color: theme.palette.linkColor,}}
>Your Organisation Statistics. </a> >Your Organisation Statistics. </a>
It exists to give you more insight into your workflows, 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> It exists to give you more insight into your workflows, 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>
<br style={{}}/>
{syncStats !== true ? null :
"PS: You are currently looking at data from your onprem synced org"}
</Typography> </Typography>
<div style={{display: "flex", flexDirection: "column", textAlign: "center",}}> <div style={{display: "flex", flexDirection: "column", textAlign: "center",}}>
<div style={{flexDirection: "row", }}> <div style={{flexDirection: "row", }}>
{filteredStatistics !== undefined ? {filteredStatistics !== undefined ?
<div style={{flex: 1, display: "flex", textAlign: "center",}}> <div style={{flex: 1, display: "flex", textAlign: "center",}}>
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}> {syncStats == true ? null :
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}. <Tooltip title={
</Typography> <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}.
<Box sx={paperStyle}>
<Typography variant="h4">
${selectedOrganization?.lead_info?.customer === false && selectedOrganization?.lead_info?.pov === false ?
0
:
apprunCost
}
</Typography> </Typography>
<Typography variant="h6"> }>
Period Cost <Box sx={paperStyle}>
</Typography> <Typography variant="h4">
</Box> ${selectedOrganization?.lead_info?.customer === false && selectedOrganization?.lead_info?.pov === false ?
</Tooltip> 0
:
apprunCost
}
</Typography>
<Typography variant="h6">
Period Cost
</Typography>
</Box>
</Tooltip>
}
{syncStats === true ? null :
<Tooltip title={ <Tooltip title={
<Typography variant="body1" style={{padding: 10, }}> <Typography variant="body1" style={{padding: 10, }}>
App runs in the selected period App runs in the selected period
</Typography> </Typography>
}> }>
<Box sx={paperStyle}> <Box sx={paperStyle}>
<Typography variant="h4"> <Typography variant="h4">
{filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions} {filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions}
</Typography> </Typography>
<Typography variant="h6"> <Typography variant="h6">
App Runs App Runs
</Typography> </Typography>
</Box> </Box>
</Tooltip> </Tooltip>
}
{syncStats === true ? null :
<Tooltip title={ <Tooltip title={
<Typography variant="body1" style={{padding: 10, }}> <Typography variant="body1" style={{padding: 10, }}>
Workflow runs in the selected period Workflow runs in the selected period
@@ -757,20 +776,24 @@ const AppStats = (defaultprops) => {
</Typography> </Typography>
</Box> </Box>
</Tooltip> </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}. {syncStats === true ? null :
</Typography> <Tooltip title={
}> <Typography variant="body1" style={{padding: 10, }}>
<Box sx={paperStyle}> 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 variant="h4">
${monthTotalCost}
</Typography> </Typography>
<Typography variant="h6"> }>
Estimated cost <Box sx={paperStyle}>
</Typography> <Typography variant="h4">
</Box> ${monthTotalCost}
</Tooltip> </Typography>
<Typography variant="h6">
Estimated cost
</Typography>
</Box>
</Tooltip>
}
</div> </div>
: null} : null}
</div> </div>
@@ -968,57 +991,58 @@ const AppStats = (defaultprops) => {
<LineChartWrapper keys={appRunCosts} height={300} width={"100%"} inputname={"Apprun cost - Cost per day"}/> <LineChartWrapper keys={appRunCosts} height={300} width={"100%"} inputname={"Apprun cost - Cost per day"}/>
*/} */}
{syncStats === true ? null :
<div style={{height: 150+resultRows.length * 25, padding: "10px 0px 10px 0px", }}>
{resultLoading ?
<div style={{margin: "auto", alignItems: "center", width: 350, height: "100%", }}>
<Typography variant="body2" color="textSecondary" component="p" style={{textAlign: "center", marginTop: 50, marginBottom: 15, }}>
Loading usage for selected period (may take a while)
<CircularProgress style={{marginTop: 15, }} />
</Typography>
</div>
:
<DataGrid
rows={resultRows}
columns={columns}
pageSize={100}
rowsPerPageOptions={[10, 20, 50, 100]}
checkboxSelection
disableSelectionOnClick
onPageSizeChange={(newPageSize) => {
//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]
// })
<div style={{height: 150+resultRows.length * 25, padding: "10px 0px 10px 0px", }}> // if (selected === undefined || selected === null) {
{resultLoading ? // continue
<div style={{margin: "auto", alignItems: "center", width: 350, height: "100%", }}> // }
<Typography variant="body2" color="textSecondary" component="p" style={{textAlign: "center", marginTop: 50, marginBottom: 15, }}>
Loading usage for selected period (may take a while)
<CircularProgress style={{marginTop: 15, }} />
</Typography>
</div>
:
<DataGrid
rows={resultRows}
columns={columns}
pageSize={100}
rowsPerPageOptions={[10, 20, 50, 100]}
checkboxSelection
disableSelectionOnClick
onPageSizeChange={(newPageSize) => {
//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) { // found.push(selected)
// continue //}
// }
// found.push(selected) //setSelectedWorkflowExecutions(found)
//} }}
// Track which items are selected
//setSelectedWorkflowExecutions(found) />
}} }
// Track which items are selected </div>
/> }
}
</div>
</div> </div>
) )
+4 -4
View File
@@ -483,7 +483,7 @@ const CloudSyncTab = (props) => {
} else { } else {
toast("Cloud Syncronization successfully set up!"); toast("Cloud Syncronization successfully set up!");
setOrgSyncResponse( setOrgSyncResponse(
"Successfully started syncronization. Cloud features you now have access to can be seen below." "Successfully started syncronization. Cloud/Hybrid features are available below."
); );
} }
@@ -541,8 +541,8 @@ const CloudSyncTab = (props) => {
Cloud syncronization Cloud syncronization
</Typography> </Typography>
<Typography variant="body2" style={{ color: theme.palette.text.secondary, fontSize: 16, fontWeight: 400, }}> <Typography variant="body2" style={{ color: theme.palette.text.secondary, fontSize: 16, fontWeight: 400, }}>
What does <a href="/docs/organizations#cloud_sync" target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.linkColor, fontSize: 16, textDecoration: 'none', }}>cloud sync</a> do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. What does <a href="/docs/organizations#cloud_sync" target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.linkColor, fontSize: 16, textDecoration: 'none', }}>cloud sync</a> do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. This will by default back up apps and workflows.
</Typography> </Typography>
</div> </div>
{isCloud ? ( {isCloud ? (
@@ -714,7 +714,7 @@ const CloudSyncTab = (props) => {
)} )}
<Typography variant="h5" style={{ marginLeft: 5, marginTop: 40, marginBottom: 5 }}> <Typography variant="h5" style={{ marginLeft: 5, marginTop: 40, marginBottom: 5 }}>
Features {isCloud ? "Cloud" : "Hybrid"} Features
</Typography> </Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 24, fontSize: 16, fontWeight: 400, marginLeft: 5, color: theme.palette.text.secondary }}> <Typography variant="body2" color="textSecondary" style={{ marginBottom: 24, fontSize: 16, fontWeight: 400, marginLeft: 5, color: theme.palette.text.secondary }}>
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> 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>
+61 -95
View File
@@ -1122,6 +1122,7 @@ const Apps2 = (props) => {
const [defaultSearch, setDefaultSearch] = useState(""); const [defaultSearch, setDefaultSearch] = useState("");
const [apps, setApps] = useState([]); const [apps, setApps] = useState([]);
const [backupApps, setBackupApps] = useState([]);
const [filteredApps, setFilteredApps] = useState([]); const [filteredApps, setFilteredApps] = useState([]);
const [appSearchLoading, setAppSearchLoading] = useState(false); const [appSearchLoading, setAppSearchLoading] = useState(false);
const [creatorProfile, setCreatorProfile] = useState({}); const [creatorProfile, setCreatorProfile] = useState({});
@@ -1198,57 +1199,9 @@ const Apps2 = (props) => {
getFramework(); getFramework();
}, []); }, []);
// Fetch apps based on the current tab : 0 -> org_apps, 1 -> my_apps, 2 -> all_apps
const fetchApps = async () => {
const baseUrl = globalUrl;
let url;
setIsLoading(true);
const userId = userdata?.id;
if (currTab === 1 && userId) {
url = `${baseUrl}/api/v1/users/${userId}/apps`;
} else if (currTab === 0) {
url = `${baseUrl}/api/v1/apps`;
}
try {
const response = await fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (currTab === 1) {
setAppsToShow(data);
setUserApps(data);
} else if (currTab === 0) {
setAppsToShow(data);
setOrgApps(data);
// For testing the empty state
// setAppsToShow([]);
// setOrgApps([]);
}
setIsLoading(false);
} catch (err) {
console.error("Error fetching apps:", err);
setIsLoading(false);
}
};
useEffect(() => { useEffect(() => {
getApps()
// Only fetch if we have required data }, [])
if (globalUrl && (currTab === 0 || (currTab === 1 && userdata?.id))) {
fetchApps();
}
}, [currTab, globalUrl, userdata?.id]); // Remove location.search dependency
// useEffect(() => {
// // setSearchQuery("");
// setSelectedCategory([]);
// setSelectedLabel([]);
// }, [currTab])
// Find top categories and tags based on the current tab // Find top categories and tags based on the current tab
useEffect(() => { useEffect(() => {
@@ -1293,11 +1246,13 @@ const Apps2 = (props) => {
}); });
}; };
/*
useEffect(() => { useEffect(() => {
if (serverside) { if (serverside) {
return null; return null;
} }
}, [serverside]); }, [serverside]);
*/
const getApps = () => { const getApps = () => {
// Get apps from localstorage // Get apps from localstorage
@@ -1308,7 +1263,7 @@ const Apps2 = (props) => {
if (storageApps === null || storageApps === undefined || storageApps.length === 0) { if (storageApps === null || storageApps === undefined || storageApps.length === 0) {
storageApps = [] storageApps = []
} else { } else {
setAppsToShow(storageApps) //setAppsToShow(storageApps)
setOrgApps(storageApps) setOrgApps(storageApps)
setApps(storageApps) setApps(storageApps)
// setFilteredApps(storageApps) // setFilteredApps(storageApps)
@@ -1344,18 +1299,25 @@ const Apps2 = (props) => {
var privateapps = []; var privateapps = [];
var valid = []; var valid = [];
var invalid = []; var invalid = [];
var backups = []
for (var key in responseJson) { for (var key in responseJson) {
const app = responseJson[key]; const app = responseJson[key];
if (app.categories !== undefined && app.categories !== null && app?.categories.includes("Eradication")) { if (app?.reference_info?.onprem_backup === true) {
backups.push(app)
continue
}
if (app?.categories !== undefined && app?.categories !== null && app?.categories?.includes("Eradication")) {
app.categories = ["EDR"] app.categories = ["EDR"]
} }
if (app.is_valid && !(!app.activated && app.generated)) { if (app?.is_valid && !(!app?.activated && app?.generated)) {
privateapps.push(app); privateapps.push(app);
} else if ( } else if (
app.private_id !== undefined && app?.private_id !== undefined &&
app.private_id.length > 0 app?.private_id.length > 0
) { ) {
valid.push(app); valid.push(app);
} else { } else {
@@ -1363,6 +1325,11 @@ const Apps2 = (props) => {
} }
} }
console.log("BACKUPAPPS: ", backups)
if (backups.length > 0) {
setBackupApps(backups)
}
privateapps.push(...valid); privateapps.push(...valid);
privateapps.push(...invalid); privateapps.push(...invalid);
console.log("privateapps: setting apps ", privateapps) console.log("privateapps: setting apps ", privateapps)
@@ -1372,39 +1339,22 @@ const Apps2 = (props) => {
// setFilteredApps(privateapps); // setFilteredApps(privateapps);
if (privateapps.length > 0) { if (privateapps.length > 0) {
if (selectedApp.id === undefined || selectedApp.id === null) { if (selectedApp?.id === undefined || selectedApp?.id === null) {
if (privateapps[0].owner !== undefined && privateapps[0].owner !== null) { if (privateapps[0]?.owner !== undefined && privateapps[0]?.owner !== null) {
getUserProfile(privateapps[0].owner); getUserProfile(privateapps[0]?.owner);
} }
// setContact(privateapps[0].contact_info)
// setSelectedApp(privateapps[0]);
// setSharingConfiguration(privateapps[0].sharing === true ? "public" : "you")
} }
// if (
// privateapps[0].actions !== null &&
// privateapps[0].actions.length > 0
// ) {
// setSelectedAction(privateapps[0].actions[0]);
// } else {
// setSelectedAction({});
// }
} }
if (privateapps.length > 0 && storageApps.length === 0) { if (privateapps?.length > 0 && storageApps?.length === 0) {
try { try {
localStorage.setItem("apps", JSON.stringify(privateapps)) localStorage.setItem("apps", JSON.stringify(privateapps))
} catch (e) { } catch (e) {
console.log("Failed to set apps in localstorage: ", e) console.log("Failed to set apps in localstorage: ", e)
} }
} }
//setTimeout(() => {
// setFirstLoad(false)
//}, 5000)
}) })
.catch((error) => { .catch((error) => {
console.log("Failed to get apps: ", error.toString());
toast(error.toString()); toast(error.toString());
setIsLoading(false); setIsLoading(false);
}); });
@@ -1780,7 +1730,6 @@ const Apps2 = (props) => {
// setOpenModal(true); // setOpenModal(true);
}; };
useEffect(() => { useEffect(() => {
const apps = currTab === 1 ? userApps : orgApps; const apps = currTab === 1 ? userApps : orgApps;
const filteredUserAppdata = filterApps(apps, searchQuery, selectedCategory, selectedLabel); const filteredUserAppdata = filterApps(apps, searchQuery, selectedCategory, selectedLabel);
@@ -1798,33 +1747,38 @@ const Apps2 = (props) => {
} else if (newTab === 1) { } else if (newTab === 1) {
const filteredUserApps = filterApps(userApps, searchQuery, selectedCategory, selectedLabel); const filteredUserApps = filterApps(userApps, searchQuery, selectedCategory, selectedLabel);
setAppsToShow(filteredUserApps); setAppsToShow(filteredUserApps);
} } else if (newTab === 3) {
const filteredUserApps = filterApps(backupApps, searchQuery, selectedCategory, selectedLabel);
setAppsToShow(filteredUserApps);
return
}
// Update URL query params based on tab index // Update URL query params based on tab index
const tabMapping = { const tabMapping = {
0: 'org_apps', 0: 'org_apps',
1: 'my_apps', 1: 'my_apps',
2: 'all_apps' 2: 'all_apps',
3: 'backup_apps',
}; };
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
queryParams.set('tab', tabMapping[newTab]); queryParams.set('tab', tabMapping[newTab]);
// Maintain search query in URL regardless of tab // Maintain search query in URL regardless of tab
if (searchQuery) { if (searchQuery) {
queryParams.set('q', searchQuery); queryParams.set('q', searchQuery);
} else { } else {
queryParams.delete('q'); queryParams.delete('q');
} }
navigate(`${location.pathname}?${queryParams.toString()}`); navigate(`${location.pathname}?${queryParams.toString()}`);
}; };
// Update useEffect for filtering without URL manipulation // Update useEffect for filtering without URL manipulation
useEffect(() => { useEffect(() => {
if (currTab === 2) return; // Skip for "Discover Apps" tab as it uses Algolia if (currTab === 2) return; // Skip for "Discover Apps" tab as it uses Algolia
const apps = currTab === 1 ? userApps : orgApps; const apps = currTab === 1 ? userApps : currTab === 3 ? backupApps : orgApps;
const filteredApps = filterApps(apps, searchQuery, selectedCategory, selectedLabel); const filteredApps = filterApps(apps, searchQuery, selectedCategory, selectedLabel);
setAppsToShow(filteredApps); setAppsToShow(filteredApps);
}, [searchQuery, selectedCategory, selectedLabel, currTab, userApps, orgApps]); }, [searchQuery, selectedCategory, selectedLabel, currTab, userApps, orgApps]);
@@ -1914,7 +1868,7 @@ const Apps2 = (props) => {
<div style={boxStyle}> <div style={boxStyle}>
<div style={{ display: "flex", flexDirection: "row", width: "100%", justifyContent: "space-between" }}> <div style={{ display: "flex", flexDirection: "row", width: "100%", justifyContent: "space-between" }}>
<Typography variant="h4" style={{ marginBottom: 20, paddingLeft: 15, textTransform: 'none', fontFamily: theme?.typography?.fontFamily }}> <Typography variant="h4" style={{ marginBottom: 20, paddingLeft: 15, textTransform: 'none', fontFamily: theme?.typography?.fontFamily }}>
{currTab === 0 ? "Org" : currTab === 1 ? "Your" : "Discover"} Apps {currTab === 0 ? "Org" : currTab === 1 ? "Your" : currTab === 3 ? "Backup" : "Discover"} Apps
</Typography> </Typography>
{isCloud ? null : ( {isCloud ? null : (
<span style={{ display: "flex", gap: 15 }}> <span style={{ display: "flex", gap: 15 }}>
@@ -2027,6 +1981,7 @@ const Apps2 = (props) => {
...(currTab === 1 ? tabActive : {}) ...(currTab === 1 ? tabActive : {})
}} }}
/> />
<Tab <Tab
label="Discover Public Apps" label="Discover Public Apps"
style={{ style={{
@@ -2035,6 +1990,17 @@ const Apps2 = (props) => {
...(currTab === 2 ? tabActive : {}) ...(currTab === 2 ? tabActive : {})
}} }}
/> />
{backupApps.length > 0 &&
<Tab
label={`Onprem Backup (${backupApps.length})`}
style={{
...tabStyle,
marginLeft: 25,
...(currTab === 3 ? tabActive : {})
}}
/>
}
</Tabs> </Tabs>
</div> </div>
<div style={{ display: "flex", justifyContent: "space-between", gap: 10, marginBottom: 20, height: 45, paddingRight: 25 }}> <div style={{ display: "flex", justifyContent: "space-between", gap: 10, marginBottom: 20, height: 45, paddingRight: 25 }}>
@@ -2043,7 +2009,7 @@ const Apps2 = (props) => {
minWidth: "25%", minWidth: "25%",
maxWidth: "25%" maxWidth: "25%"
}}> }}>
{(currTab === 0 || currTab === 1) ? ( {(currTab === 0 || currTab === 1 || currTab === 3) ? (
<TextField <TextField
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -2253,7 +2219,7 @@ const Apps2 = (props) => {
<div> <div>
{ {
currTab === 0 && ( currTab === 0 || currTab === 3 && (
<div style={{ minHeight: 570 }}> <div style={{ minHeight: 570 }}>
{isLoading ? ( {isLoading ? (
<LoadingGrid /> <LoadingGrid />
@@ -2285,7 +2251,7 @@ const Apps2 = (props) => {
handleAppClick={handleAppClick} handleAppClick={handleAppClick}
leftSideBarOpenByClick={leftSideBarOpenByClick} leftSideBarOpenByClick={leftSideBarOpenByClick}
userdata={userdata} userdata={userdata}
fetchApps={fetchApps} fetchApps={getApps}
setUserApps={setUserApps} setUserApps={setUserApps}
appsToShow={appsToShow} appsToShow={appsToShow}
@@ -2338,7 +2304,7 @@ const Apps2 = (props) => {
{appsToShow.map((data, index) => ( {appsToShow.map((data, index) => (
<AppCard key={index} data={data} index={index} mouseHoverIndex={mouseHoverIndex} setMouseHoverIndex={setMouseHoverIndex} globalUrl={globalUrl} deactivatedIndexes={deactivatedIndexes} currTab={currTab} userdata={userdata} <AppCard key={index} data={data} index={index} mouseHoverIndex={mouseHoverIndex} setMouseHoverIndex={setMouseHoverIndex} globalUrl={globalUrl} deactivatedIndexes={deactivatedIndexes} currTab={currTab} userdata={userdata}
handleAppClick={handleAppClick} leftSideBarOpenByClick={leftSideBarOpenByClick} handleAppClick={handleAppClick} leftSideBarOpenByClick={leftSideBarOpenByClick}
fetchApps={fetchApps} fetchApps={getApps}
setUserApps={setUserApps} setUserApps={setUserApps}
appsToShow={appsToShow} appsToShow={appsToShow}
setAppsToShow={setAppsToShow} setAppsToShow={setAppsToShow}
+46 -4
View File
@@ -678,6 +678,7 @@ const Workflows2 = (props) => {
var upload = ""; var upload = "";
const [workflows, setWorkflows] = React.useState([]); const [workflows, setWorkflows] = React.useState([]);
const [backupWorkflows, setBackupWorkflows] = React.useState([]);
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
const [selectedUsecases, setSelectedUsecases] = React.useState([]); const [selectedUsecases, setSelectedUsecases] = React.useState([]);
const [filteredWorkflows, setFilteredWorkflows] = React.useState([]); const [filteredWorkflows, setFilteredWorkflows] = React.useState([]);
@@ -756,7 +757,8 @@ const Workflows2 = (props) => {
const tabMapping = { const tabMapping = {
0: 'org_workflows', 0: 'org_workflows',
1: 'my_workflows', 1: 'my_workflows',
2: 'all_workflows' 2: 'all_workflows',
3: 'backup_apps',
}; };
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
queryParams.set('tab', tabMapping[newValue]); queryParams.set('tab', tabMapping[newValue]);
@@ -1357,15 +1359,25 @@ const Workflows2 = (props) => {
} }
var newarray = [] var newarray = []
var backupWf = []
for (var wfkey in responseJson) { for (var wfkey in responseJson) {
const wf = responseJson[wfkey] const wf = responseJson[wfkey]
if (wf.public === true || wf.hidden === true) { if (wf.public === true || wf.hidden === true) {
continue continue
} }
if (wf?.backup_config?.onprem_backup === true) {
backupWf.push(wf)
continue
}
newarray.push(wf) newarray.push(wf)
} }
if (backupWf.length > 0) {
setBackupWorkflows(backupWf)
}
var setProdFilter = false var setProdFilter = false
var actionnamelist = []; var actionnamelist = [];
@@ -4332,6 +4344,17 @@ const Workflows2 = (props) => {
}} }}
/> />
{backupWorkflows.length > 0 &&
<Tab
label={`Onprem Backup (${backupWorkflows.length})`}
style={{
...tabStyle,
marginLeft: 25,
...(currTab === 3 ? tabActive : {})
}}
/>
}
<Tab <Tab
label="Org Forms" label="Org Forms"
onClick={() => { onClick={() => {
@@ -4341,7 +4364,7 @@ const Workflows2 = (props) => {
...tabStyle, ...tabStyle,
marginRight: 0, marginRight: 0,
marginLeft: 25, marginLeft: 25,
...(currTab === 3 ? tabActive : {}) ...(currTab === 4 ? tabActive : {})
}} }}
/> />
</Tabs> </Tabs>
@@ -4676,8 +4699,6 @@ const Workflows2 = (props) => {
paddingBottom: 40 paddingBottom: 40
}}> }}>
{currTab === 0 && orgWorkflows.map((data, index) => { {currTab === 0 && orgWorkflows.map((data, index) => {
// Shouldn't be a part of this list // Shouldn't be a part of this list
if (data.public === true) { if (data.public === true) {
@@ -4699,6 +4720,27 @@ const Workflows2 = (props) => {
) )
})} })}
{currTab === 3 && backupWorkflows.map((data, index) => {
// Shouldn't be a part of this list
if (data.public === true) {
return null
}
// if (firstLoad) {
// workflowDelay += 75
// } else {
// return <WorkflowPaper key={index} data={data} />
// }
return (
<span key={index}>
{/*<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>*/}
<WorkflowPaper data={data} />
{/*</Zoom>*/}
</span>
)
})}
{ {
currTab === 1 && myWorkflows.map((data, index) => { currTab === 1 && myWorkflows.map((data, index) => {
if (data.public === true) { if (data.public === true) {