Added statistics to both backend and frontend + cloud sync

This commit is contained in:
Frikky
2023-10-17 02:40:32 +02:00
parent 0579f60b58
commit 595c217a7c
32 changed files with 4035 additions and 1753 deletions
+2 -2
View File
@@ -71,8 +71,8 @@ SHUFFLE_SWARM_BRIDGE_DEFAULT_MTU=1500 # 1500 by default
# the container to the docker0
SHUFFLE_SWARM_BRIDGE_DEFAULT_INTERFACE=eth0
# Used for auto-cleanup of containers. REALLY important at scale.
SHUFFLE_CONTAINER_AUTO_CLEANUP=false
# Used for auto-cleanup of containers. REALLY important at scale. Set to false to see all container info.
SHUFFLE_CONTAINER_AUTO_CLEANUP=true
SHUFFLE_ELASTIC=true
SHUFFLE_LOGS_DISABLED=false
SHUFFLE_CHAT_DISABLED=false # Controls support chat
+1 -1
View File
@@ -1,6 +1,6 @@
module shuffle-shared
//replace github.com/shuffle/shuffle-shared => ../../../../git/shuffle-shared
replace github.com/shuffle/shuffle-shared => ../../../../git/shuffle-shared
go 1.19
-58
View File
@@ -700,64 +700,6 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
}
}
/*
// Removed as UserInput is now handled as an app
if actionResult.Status == "WAITING" && actionResult.Action.AppName == "User Input" {
log.Printf("[INFO] SHOULD WAIT A BIT AND RUN USER INPUT! WAITING!")
var trigger shuffle.Trigger
err = json.Unmarshal([]byte(actionResult.Result), &trigger)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling actionresult for user input: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
orgId := workflowExecution.ExecutionOrg
if len(workflowExecution.OrgId) == 0 && len(workflowExecution.Workflow.OrgId) > 0 {
orgId = workflowExecution.Workflow.OrgId
}
err := handleUserInput(trigger, orgId, workflowExecution.Workflow.ID, workflowExecution.ExecutionId)
if err != nil {
log.Printf("[WARNING] Failed userinput handler: %s", err)
actionResult.Result = fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)
workflowExecution.Results = append(workflowExecution.Results, actionResult)
workflowExecution.Status = "ABORTED"
err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true)
if err != nil {
log.Printf("[WARNING] Failed to set execution during wait: %s", err)
} else {
log.Printf("[INFO] Successfully set the execution %s to waiting.", workflowExecution.ExecutionId)
}
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error: %s"}`, err)))
return
} else {
log.Printf("[INFO] Successful userinput handler")
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "CLOUD IS DONE"}`)))
actionResult.Result = `{"success": True, "reason": "Waiting for user feedback based on configuration"}`
workflowExecution.Results = append(workflowExecution.Results, actionResult)
workflowExecution.Status = actionResult.Status
err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true)
if err != nil {
log.Printf("[WARNING] Failed setting userinput: %s", err)
} else {
log.Printf("[DEBUG] Successfully set the execution to waiting.")
}
}
return
}
*/
runWorkflowExecutionTransaction(ctx, 0, workflowExecution.ExecutionId, actionResult, resp)
}
-2
View File
@@ -1856,8 +1856,6 @@ const AppFramework = (props) => {
//autounselectify={true}
var usecasediff = -100
const bgColor = color === undefined || color === null || color.length === 0 ? theme.palette.surfaceColor : color
console.log("Background: ", bgColor)
return (
<div style={{margin: "auto", backgroundColor: bgColor, position: "relative", }}>
<div style={{position: "absolute"}}>
+1 -1
View File
@@ -200,7 +200,7 @@ const Appsearch = props => {
const CustomHits = connectHits(InputHits)
return (
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%",}}>
<div style={{width: 287, height: 295, padding: "16px 16px 267px 16px", alignItems: "center", gap: 138,}}>
<InstantSearch searchClient={searchClient} indexName="appsearch">
{/* showSearch === false ? null :
<div style={{maxWidth: 450, margin: "auto", }}>
@@ -18,7 +18,6 @@ import {
Grid,
Paper,
Typography,
TextField,
Zoom,
} from "@mui/material";
@@ -294,9 +294,6 @@ const AuthenticationData = (props) => {
InputProps={{
style: {
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: 50,
fontSize: "1em",
},
disableUnderline: true,
@@ -386,7 +383,6 @@ const AuthenticationData = (props) => {
PaperProps={{
style: {
pointerEvents: "auto",
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: 600,
minHeight: 600,
@@ -441,9 +437,6 @@ const AuthenticationData = (props) => {
InputProps={{
style: {
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: 50,
fontSize: "1em",
},
}}
+717 -23
View File
@@ -1,32 +1,118 @@
import React, { useState, useEffect } from "react";
import theme from "../theme.jsx";
import ReactGA from 'react-ga4';
import theme from "../theme.jsx";
import { useTheme } from "@mui/styles";
import countries from "../components/Countries.jsx";
import {
Box,
Paper,
Typography,
Divider,
Button,
Grid,
Card,
List,
ListItemText,
ListItem,
Dialog,
DialogTitle,
DialogContent,
TextField,
} from "@mui/material";
import { useAlert } from "react-alert";
import { useNavigate, Link } from "react-router-dom";
import { Autocomplete } from "@mui/material";
import { toast } from "react-toastify"
import {
Cached as CachedIcon,
} from "@mui/icons-material";
//import { useAlert
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
import BillingStats from "../components/BillingStats.jsx";
const Billing = (props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, } = props;
console.log("Billing: ", billingInfo);
const alert = useAlert();
//const alert = useAlert();
let navigate = useNavigate();
const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false);
const [dealList, setDealList] = React.useState([]);
const [dealName, setDealName] = React.useState("");
const [dealAddress, setDealAddress] = React.useState("");
const [dealType, setDealType] = React.useState("MSSP");
const [dealCountry, setDealCountry] = React.useState("United States");
const [dealCurrency, setDealCurrency] = React.useState("USD");
const [dealStatus, setDealStatus] = React.useState("initiated");
const [dealValue, setDealValue] = React.useState("");
const [dealDiscount, setDealDiscount] = React.useState("");
const [dealerror, setDealerror] = React.useState("");
const stripe = typeof window === 'undefined' || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : ""
console.log("Stripe: ", stripe)
const products = [
{ code: "", label: "MSSP", phone: "" },
{ code: "", label: "Enterprise", phone: "" },
{ code: "", label: "Consultancy", phone: "" },
{ code: "", label: "Support", phone: "" },
];
const handleGetDeals = (orgId) => {
console.log("Get deals!");
if (orgId.length === 0) {
toast(
"Organization ID not defined (get deals). Please contact us on https://shuffler.io if this persists logout."
);
return;
}
const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`;
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Bad status code in get deals: ", response.status);
}
return response.json();
})
.then((responseJson) => {
console.log("Got deals: ", responseJson);
if (responseJson.success === false) {
toast("Failed loading deals. Contact support if this persists");
} else {
setDealList(responseJson);
}
})
.catch((error) => {
console.log("Error getting org deals: ", error);
toast(
"Failed getting deals for your org. Contact support if this persists."
);
});
};
useEffect(() => {
if (isCloud && selectedOrganization.partner_info !== undefined && selectedOrganization.partner_info.reseller === true) {
handleGetDeals(selectedOrganization.id);
}
}, [])
const paperStyle = {
padding: 20,
height: "100%",
minHeight: 280,
maxWidth: 400,
width: "100%",
backgroundColor: theme.palette.surfaceColor,
borderRadius: theme.palette.borderRadius,
border: "1px solid rgba(255,255,255,0.3)",
marginRight: 10,
}
@@ -131,24 +217,22 @@ const Billing = (props) => {
})
.then(function (responseJson) {
if (responseJson.success !== undefined && responseJson.success) {
alert.success("Successfully stopped subscription!");
toast("Successfully stopped subscription!");
} else {
alert.error("Failed stopping subscription. Please contact us.");
toast("Failed stopping subscription. Please contact us.");
}
})
.catch(function (error) {
console.log("Error: ", error);
alert.error("Failed stopping subscription. Please contact us.");
toast("Failed stopping subscription. Please contact us.");
});
};
const SubscriptionObject = (props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, } = props;
const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, highlight, } = props;
console.log("Sub: ", subscription)
var top_text = "Base Access"
if (subscription.limit === undefined && subscription.level !== undefined) {
if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) {
subscription.name = "Enterprise"
subscription.currency_text = "$"
subscription.price = subscription.level*180
@@ -161,12 +245,41 @@ const Billing = (props) => {
]
}
var newPaperstyle = JSON.parse(JSON.stringify(paperStyle))
if (subscription.name === "Enterprise" && subscription.active === true) {
top_text = "Current Plan"
newPaperstyle.border = "1px solid #f85a3e"
}
var showSupport = false
if (subscription.name.includes("default")) {
top_text = "Custom Contract"
newPaperstyle.border = "1px solid #f85a3e"
showSupport = true
}
if (subscription.name.includes("App Run Units")) {
top_text = "Cloud Access"
showSupport = true
}
if (subscription.name.includes("Open Source")) {
top_text = "Open Source"
showSupport = true
}
if (subscription.name.includes("Scale")) {
top_text = "Scale access"
}
if (highlight === true) {
// Add an "Upgrade now" button
newPaperstyle.border = "1px solid #f85a3e"
}
return (
<Paper style={paperStyle}>
<Paper style={newPaperstyle}>
<div style={{display: "flex"}}>
<Typography variant="h6" style={{ marginTop: 10, marginBottom: 10 }}>
{top_text}
@@ -177,6 +290,8 @@ const Billing = (props) => {
<Typography variant="body1" style={{ marginTop: 20, }}>
{subscription.name}
</Typography>
{subscription.currency_text !== undefined ?
<div style={{display: "flex", }}>
<Typography variant="h6" style={{ marginTop: 10, }}>
{subscription.currency_text}{subscription.price}
@@ -185,16 +300,41 @@ const Billing = (props) => {
/ {subscription.interval}
</Typography>
</div>
: null}
<Typography variant="body2" color="textSecondary" style={{ marginTop: 10, }}>
Features
</Typography>
<ul>
{subscription.features !== undefined && subscription.features !== null ?
subscription.features.map((feature, index) => {
var parsedFeature = feature
if (feature.includes("Documentation: ")) {
parsedFeature =
<a
href={feature.split("Documentation: ")[1]}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e",}}
>
Documentation to get started
</a>
}
if (feature.includes("Licensed Worker: ")) {
parsedFeature =
<a
href={feature.split("Licensed Worker: ")[1]}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e",}}
>
Download the licensed worker
</a>
}
return (
<li>
<li key={index}>
<Typography variant="body2" color="textPrimary" style={{ }}>
{feature}
{parsedFeature}
</Typography>
</li>
)
@@ -202,38 +342,355 @@ const Billing = (props) => {
: null}
</ul>
</div>
{/*subscription.name === "Pay as you go" && subscription.limit <= 10000 ?
{(highlight === true && subscription.name === "Pay as you go" && subscription.limit <= 10000) || subscription.name.includes("Scale") ?
<span>
<Typography variant="body2" color="textSecondary" style={{ marginTop: 20, marginBottom: 10 }}>
You are not subscribed to any plan and are using the free plan with max 10,000 apps per month. Activate billing to de-activate this limit.
{subscription.name.includes("Scale") ?
""
:
"You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit."
}
</Typography>
<Button
variant="contained"
color="primary"
style={{ marginTop: 20, marginBottom: 10, }}
onClick={() => {
handleStripeRedirect()
if (isCloud) {
navigate("/pricing?tab=cloud&highlight=true")
} else {
window.open("https://shuffler.io/pricing?tab=onprem&highlight=true", "_blank")
}
}}
>
Activate Billing
Upgrade Now
</Button>
</span>
: null*/}
: null}
{showSupport ?
<Button variant="outlined" color="primary" style={{ marginTop: 20, marginBottom: 10, }} onClick={() => {
console.log("Support click")
if (window.drift !== undefined) {
//window.drift.api.startInteraction({ interactionId: 340045 })
window.drift.api.startInteraction({ interactionId: 340043 })
} else {
navigate("/contact")
}
}}>
Get Support
</Button>
: null }
</Paper>
)
}
const addDealModal = (
<Dialog
open={selectedDealModalOpen}
onClose={() => {
setSelectedDealModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
<DialogTitle style={{ maxWidth: 450, margin: "auto" }}>
<span style={{ color: "white" }}>Register new deal</span>
</DialogTitle>
<DialogContent>
<div style={{ display: "flex" }}>
<TextField
style={{
marginTop: 0,
backgroundColor: theme.palette.inputColor,
flex: 3,
marginRight: 10,
}}
InputProps={{
style: {
height: 50,
color: "white",
},
}}
color="primary"
required
fullWidth={true}
placeholder="Name"
type="text"
id="standard-required"
autoComplete="username"
margin="normal"
label="Name"
variant="outlined"
defaultValue={dealName}
onChange={(e) => {
setDealName(e.target.value);
}}
/>
<TextField
style={{
marginTop: 0,
backgroundColor: theme.palette.inputColor,
flex: 3,
marginRight: 10,
}}
InputProps={{
style: {
height: 50,
color: "white",
},
}}
color="primary"
required
fullWidth={true}
placeholder="Address"
label="Address"
type="text"
id="standard-required"
autoComplete="username"
margin="normal"
variant="outlined"
defaultValue={dealAddress}
onChange={(e) => {
setDealAddress(e.target.value);
}}
/>
</div>
<div style={{ display: "flex", marginTop: 10 }}>
<TextField
style={{
marginTop: 0,
backgroundColor: theme.palette.inputColor,
flex: 1,
marginRight: 10,
}}
InputProps={{
style: {
height: 50,
color: "white",
},
}}
color="primary"
required
fullWidth={true}
placeholder="1000"
label="Value (USD)"
type="text"
id="standard-required"
margin="normal"
variant="outlined"
defaultValue={dealValue}
onChange={(e) => {
setDealValue(e.target.value);
}}
/>
<Autocomplete
id="country-select"
sx={{ width: 250 }}
options={countries}
variant="outlined"
autoHighlight
getOptionLabel={(option) => option.label}
onChange={(event, newValue) => {
setDealCountry(newValue.label);
}}
renderOption={(props, option) => (
<Box
component="li"
sx={{ "& > img": { mr: 2, flexShrink: 0 } }}
{...props}
>
<img
loading="lazy"
width="20"
src={`https://flagcdn.com/w20/${option.code.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${option.code.toLowerCase()}.png 2x`}
alt=""
/>
{option.label} ({option.code}) +{option.phone}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
style={{
backgroundColor: theme.palette.inputColor,
flex: 1,
marginTop: 0,
marginRight: 10,
}}
variant="outlined"
label="Choose a country"
defaultValue={dealCountry}
inputProps={{
...params.inputProps,
autoComplete: "new-password", // disable autocomplete and autofill
}}
/>
)}
/>
<Autocomplete
id="product-select"
sx={{ width: 250 }}
options={products}
variant="outlined"
autoHighlight
onChange={(event, newValue) => {
setDealType(newValue);
}}
getOptionLabel={(option) => option.label}
renderOption={(props, option) => (
<Box
component="li"
sx={{ "& > img": { mr: 2, flexShrink: 0 } }}
{...props}
>
{option.label}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
style={{
backgroundColor: theme.palette.inputColor,
flex: 1,
marginTop: 0,
}}
variant="outlined"
label="Choose a product"
defaultValue={dealType}
inputProps={{
...params.inputProps,
autoComplete: "new-password", // disable autocomplete and autofill
}}
/>
)}
/>
</div>
{dealerror.length > 0 ? (
<Typography
variant="body1"
color="textSecondary"
style={{ margin: 10 }}
>
error registering: {dealerror}
</Typography>
) : null}
<div style={{ display: "flex", width: 300, margin: "auto" }}>
<Button
style={{ maxHeight: 50, flex: 1, margin: 5 }}
variant="outlined"
color="secondary"
disabled={false}
onClick={() => {
setSelectedDealModalOpen(false);
//setDealName("")
//setDealAddress("")
//setDealCountry("")
//setDealValue("")
}}
>
Cancel
</Button>
<Button
style={{ maxHeight: 50, flex: 1, margin: 5 }}
variant="contained"
color="primary"
disabled={
dealName.length <= 3 ||
dealAddress.length <= 3 ||
dealCountry.length === 0 ||
dealValue.length === 0 ||
dealType.length === 0
}
onClick={() => {
submitDeal(dealName, dealAddress, dealCountry, dealValue);
}}
>
Submit
</Button>
</div>
</DialogContent>
</Dialog>
);
const submitDeal = (dealName, dealAddress, dealCountry, dealValue) => {
if (dealerror.length > 0) {
setDealerror("");
}
const orgId = selectedOrganization.id;
const data = {
reseller_org: orgId,
name: dealName,
address: dealAddress,
country: dealCountry,
value: dealValue,
};
const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`;
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(function (response) {
if (response.status !== 200) {
console.log("Error in response");
}
return response.json();
})
.then(function (responseJson) {
if (responseJson.success === true) {
setSelectedDealModalOpen(false);
toast(
"Added new deal! We will be in touch shortly with an update."
);
setDealName("");
setDealAddress("");
setDealValue("");
setDealCountry("United States");
setDealType("MSSP");
} else {
setDealerror(responseJson.reason);
}
})
.catch(function (error) {
//console.log("Error: ", error);
setDealerror(error.toString());
toast("Failed adding deal reg: ", error);
});
};
return (
<div>
{addDealModal}
<Typography variant="h6" style={{ marginTop: 20, marginBottom: 10 }}>
Billing
</Typography>
<Typography variant="body1" style={{ marginTop: 20, marginBottom: 10 }}>
We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below.
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10}}>
{isCloud ?
"We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below."
:
"Shuffle is an Open Source automation platform, and no license is required to use it. You may however activate Cloud Sync, get our Scale license, get help with Kubernetes, or talk to Shuffle's Support team to get automation help."
}
</Typography>
<div style={{display: "flex", maxWidth: 768, minWidth: 768, }}>
{billingInfo.subscription !== undefined && billingInfo.subscription !== null ?
{isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ?
<SubscriptionObject
globalUrl={globalUrl}
userdata={userdata}
@@ -242,7 +699,46 @@ const Billing = (props) => {
stripeKey={stripeKey}
selectedOrganization={selectedOrganization}
subscription={billingInfo.subscription}
highlight={selectedOrganization.subscriptions === undefined || selectedOrganization.subscriptions === null || selectedOrganization.subscriptions.length === 0}
/>
: !isCloud ?
<span style={{display: "flex", }}>
<SubscriptionObject
globalUrl={globalUrl}
userdata={userdata}
serverside={false}
billingInfo={undefined}
selectedOrganization={selectedOrganization}
subscription={{
name: "Open Source",
limit: 0,
features: [
"Unlimited app runs/month, but may be slow. Only limited by CPU.",
"Multi-Tenancy",
"Single-Sign-On",
"Cloud Sync",
],
}}
highlight={true}
/>
<SubscriptionObject
globalUrl={globalUrl}
userdata={userdata}
serverside={false}
billingInfo={undefined}
selectedOrganization={selectedOrganization}
subscription={{
name: "Scale",
limit: 0,
features: [
"Scale License. Runs faster than Open Source.",
"Priority Support",
"Workflow & App development help",
],
}}
highlight={false}
/>
</span>
: null}
{isCloud &&
selectedOrganization.subscriptions !== undefined &&
@@ -309,6 +805,204 @@ const Billing = (props) => {
</Grid>
*/}
</div>
{isCloud &&
selectedOrganization.partner_info !== undefined &&
selectedOrganization.partner_info.reseller === true ? (
<div style={{ marginTop: 30, marginBottom: 200 }}>
<Typography
style={{ marginTop: 40, marginLeft: 10, marginBottom: 5 }}
variant="h6"
>
Reseller dashboard
</Typography>
<Button
variant="contained"
color="primary"
style={{ margin: 15 }}
onClick={() => {
setSelectedDealModalOpen(true);
}}
>
Add deal
</Button>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
variant="contained"
color="primary"
onClick={() => {
handleGetDeals(userdata.active_org.id);
}}
>
<CachedIcon />
</Button>
<List>
<ListItem>
<ListItemText
primary="Name"
style={{
minWidth: 200,
maxWidth: 200,
}}
/>
<ListItemText
primary="Status"
style={{ minWidth: 150, maxWidth: 150, marginLeft: 5 }}
/>
<ListItemText
primary="Value"
style={{ minWidth: 125, maxWidth: 125 }}
/>
<ListItemText
primary="Discount"
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary="Address"
style={{
marginleft: 10,
minWidth: 100,
maxWidth: 100,
overflow: "hidden",
}}
/>
<ListItemText
primary="Country"
style={{
marginleft: 10,
minWidth: 100,
maxWidth: 100,
overflow: "hidden",
}}
/>
<ListItemText
primary="Created"
style={{ minWidth: 200, maxWidth: 100 }}
/>
<ListItemText
primary="Last edited"
style={{ minWidth: 200, maxWidth: 100 }}
/>
</ListItem>
<Divider />
{dealList.length === 0 ? (
<Typography
variant="h6"
style={{
textAlign: "center",
margin: "auto",
width: 600,
marginTop: 50,
marginBottom: 50,
}}
>
No deals registered yet. Click "Add deal" to register one
</Typography>
) : (
dealList.map((deal, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
return (
<ListItem
key={index}
style={{ backgroundColor: bgColor }}
>
<ListItemText
primary={deal.name}
style={{
minWidth: 200,
maxWidth: 200,
overflow: "hidden",
}}
/>
<ListItemText
primary={deal.status}
style={{
minWidth: 150,
maxWidth: 150,
marginLeft: 5,
color:
deal.status.toLowerCase() === "requested"
? "yellow"
: deal.status.toLowerCase() === "denied" ||
deal.status.toLowerCase() === "cancelled"
? "red"
: "green",
}}
/>
<ListItemText
primary={`\$${deal.value}`}
style={{ minWidth: 125, maxWidth: 125 }}
/>
<ListItemText
primary={
deal.discount.length === 0 ? "TBD" : deal.discount
}
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary={deal.address}
style={{
marginleft: 10,
minWidth: 100,
maxWidth: 100,
overflow: "hidden",
}}
/>
<ListItemText
primary={deal.country}
style={{
marginleft: 10,
minWidth: 100,
maxWidth: 100,
overflow: "hidden",
}}
/>
<ListItemText
primary={new Date(
deal.created * 1000
).toISOString()}
style={{ minWidth: 200, maxWidth: 200 }}
/>
<ListItemText
primary={new Date(deal.edited * 1000).toISOString()}
style={{ minWidth: 200, maxWidth: 200 }}
/>
</ListItem>
);
})
)}
</List>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
</div>
) : null}
<div style={{ marginTop: 30, }}>
<Typography
style={{ marginTop: 40, marginLeft: 10, marginBottom: 5 }}
variant="h6"
>
Billing Usage Overview
</Typography>
</div>
<BillingStats
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
/>
</div>
)
}
+280
View File
@@ -0,0 +1,280 @@
import React, { useState, useEffect } from 'react';
import classNames from "classnames";
import theme from '../theme.jsx';
import {
Tooltip,
TextField,
IconButton,
Button,
Typography,
Grid,
Paper,
Chip,
Checkbox,
} from "@mui/material";
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';
const LineChartWrapper = ({keys, inputname, height, width}) => {
const [hovered, setHovered] = useState("");
const inputdata = keys.data === undefined ? keys : keys.data
return (
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, padding: 30, marginTop: 15, }}>
<Typography variant="h6" style={{marginBotton: 15, }}>
{inputname}
</Typography>
<BarChart
width={"100%"}
height={height}
data={inputdata}
gridlines={
<GridlineSeries line={<Gridline direction="all" />} />
}
/>
</div>
)
}
const AppStats = (defaultprops) => {
const { globalUrl, selectedOrganization, userdata, } = 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 [workflowRuns, setWorkflowRuns] = useState(undefined);
const [subflowRuns, setSubflowRuns] = useState(undefined);
const handleDataSetting = (inputdata, grouping) => {
if (inputdata === undefined || inputdata === null) {
return
}
const dailyStats = inputdata.daily_statistics
if (dailyStats === undefined || dailyStats === null) {
return
}
console.log("Looking at daily data: ", inputdata)
var appRuns = {
"key": "App Runs",
"data": []
}
var workflowRuns = {
"key": "Workflow Runs (includes subflows)",
"data": []
}
var subflowRuns = {
"key": "Subflow Runs",
"data": []
}
for (let key in dailyStats) {
// Always skips first one as it has accumulated data in it
if (key === 0) {
continue
}
const item = dailyStats[key]
if (item["date"] === undefined) {
console.log("No date: ", item)
continue
}
// Check if app_executions key in item
if (item["app_executions"] !== undefined && item["app_executions"] !== null) {
appRuns["data"].push({
key: new Date(item["date"]),
data: item["app_executions"]
})
}
// Check if workflow_executions key in item
if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(item["date"]),
data: item["workflow_executions"]
})
}
if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(item["date"]),
data: item["subflow_executions"]
})
}
}
// 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"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_subflow_executions"]
})
}
setSubflowRuns(subflowRuns)
setWorkflowRuns(workflowRuns)
setAppruns(appRuns)
}
const getStats = () => {
fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`, {
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!: ", response.status);
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
return
}
setStatistics(responseJson)
handleDataSetting(responseJson, "day")
})
.catch((error) => {
console.log("error: ", error)
});
}
useEffect(() => {
getStats()
}, [])
const paperStyle = {
textAlign: "center",
padding: 40,
margin: 5,
backgroundColor: theme.palette.surfaceColor,
maxWidth: 300,
}
const data = (
<div className="content" style={{width: "100%", margin: "auto", }}>
<Typography variant="body1" style={{margin: "auto", marginLeft: 10, marginBottom: 20, }}>
All Stat widgets are monthly and gathered from <a
href={`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e",}}
>Your Organization Statistics. </a>
This is a feature to help give you more insight into Shuffle, and will be populating over time.
</Typography>
{statistics !== undefined ?
<div style={{display: "flex", textAlign: "center",}}>
<Paper style={paperStyle}>
<Typography variant="h4">
{statistics.monthly_workflow_executions}
</Typography>
<Typography variant="h6">
Workflow Runs
</Typography>
</Paper>
<Paper style={paperStyle}>
<Typography variant="h4">
{statistics.monthly_app_executions}
</Typography>
<Typography variant="h6">
App Runs
</Typography>
</Paper>
</div>
: null}
{appRuns === undefined ?
null
:
<LineChartWrapper keys={appRuns} height={300} width={"100%"} inputname={"Daily App Runs"}/>
}
{workflowRuns === undefined ?
null
:
<LineChartWrapper keys={workflowRuns} height={300} width={"100%"} inputname={"Daily Workflow Runs (including subflows)"}/>
}
{subflowRuns === undefined ?
null
:
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"}/>
}
</div>
)
const dataWrapper = (
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div>
);
return dataWrapper;
}
export default AppStats;
+361 -112
View File
@@ -16,11 +16,17 @@ import {
ListItem,
ListItemText,
Collapse,
IconButton,
} from "@mui/material";
import {
FavoriteBorder as FavoriteBorderIcon,
Error as ErrorIcon,
CheckCircleRounded as CheckCircleRoundedIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Check as CheckIcon,
Visibility as VisibilityIcon,
} from "@mui/icons-material";
import { FixName } from "../views/Apps.jsx";
import aa from 'search-insights'
@@ -34,27 +40,26 @@ import aa from 'search-insights'
// Specifically used for UNSAVED workflows only?
const ConfigureWorkflow = (props) => {
const {
apps,
theme,
isCloud,
workflow,
userdata,
globalUrl,
theme,
workflow,
newWebhook,
referenceUrl,
saveWorkflow,
showTriggers,
submitSchedule,
setSelectedApp,
selectedAction,
appAuthentication,
setSelectedAction,
setAuthenticationModalOpen,
setSelectedApp,
apps,
selectedAction,
setConfigureWorkflowModalOpen,
saveWorkflow,
newWebhook,
submitSchedule,
referenceUrl,
isCloud,
setAuthenticationType,
alert,
showTriggers,
workflowExecutions,
getWorkflowExecution,
setAuthenticationType,
setAuthenticationModalOpen,
setConfigureWorkflowModalOpen,
} = props;
const [requiredActions, setRequiredActions] = React.useState([]);
@@ -64,9 +69,39 @@ const ConfigureWorkflow = (props) => {
const [itemChanged, setItemChanged] = React.useState(false);
const [firstLoad, setFirstLoad] = React.useState("");
const [showFinalizeAnimation, setShowFinalizeAnimation] = React.useState(false);
const [loopRunning, setLoopRunning] = useState(false)
const [checkStarted, setCheckStarted] = React.useState(false);
const stop = () => {
setLoopRunning(false)
}
const start = () => {
setLoopRunning(true)
}
useEffect(() => {
if (loopRunning) {
const intervalId = setInterval(() => {
if (!loopRunning) {
clearInterval(intervalId);
}
if (getWorkflowExecution !== undefined && workflowExecutions !== undefined) {
const paramkey = workflow.id
getWorkflowExecution(paramkey)
} else {
console.log("Executions or getWorkflowExecutions not defined")
}
}, 3000)
return () => clearInterval(intervalId);
}
}, [loopRunning])
/*
const { start, stop } = useInterval({
duration: 3000,
startImmediate: false,
@@ -79,6 +114,7 @@ const ConfigureWorkflow = (props) => {
}
},
});
*/
// ONLY when component is being unloaded, run stop() function
// This is to prevent the interval from running when the component is not being used
@@ -92,15 +128,17 @@ const ConfigureWorkflow = (props) => {
*/
// Where is this from?
if (workflow === undefined || workflow === null) {
if (workflow === undefined || workflow === null || workflow.id === undefined) {
return null;
}
if (apps === undefined || apps === null) {
console.log("Apps is undefined or null: ", apps)
return null;
}
if (appAuthentication === undefined || appAuthentication === null) {
console.log("App authentication is undefined or null: ", appAuthentication)
return null;
}
@@ -136,13 +174,19 @@ const ConfigureWorkflow = (props) => {
if (firstLoad.length === 0 || firstLoad !== workflow.id) {
if (apps === undefined || apps === null || apps.length === 0) {
console.log("No apps loaded: ", apps);
if (setConfigureWorkflowModalOpen !== undefined) {
setConfigureWorkflowModalOpen(false);
}
return null;
}
setFirstLoad(workflow.id)
const newactions = [];
for (let [key, keyval] in Object.entries(workflow.actions)) {
const action = workflow.actions[key];
var newaction = {
large_image: action.large_image,
@@ -160,17 +204,19 @@ const ConfigureWorkflow = (props) => {
show_steps: false,
}
//console.log("Action: ", key, keyval)
if (action.app_name.toLowerCase().endsWith("_api")) {
action.app_name = action.app_name.slice(0, -4)
}
const app = apps.find((app) =>
app.id === action.app_id ||
(app.name === action.app_name &&
(app.app_version === action.app_version || (app.loop_versions !== null && app.loop_versions.includes(action.app_version))))
)
//console.log("FOUND APP: ", app)
// ID match OR name match + version match
//const app = apps.find((app) => app.id === action.app_id || (app.name === action.app_name && (app.app_version === action.app_version || (app.loop_versions !== null && app.loop_versions.includes(action.app_version)))))
//
// without version match
const newappname = action.app_name.toLowerCase().replaceAll(" ", "_")
const app = apps.find((app) => app.id === action.app_id || app.name.toLowerCase().replaceAll(" ", "_") === newappname)
if (app === undefined || app === null) {
const subapp = apps.find(app => app.name === action.app_name)
if (subapp !== undefined && subapp !== null) {
newaction.update_version = "1.1.0"
@@ -211,7 +257,6 @@ const ConfigureWorkflow = (props) => {
}
} else if (action.authentication_id !== "" && app.authentication.required === true) {
console.log("Should verify authentication ID ", action.authentication_id)
}
newaction.app = app;
@@ -278,6 +323,7 @@ const ConfigureWorkflow = (props) => {
if (workflow.workflow_variables !== undefined && workflow.workflow_variables !== null && workflow.workflow_variables.length !== 0) {
for (let [key,keyval] in Object.entries(workflow.workflow_variables)) {
const variable = workflow.workflow_variables[key];
if (variable.value === undefined || variable.value === undefined || variable.value.length === 0) {
variable.value = "";
requiredVariables.push(variable);
@@ -294,6 +340,7 @@ const ConfigureWorkflow = (props) => {
if (trigger.trigger_type === "WEBHOOK") {
console.log("Found webhook: ", trigger)
if (trigger.app_association !== undefined && trigger.app_association.name !== null && trigger.app_association.name !== "") {
console.log("Actions: ", newactions)
const findapp = trigger.app_association.name.toLowerCase()
@@ -337,13 +384,9 @@ const ConfigureWorkflow = (props) => {
requiredTriggers.push(trigger);
}
}
}
if (
requiredTriggers.length === 0 &&
requiredVariables.length === 0 &&
newactions.length === 0
) {
if (requiredTriggers.length === 0 && requiredVariables.length === 0 && newactions.length === 0 && setConfigureWorkflowModalOpen !== undefined) {
setConfigureWorkflowModalOpen(false);
}
@@ -381,7 +424,7 @@ const ConfigureWorkflow = (props) => {
const { trigger } = props
return (
<ListItem>
<ListItem style={{padding: 0, }}>
<ListItemAvatar>
<Avatar variant="rounded">
<img
@@ -446,39 +489,6 @@ const ConfigureWorkflow = (props) => {
{trigger.status !== "running" ? "Start" : "Running"}
</Button>
) : null}
{/*
<ListItemText
primary={
<TextField
style={{backgroundColor: theme.palette.inputColor, borderRadius: 5,}}
InputProps={{
style:{
color: "white",
minHeight: 50,
marginLeft: 5,
maxWidth: "95%",
fontSize: "1em",
},
endAdornment: (
<InputAdornment position="end">
</InputAdornment>
)
}}
fullWidth
color="primary"
type={"text"}
placeholder={`New value for ${trigger.name}`}
onChange={(event) => {
console.log("NEW VALUE ON INDEX", trigger.value)
}}
onBlur={(event) => {
//workflow.variables[variable.index] = event.target.value
}}
/>
}
style={{}}
/>
*/}
</ListItem>
);
};
@@ -488,7 +498,7 @@ const ConfigureWorkflow = (props) => {
//<Typography variant="body2">Name: {variable.name} - {variable.value}. </Typography>
return (
<ListItem>
<ListItem style={{padding: 0, }}>
<ListItemAvatar>
<Avatar>
<FavoriteBorderIcon />
@@ -589,28 +599,250 @@ const ConfigureWorkflow = (props) => {
};
const AppSectionSelfcontained = (props) => {
const { action } = props;
const [opened, setOpened] = useState(false);
const [filled, setFilled] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [finalized, setFinalized] = useState(false);
const [authFields, setAuthFields] = useState([])
const [sensitiveFields, setSensitiveFields] = useState([])
if (authFields.length === 0 && opened === true) {
// Loop through fields of the action
var newfields = []
const params = action.action.parameters
var sensitiveIndexes = []
var index = 0
for (let key in params) {
const param = params[key]
if (param.configuration === true) {
if (param.name.toLowerCase().includes("key") || param.name.toLowerCase().includes("token") || param.name.toLowerCase().includes("password")) {
sensitiveIndexes.push(index)
}
newfields.push({
"key": param.name,
"example": param.example === undefined ? "" : param.example,
"value": param.name === "url" ? param.example : "",
})
index += 1
}
}
if (newfields.length > 0) {
setSensitiveFields(sensitiveIndexes)
setAuthFields(newfields)
}
}
const submitLocalAuth = (app, fields) => {
const appAuthData = {
active: true,
app: app,
fields: fields,
label: "Authentication for " + app.name,
usage: [{"workflow_id": workflow.id}],
auto_distribute: true,
}
fetch(globalUrl + "/api/v1/apps/authentication", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(appAuthData),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for setting app auth :O!");
}
setSubmitted(false)
return response.json();
})
.then((responseJson) => {
if (!responseJson.success) {
toast("Failed to set app auth: " + responseJson.reason);
} else {
toast("App auth set for app " + app.name.replace("_", " "));
setFinalized(true)
setOpened(false)
}
})
.catch((error) => {
setSubmitted(false)
//toast(error.toString());
console.log("New auth error: ", error.toString());
});
}
var parsedName = action.app_name.replaceAll("_", " ");
if (action.app_name.toLowerCase().endsWith("_api")) {
parsedName = parsedName.substring(0, parsedName.length - 4);
}
// Remove _basic at the end if it exists
if (parsedName.toLowerCase().endsWith("_basic")) {
parsedName = parsedName.substring(0, parsedName.length - 6);
}
parsedName = (parsedName.charAt(0).toUpperCase() + parsedName.slice(1)).replaceAll("_", " ");
return (
<ListItem
style={{padding: 0, display: "flex", flexDirection: "column", }}
>
<div
style={{
border: filled ? `1px solid ${theme.palette.green}` : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, width: "100%", padding: 12, cursor: "pointer",
}}
id="app-config"
>
<div style={{display: "flex", }}
onClick={() => {
setOpened(!opened);
}}
>
<div style={{display: "flex", flex: 7, }}>
<span style={{marginTop: 10, marginRight: 10, }}>
{!opened ? <ExpandMoreIcon /> : <ExpandLessIcon />}
</span>
<img
alt={parsedName}
style={{ margin: 4, minHeight: 40, maxHeight: 40, borderRadius: 30, }}
src={action.large_image}
/>
<Typography style={{ marginLeft: 10, marginTop: 10, }} variant="body1">
<b>{finalized ? "Authenticated" : `Configure ${parsedName}`}</b>
</Typography>
</div>
{filled ?
<CheckIcon style={{color: theme.palette.green, marginLeft: 10, marginTop: 10, flex: 1, }} />
: null}
</div>
{opened ?
<div style={{padding: 12, }}>
{authFields.map((field, index) => {
var parsedName = field.key
// Remove _basic at the end if it exists
if (parsedName.toLowerCase().endsWith("_basic")) {
parsedName = parsedName.substring(0, parsedName.length - 6);
}
parsedName = (parsedName.charAt(0).toUpperCase() + parsedName.slice(1)).replaceAll("_", " ");
return (
<div key={index} style={{marginBottom: 15, marginLeft: 10, marginRight: 10, }}>
<Typography style={{ }} variant="body1">
{parsedName}
</Typography>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
}}
defaultValue={field.value}
onChange={(event) => {
event.preventDefault();
authFields[index].value = event.target.value;
setAuthFields(authFields);
var allFilled = true;
authFields.forEach((field) => {
if (field.value.length === 0) {
allFilled = false;
} else {
//console.log("Field is not filled: "+field.key)
}
})
if (allFilled) {
console.log("Should test the fields, and submit them")
setFilled(true);
} else {
if (filled) {
setFilled(false);
}
}
}}
endAdornment={
// Show item that can show field value if password
//field.name.toLowerCase().includes("key") || field.name.toLowerCase().includes("token") || field.name.toLowerCase().includes("password") ?
field.key.toLowerCase().includes("key") || field.key.toLowerCase().includes("token") || field.key.toLowerCase().includes("password") ?
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => {
setSensitiveFields(sensitiveFields.filter((item) => item !== index))
}}
onMouseDown={(event) => {
event.preventDefault();
}}
>
<VisibilityIcon />
</IconButton>
</InputAdornment>
: null
}
fullWidth
color="primary"
type={sensitiveFields.includes(index) ? "password" : "text"}
placeholder={field.example ? field.example : `Enter your ${field.key}`}
data-lpignore="true"
dataLPIgnore="true"
autocomplete="off"
/>
</div>
)
})}
<Button
variant="contained"
color="primary"
style={{
marginTop: 15,
width: 150,
marginLeft: 135,
}}
disabled={!filled || submitted}
onClick={() => {
setSubmitted(true);
// const submitLocalAuth = (app, fields) => {
submitLocalAuth({"id": action.app.id, "name": action.app.name, "version": action.app.version, "large_image": action.large_image, }, authFields);
}}
>
{submitted ? <CircularProgress style={{color: theme.palette.primary.main, }} /> : "Submit"}
</Button>
</div>
: null}
</div>
</ListItem>
)
}
const AppSection = (props) => {
const { action } = props;
var parsedName = action.app_name.replaceAll("_", " ");
if (action.app_name.toLowerCase().endsWith("_api")) {
parsedName = parsedName.substring(0, parsedName.length - 4);
}
return (
<ListItem>
{/*
<ListItemAvatar>
<Avatar variant="rounded">
<img
alt={action.app_name}
src={action.large_image}
style={{ width: 50 }}
/>
</Avatar>
</ListItemAvatar>
<ListItemText
primary={FixName(action.app_name)}
secondary={action.app_version}
style={{}}
/>
*/}
{action.must_authenticate ?
<Button
fullWidth
@@ -631,23 +863,23 @@ const ConfigureWorkflow = (props) => {
}}
color="primary"
onClick={() => {
setAuthenticationType(
action.app.authentication.type === "oauth2" &&
action.app.authentication.redirect_uri !== undefined &&
action.app.authentication.redirect_uri !== null
? {
if (setAuthenticationType !== undefined) {
setAuthenticationType(action.app.authentication.type === "oauth2" && action.app.authentication.redirect_uri !== undefined && action.app.authentication.redirect_uri !== null
?
{
type: "oauth2",
redirect_uri: action.app.authentication.redirect_uri,
token_uri: action.app.authentication.token_uri,
scope: action.app.authentication.scope,
}
: {
:
{
type: "",
}
)
}
setItemChanged(true);
if (setSelectedAction !== undefined) {
setSelectedAction(action.action);
}
@@ -656,7 +888,9 @@ const ConfigureWorkflow = (props) => {
setSelectedApp(action.app);
}
if (setAuthenticationModalOpen !== undefined) {
setAuthenticationModalOpen(true);
}
}}
>
<img
@@ -765,7 +999,7 @@ const ConfigureWorkflow = (props) => {
//backgroundColor: selectedUsecaseCategory === usecase.name ? usecase.color : theme.palette.surfaceColor,
const BoxHighlight = (props) => {
const {data, appname, appinfo, index, activeStep, setActiveStep, finished, } = props
const {data, appname, appinfo, index, activeStep, setActiveStep, filled, } = props
const [hovered, setHovered] = useState(false)
const [isOpen, setIsOpen] = useState(false)
@@ -773,7 +1007,6 @@ const ConfigureWorkflow = (props) => {
// This kind of just works for new workflows..
// What if we try many times?
var webhook = {
"name": "Testhook",
"description": `A Webhook Trigger has been started and is ready to receive events from ${appname}. Click to copy the URL to send events to.`,
@@ -781,7 +1014,7 @@ const ConfigureWorkflow = (props) => {
}
useEffect(() => {
if (data.type === "webhook" && !finished) {
if (data.type === "webhook" && !filled) {
if (!checkStarted) {
setCheckStarted(true)
start()
@@ -833,7 +1066,7 @@ const ConfigureWorkflow = (props) => {
<div style={{display: "flex"}}>
<Typography variant="h6" style={{flex: 10, }}>{data.title}</Typography>
{finished ?
{filled ?
<CheckCircleRoundedIcon style={{color: "#0f9d58", flex: 1, }} />
:
<ErrorIcon style={{color: "#ffd300", flex: 1, }} />
@@ -870,7 +1103,7 @@ const ConfigureWorkflow = (props) => {
}}>
<Typography variant="body2" color="textSecondary">{webhook.description}</Typography>
{/*<Typography variant="body2" color="textSecondary">{webhook.url}</Typography>*/}
{isLoading && finished === false ?
{isLoading && filled === false ?
<div style={{margin: "auto", width: 60, height: 60, marginTop: 5, }}>
<CircularProgress />
</div>
@@ -879,7 +1112,10 @@ const ConfigureWorkflow = (props) => {
}
</div>
:
setConfigureWorkflowModalOpen !== undefined ?
<AppSection key={index} action={appinfo} />
:
<AppSectionSelfcontained key={index} action={appinfo} />
}
</div>
: null}
@@ -923,10 +1159,10 @@ const ConfigureWorkflow = (props) => {
</div>
{clicked === true ?
data.steps.map((step, index) => {
var finished = false
var filled = false
if (step.type === "activate") {
if (data.activation_done === true) {
finished = true
filled = true
if (index === activeStep && firstRun === true) {
setActiveStep(activeStep+1)
@@ -941,10 +1177,10 @@ const ConfigureWorkflow = (props) => {
if (step.type === "authenticate") {
console.log("AUTH STEP: ", step)
if (data.must_authenticate === true ) {
finished = false
filled = false
} else {
if (data.activation_done === true && data.auth_done === true) {
finished = true
filled = true
if (firstRun) {
setFinishCount(finishCount+1)
@@ -963,7 +1199,7 @@ const ConfigureWorkflow = (props) => {
if (exec.execution_argument !== undefined && exec.execution_argument !== null && exec.execution_argument.length > 0 && exec.execution_source === "webhook") {
//console.log("Done: ", exec)
finished = true
filled = true
if (index === activeStep && firstRun === true) {
setActiveStep(activeStep+1)
@@ -991,7 +1227,7 @@ const ConfigureWorkflow = (props) => {
}
return (
<BoxHighlight appinfo={data} appname={"Wazuh"} key={index} data={step} index={index} activeStep={activeStep} setActiveStep={setActiveStep} finished={finished} />
<BoxHighlight appinfo={data} appname={"Wazuh"} key={index} data={step} index={index} activeStep={activeStep} setActiveStep={setActiveStep} filled={filled} />
)
})
: null}
@@ -1002,27 +1238,39 @@ const ConfigureWorkflow = (props) => {
const topColor = "#f86a3e, #fc3922"
return (
<div>
<div style={{height: 75, width: "100%", background: `linear-gradient(to right, ${topColor}`, position: "relative",}}>
</div>
<div style={{margin: "25px 50px 50px 50px", maxHeight: 475, }}>
{setConfigureWorkflowModalOpen !== undefined ?
<div style={{height: 75, width: "100%", background: `linear-gradient(to right, ${topColor}`, position: "relative",}} />
: null}
<div style={{margin: setConfigureWorkflowModalOpen !== undefined ? "0px 50px 0px 50px" : "35px 0px 0px", maxHeight: 475, }}>
{setConfigureWorkflowModalOpen !== undefined ?
<Typography variant="h6">{workflow.name}</Typography>
<Typography variant="body2" color="textSecondary">
The following configuration makes the workflow ready immediately.
: null
}
<Typography variant="body2" style={{}}>
Please configure the following apps for automatic startup of automation:
</Typography>
{requiredActions.length > 0 ? (
<span>
{setConfigureWorkflowModalOpen !== undefined ?
<Typography variant="body1" style={{ marginTop: 10, }}>
Required Actions
</Typography>
: null}
<List>
<List style={{paddingBottom: 250, }}>
{requiredActions.map((data, index) => {
return (
<div key={index}>
{data.steps !== undefined && data.steps !== null && data.show_steps === true ?
<div key={index} style={{marginBottom: 10, }}>
{data.steps !== undefined && data.steps !== null && data.show_steps === true && setConfigureWorkflowModalOpen !== undefined ?
<AppWrapper data={data} parentindex={index} />
:
setConfigureWorkflowModalOpen !== undefined ?
<AppSection key={index} action={data} />
:
<AppSectionSelfcontained key={index} action={data} />
}
</div>
)
@@ -1031,7 +1279,7 @@ const ConfigureWorkflow = (props) => {
</span>
) : null}
{requiredVariables.length > 0 ? (
{setConfigureWorkflowModalOpen !== undefined && requiredVariables.length > 0 ? (
<span>
<Typography variant="body1" style={{ marginTop: 10 }}>
Variables
@@ -1044,7 +1292,7 @@ const ConfigureWorkflow = (props) => {
</span>
) : null}
{requiredTriggers.length > 0 && showTriggers !== false ? (
{setConfigureWorkflowModalOpen !== undefined && requiredTriggers.length > 0 && showTriggers !== false ? (
<span>
<Typography variant="body1" style={{ marginTop: 10 }}>
Triggers
@@ -1056,8 +1304,8 @@ const ConfigureWorkflow = (props) => {
</List>
</span>
) : null}
<div style={{ textAlign: "center", display: "flex", marginTop: 20 }}>
{setConfigureWorkflowModalOpen !== undefined ?
<div style={{ textAlign: "left", display: "flex", marginTop: 20 }}>
{showFinalizeAnimation ?
<img id="finalize_gif" src="/images/finalize.gif" alt="finalize workflow animation" style={{width: 150, margin: "auto",}} onLoad={() => {
console.log("Img loaded.")
@@ -1100,6 +1348,7 @@ const ConfigureWorkflow = (props) => {
</ButtonGroup>
}
</div>
: null}
</div>
</div>
);
+37 -6
View File
@@ -5,6 +5,7 @@ import { MuiChipsInput } from "mui-chips-input";
import UsecaseSearch from "../components/UsecaseSearch.jsx"
import WorkflowGrid from "../components/WorkflowGrid.jsx"
import dayjs from 'dayjs';
import WorkflowTemplatePopup from "./WorkflowTemplatePopup.jsx";
import {
Badge,
@@ -57,7 +58,7 @@ import {
} from "@mui/icons-material";
const EditWorkflow = (props) => {
const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, } = props
const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, } = props
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
@@ -144,6 +145,8 @@ const EditWorkflow = (props) => {
}
const newWorkflow = isEditing === true ? false : true
const priority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true)
console.log("PRIO: ", priority)
var upload = "";
var total_count = 0
@@ -161,6 +164,7 @@ const EditWorkflow = (props) => {
maxWidth: isMobile ? "90%" : 650,
minHeight: 400,
paddingTop: 25,
paddingLeft: 50,
//minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550,
//maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550,
},
@@ -170,7 +174,7 @@ const EditWorkflow = (props) => {
<div style={{display: "flex"}}>
<div style={{flex: 1, color: "rgba(255,255,255,0.9)" }}>
<div style={{display: "flex"}}>
<Typography variant="h6" style={{flex: 9, }}>
<Typography variant="h4" style={{flex: 9, }}>
{newWorkflow ? "New" : "Editing"} workflow
</Typography>
{newWorkflow === true ? null :
@@ -193,7 +197,7 @@ const EditWorkflow = (props) => {
</div>
}
</div>
<Typography variant="body2" color="textSecondary" style={{maxWidth: 440,}}>
<Typography variant="body2" color="textSecondary" style={{marginTop: 20, maxWidth: 440,}}>
Workflows can be built from scratch, or from templates. <a href="/usecases" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Usecases</a> can help you discover next steps, and you can <a href="/search?tab=workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>search</a> for them directly. <a href="/docs/workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Learn more</a>
</Typography>
{showUpload === true ?
@@ -480,7 +484,7 @@ const EditWorkflow = (props) => {
</DialogContent>
<DialogActions style={{paddingRight: 20, }}>
<DialogActions style={{paddingRight: 100, }}>
<Button
style={{}}
onClick={() => {
@@ -542,11 +546,38 @@ const EditWorkflow = (props) => {
}}
color="primary"
>
{submitLoading ? <CircularProgress color="secondary" /> : "Submit"}
{submitLoading ? <CircularProgress color="secondary" /> : "Done"}
</Button>
</DialogActions>
{newWorkflow === true ?
<span style={{marginTop: 30, }}>
<Typography variant="h6" style={{marginLeft: 30, paddingBottom: 0, }}>
Relevant Workflows
</Typography>
{newWorkflow === true && name.length > 5 ?
{priority === null || priority === undefined ? null :
<div style={{marginLeft: 30, }}>
<WorkflowTemplatePopup
userdata={userdata}
globalUrl={globalUrl}
srcapp={priority.description.split("&").length > 2 ? priority.description.split("&")[0] : ""}
img1={priority.description.split("&").length > 2 ? priority.description.split("&")[1] : ""}
dstapp={priority.description.split("&").length > 3 ? priority.description.split("&")[2] : ""}
img2={priority.description.split("&").length > 3 ? priority.description.split("&")[3] : ""}
title={priority.name}
description={priority.description.split("&").length > 4 ? priority.description.split("&")[4] : ""}
apps={apps}
/>
</div>
}
</span>
: null}
{newWorkflow === true && name.length > 2 ?
<div style={{marginLeft: 30, }}>
<WorkflowGrid
maxRows={1}
+324
View File
@@ -0,0 +1,324 @@
import React, { useState, useEffect } from "react";
import ReactGA from 'react-ga4';
import 'react-alice-carousel/lib/alice-carousel.css';
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
import theme from '../theme.jsx';
import CheckBoxSharpIcon from '@mui/icons-material/CheckBoxSharp';
import {
Checkbox,
Button,
Collapse,
IconButton,
FormGroup,
FormControl,
InputLabel,
FormLabel,
FormControlLabel,
Select,
MenuItem,
Grid,
Paper,
Typography,
TextField,
Zoom,
List,
ListItem,
ListItemText,
Divider,
Tooltip,
Chip,
ButtonGroup,
Dialog,
DialogTitle,
DialogActions,
DialogContent,
} from "@mui/material";
import { useNavigate, Link } from "react-router-dom";
import WorkflowTemplatePopup from "../components/WorkflowTemplatePopup.jsx";
const ExploreWorkflow = (props) => {
const { userdata, globalUrl } = props
const [activeUsecases, setActiveUsecases] = useState(0);
const [modalOpen, setModalOpen] = React.useState(false);
const [suggestedUsecases, setSuggestedUsecases] = useState([])
const [usecasesSet, setUsecasesSet] = useState(false)
const [apps, setApps] = useState([])
const sizing = 475
let navigate = useNavigate();
const imagestyle = {
height: 40,
borderRadius: 40,
//border: "2px solid rgba(255,255,255,0.3)",
}
const loadApps = () => {
fetch(`${globalUrl}/api/v1/apps`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
return response.json();
})
.then((responseJson) => {
if (responseJson === null) {
console.log("null-response from server")
const pretend_apps = [{
"name": "TBD",
"app_name": "TBD",
"app_version": "TBD",
"description": "TBD",
"version": "TBD",
"large_image": "",
}]
setApps(pretend_apps)
return
}
if (responseJson.success === false) {
console.log("error loading apps: ", responseJson)
return
}
setApps(responseJson);
})
.catch((error) => {
console.log("App loading error: " + error.toString());
})
}
// Find priorities in userdata.priorities and check if the item.type === "usecase"
// If so, set the item.isActive to true
if (usecasesSet === false && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0 && suggestedUsecases.length === 0) {
var tmpUsecases = []
for (let i = 0; i < userdata.priorities.length; i++) {
if (userdata.priorities[i].type !== "usecase" || userdata.priorities[i].active === false) {
continue
}
tmpUsecases.push(userdata.priorities[i])
}
setSuggestedUsecases(tmpUsecases)
setUsecasesSet(true)
loadApps()
}
const modalView = (
// console.log("key:", dataValue.key),
//console.log("value:",dataValue.value),
<Dialog
open={modalOpen}
onClose={() => {
setModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
<DialogTitle style={{}}>
<div style={{ color: "white", display: "flex", alignItems: "center", justifyContent: "center" }}>
<CheckBoxSharpIcon sx={{ borderRadius: 4, color: "rgba(255, 132, 68, 1)" }} style={{ width: 24 }} />
<span style={{ marginLeft: 8, color: "rgba(255, 132, 68, 1)", fontSize: 16, width: 60 }}>Sign Up</span>
<div style={{ borderTop: "1px solid rgba(255, 132, 68, 1)", width: 85, marginLeft: 8, marginRight: 8 }} />
<CheckBoxSharpIcon sx={{ borderRadius: 4, color: "rgba(255, 132, 68, 1)" }} style={{ width: 24 }} />
<span style={{ marginLeft: 8, color: "rgba(255, 132, 68, 1)", fontSize: 16, width: 60 }}>Setup</span>
<div style={{ borderTop: "1px solid rgba(255, 132, 68, 1)", width: 85, marginRight: 8 }} />
<CheckBoxSharpIcon sx={{ borderRadius: 4, color: "rgba(255, 132, 68, 1)" }} style={{ width: 24 }} />
<span style={{ marginLeft: 8, color: "rgba(255, 132, 68, 1)", fontSize: 16, width: 60 }}>Explore</span>
</div>
</DialogTitle>
<Typography style={{ fontSize: 16, width: 252, marginLeft: 167 }}>
Heres a recommended workflow:
</Typography>
{/* <div style={{ marginTop: 0, maxWidth: 700, minWidth: 700, margin: "auto", minHeight: sizing, maxHeight: sizing, }}>
<div style={{ marginTop: 0, }}>
<div className="thumbs" style={{ display: "flex" }}>
<Tooltip title={"Previous usecase"}>
<IconButton
style={{
// backgroundColor: thumbIndex === 0 ? "inherit" : "white",
zIndex: 5000,
minHeight: 50,
maxHeight: 50,
color: "grey",
marginTop: 150,
borderRadius: 50,
border: "1px solid rgba(255,255,255,0.3)",
}}
onClick={() => {
slidePrev()
}}
>
<ArrowBackIosNewIcon />
</IconButton>
</Tooltip>
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, }}>
<AliceCarousel
style={{ backgroundColor: theme.palette.surfaceColor, minHeight: 750, maxHeight: 750, }}
items={formattedCarousel}
activeIndex={thumbIndex}
infiniteLoop
mouseTracking={false}
responsive={responsive}
// activeIndex={activeIndex}
controlsStrategy="responsive"
autoPlay={false}
infinite={true}
animationType="fadeout"
animationDuration={800}
disableButtonsControls
/>
</div>
<Tooltip title={"Next usecase"}>
<IconButton
style={{
backgroundColor: thumbIndex === usecaseButtons.length - 1 ? "inherit" : "white",
zIndex: 5000,
minHeight: 50,
maxHeight: 50,
color: "grey",
marginTop: 150,
borderRadius: 50,
border: "1px solid rgba(255,255,255,0.3)",
}}
onClick={() => {
slideNext()
}}
>
<ArrowForwardIosIcon />
</IconButton>
</Tooltip>
</div>
</div>
</div> */}
<DialogActions style={{ paddingLeft: "30px", paddingRight: '30px' }}>
<Button
style={{ borderRadius: "0px" }}
onClick={() => setModalOpen(false)}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "0px" }}
onClick={() => {
console.log("hello")
}}
color="primary"
>
Submit
</Button>
</DialogActions>
</Dialog>
);
return (
<div style={{ marginTop: 0, margin: "auto", minHeight: sizing, maxHeight: sizing, }}>
{modalView}
<Typography variant="h4" style={{ marginLeft: 8, marginTop: 40, marginRight: 30, marginBottom: 0, }} color="rgba(241, 241, 241, 1)">
Start using workflows
</Typography>
<Typography variant="body2" style={{ marginLeft: 8, marginTop: 10, marginRight: 30, marginBottom: 40, }} color="rgba(158, 158, 158, 1)">
Based on what you selected heres our recommendations!
</Typography>
<div style={{ marginTop: 0, }}>
<div className="thumbs" style={{ display: "flex" }}>
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, }}>
<Grid item xs={11} style={{}}>
{suggestedUsecases.length === 0 && usecasesSet ?
<Typography variant="h6" style={{ marginTop: 30, marginBottom: 50, }} color="rgba(158, 158, 158, 1)">
All Workflows are already added for your current apps!
</Typography>
:
suggestedUsecases.map((priority, index) => {
const srcapp = priority.description.split("&")[0]
var image1 = priority.description.split("&")[1]
var image2 = ""
var dstapp = ""
if (priority.description.split("&").length > 3) {
dstapp = priority.description.split("&")[2]
image2 = priority.description.split("&")[3]
}
const name = priority.name.replace("Suggested Usecase: ", "")
var description = ""
if (priority.description.split("&").length > 4) {
description = priority.description[4]
}
// FIXME: Should have a proper description
description = ""
return (
<WorkflowTemplatePopup
userdata={userdata}
globalUrl={globalUrl}
img1={image1}
srcapp={srcapp}
img2={image2}
dstapp={dstapp}
title={name}
description={description}
apps={apps}
/>
)
})}
</Grid>
<div>
<div style={{ marginTop: 32 }}>
<Typography variant="body2" style={{ fontSize: 16, marginTop: 24 }} color="rgba(158, 158, 158, 1)">
<Button variant="contained" type="submit"
fullWidth style={{
borderRadius: 200,
height: 51,
width: 464,
fontSize: 16,
padding: "16px 24px",
margin: "auto",
itemAlign: "center",
background: activeUsecases === 0 ? "rgba(47, 47, 47, 1)" : "linear-gradient(90deg, #F86744 0%, #F34475 100%)",
color: activeUsecases === 0? "rgba(158, 158, 158, 1)" : "rgba(241, 241, 241, 1)",
border: activeUsecases === 0 ? "1px solid rgba(158, 158, 158, 1)" : "none",
}}
onClick={() => {
navigate("/workflows?message="+activeUsecases+" workflows added")
}}>
Continue to workflows
</Button>
</Typography>
</div>
<Typography variant="body2" style={{ fontSize: 16, marginTop: 24 }} color="rgba(158, 158, 158, 1)">
<Link style={{ color: "#f86a3e", marginLeft: 145 }} to="/usecases" className="btn btn-primary">
Explore usecases
</Link>
</Typography>
</div>
</div>
</div>
</div>
</div>
)
}
export default ExploreWorkflow
+3 -2
View File
@@ -213,6 +213,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
const notificationWidth = 300
const imagesize = 22;
const boxColor = "#86c142";
const NotificationItem = (props) => {
const {data} = props
@@ -263,7 +264,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
{data.reference_url !== undefined && data.reference_url !== null && data.reference_url.length > 0 ?
<Link to={data.reference_url} style={{color: "#f86a3e", textDecoration: "none",}}>
<Typography variant="body1">
{data.title}
{data.title} ({data.amount})
</Typography >
</Link>
:
@@ -277,7 +278,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
:
null
}
<Typography variant="body2">
<Typography variant="body2" style={{maxHeight: 200, overflowX: "hidden", overflowY: "auto", }}>
{data.description}
</Typography >
{/*data.tags !== undefined && data.tags !== null && data.tags.length > 0 ?
+1 -1
View File
@@ -2278,7 +2278,7 @@ const ParsedAction = (props) => {
handleItemClick([innerdata]);
}}
>
<Paper style={{minHeight: 500, maxHeight: 500, minWidth: 275, maxWidth: 275, position: "fixed", top: menuPosition1.top-200, left: menuPosition1.left-455, padding: "10px 0px 10px 10px", backgroundColor: theme.palette.inputColor, overflow: "hidden", overflowY: "auto", border: "1px solid rgba(255,255,255,0.3)",}}>
<Paper style={{minHeight: 500, maxHeight: 500, minWidth: 275, maxWidth: 275, position: "fixed", top: menuPosition1.top-200, left: menuPosition1.left-455, padding: "10px 0px 10px 10px", overflow: "hidden", overflowY: "auto", border: "1px solid rgba(255,255,255,0.3)",}}>
<MenuItem
key={innerdata.name}
style={{
+1 -1
View File
@@ -69,7 +69,7 @@ const Priority = (props) => {
return (
<div style={{border: priority.active === false ? "1px solid #000000" : priority.severity === 1 ? "1px solid #f85a3e" : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, marginTop: 10, marginBottom: 10, padding: 15, textAlign: "center", minHeight: 100, maxHeight: 100, textAlign: "left", backgroundColor: theme.palette.surfaceColor, display: "flex", }}>
<div style={{border: priority.active === false ? "1px solid #000000" : priority.severity === 1 ? "1px solid #f85a3e" : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, marginTop: 10, marginBottom: 10, padding: 15, textAlign: "center", minHeight: isCloud ? 70 : 100, maxHeight: isCloud ? 70 : 100, textAlign: "left", backgroundColor: theme.palette.surfaceColor, display: "flex", }}>
<div style={{flex: 2, overflow: "hidden",}}>
<span style={{display: "flex", }}>
{priority.type === "usecase" || priority.type == "apps" ? <AutoFixHighIcon style={{height: 19, width: 19, marginLeft: 3, marginRight: 10, }}/> : null}
+21 -9
View File
@@ -34,14 +34,13 @@ import {
Circle as CircleIcon,
Add as AddIcon,
PlayArrow as PlayArrowIcon,
} from '@mui/icons-material';
import {
AutoFixHigh as AutoFixHighIcon,
Close as CloseIcon,
CompressOutlined,
QrCodeScannerOutlined,
} from '@mui/icons-material';
import { validateJson } from "../views/Workflows.jsx";
import ReactJson from "react-json-view";
import PaperComponent from "../components/PaperComponent.jsx";
@@ -823,10 +822,6 @@ const CodeEditor = (props) => {
}
const executeSingleAction = (inputdata) => {
//if (serverside === true) {
// return
//}
if (validation === true) {
inputdata = JSON.stringify(inputdata)
}
@@ -834,7 +829,10 @@ const CodeEditor = (props) => {
// Shuffle Tools 1.2.0 (in most cases?)
const appid = toolsAppId !== undefined && toolsAppId !== null && toolsAppId.length > 0 ? toolsAppId : "3e2bdf9d5069fe3f4746c29d68785a6a"
const actiondata = {"description":"Repeats the call parameter","id":"","name":"repeat_back_to_me","label":"","node_type":"","environment":"","sharing":false,"private_id":"","public_id":"","app_id": appid,"tags":null,"authentication":[],"tested":false,"parameters":[{"description":"The message to repeat","id":"","name":"call","example":"REPEATING: Hello world","value":inputdata,"multiline":true,"options":null,"action_field":"","variant":"STATIC_VALUE","required":true,"configuration":false,"tags":null,"schema":{"type":"string"},"skip_multicheck":false,"value_replace":null,"unique_toggled":false,"autocompleted":false}],"execution_variable":{"description":"","id":"","name":"","value":""},"returns":{"description":"","example":"","id":"","schema":{"type":"string"}},"authentication_id":"","example":"","auth_not_required":false,"source_workflow":"","run_magic_output":false,"run_magic_input":false,"execution_delay":0,"app_name":"Shuffle Tools","app_version":"1.2.0","selectedAuthentication":{}}
const actionname = selectedAction.name === "execute_python" && !inputdata.replaceAll(" ", "").includes("{%python%}") ? "execute_python" : "repeat_back_to_me"
const params = actionname === "execute_python" ? [{"name": "code", "value":inputdata}] : [{"name":"call", "value": inputdata}]
const actiondata = {"description":"Repeats the call parameter","id":"","name":actionname,"label":"","node_type":"","environment":"","sharing":false,"private_id":"","public_id":"","app_id": appid,"tags":null,"authentication":[],"tested":false,"parameters": params, "execution_variable":{"description":"","id":"","name":"","value":""},"returns":{"description":"","example":"","id":"","schema":{"type":"string"}},"authentication_id":"","example":"","auth_not_required":false,"source_workflow":"","run_magic_output":false,"run_magic_input":false,"execution_delay":0,"app_name":"Shuffle Tools","app_version":"1.2.0","selectedAuthentication":{}}
setExecutionResult({
"valid": false,
@@ -924,6 +922,20 @@ const CodeEditor = (props) => {
},
}}
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 14,
right: 18,
color: "grey",
}}
onClick={() => {
setExpansionModalOpen(false)
}}
>
<CloseIcon />
</IconButton>
<div style={{display: "flex"}}>
<div style={{flex: 1, }}>
{ isFileEditor ?
@@ -1555,7 +1567,7 @@ const CodeEditor = (props) => {
<IconButton disabled={executing} color="primary" style={{border: `1px solid ${theme.palette.primary.main}`, marginLeft: 100, padding: 8}} variant="contained" onClick={() => {
executeSingleAction(expOutput)
}}>
<Tooltip title="Try it! This runs the Shuffle Tools 'repeat back to me' action with what you see in the expected output window. Commonly used to test your Python scripts or Liquid filters, not requiring the full workflow to run again." placement="top">
<Tooltip title="Try it! This runs the Shuffle Tools 'repeat back to me' or 'execute python' action with what you see in the expected output window. Commonly used to test your Python scripts or Liquid filters, not requiring the full workflow to run again." placement="top">
{executing ? <CircularProgress style={{height: 18, width: 18, }} /> : <PlayArrowIcon style={{height: 18, width: 18, }} /> }
</Tooltip>
File diff suppressed because it is too large Load Diff
+2 -5
View File
@@ -154,7 +154,6 @@ const AppGrid = props => {
})
.then((responseJson) => {
if (responseJson.success !== false) {
console.log("Usecases: ", responseJson)
//handleKeysetting(responseJson, workflows)
}
})
@@ -187,10 +186,10 @@ const AppGrid = props => {
}, [])
if (localMessage !== inputsearch && inputsearch !== undefined && inputsearch !== null && inputsearch.length > 0) {
console.log("In refinement: ", inputsearch)
//setLocalMessage(inputsearch)
refine(inputsearch)
defaultSearch = inputsearch
return null
} else if (onlyResults === true) {
// Don't return anything unless refinement works
return null
@@ -244,9 +243,7 @@ const AppGrid = props => {
return (
<div>
{onlyResults === true && hits.length > 0 ?
<Typography variant="h6" style={{paddingBottom: 0, }}>
Relevant Workflows
</Typography>
null
: null}
<Grid container spacing={4} style={paperAppContainer}>
{hits.map((data, index) => {
+1 -1
View File
@@ -277,7 +277,7 @@ const WorkflowPaper = (props) => {
>
{data.tags !== undefined && data.tags !== null
? data.tags.map((tag, index) => {
if (index >= 3) {
if (index >= 2) {
return null;
}
@@ -0,0 +1,382 @@
import React, { useState, useEffect } from "react";
import { toast } from "react-toastify"
import theme from '../theme.jsx';
import {
Button,
Typography,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Drawer,
CircularProgress,
IconButton,
} from "@mui/material";
import {
Check as CheckIcon,
TrendingFlat as TrendingFlatIcon,
Close as CloseIcon,
} from '@mui/icons-material';
import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup.jsx";
import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx";
const WorkflowTemplatePopup = (props) => {
const { userdata, globalUrl, img1, srcapp, img2, dstapp, title, description, visualOnly, apps } = props;
const [isActive, setIsActive] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [workflowLoading, setWorkflowLoading] = useState(false);
const [workflow, setWorkflow] = useState({});
const [appAuthentication, setAppAuthentication] = React.useState(undefined);
const imagestyle = {
height: 40,
width: 40,
borderRadius: 40,
}
if (title === undefined || title === null || title === "") {
console.log("No title for workflow template popup!");
return null
}
const getWorkflow = (workflowId) => {
fetch(`${globalUrl}/api/v1/workflows/${workflowId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
console.log("Error in workflow loading for ID ", workflowId)
} else {
setWorkflow(responseJson)
}
})
.catch((error) => {
console.log("err in framework: ", error.toString());
setWorkflowLoading(false)
})
}
const loadAppAuth = () => {
fetch(`${globalUrl}/api/v1/apps/authentication`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for setting app auth :O!");
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success) {
toast("Failed to get app auth: " + responseJson.reason);
return
}
var newauth = [];
for (let authkey in responseJson.data) {
if (responseJson.data[authkey].defined === false) {
continue;
}
newauth.push(responseJson.data[authkey]);
}
setAppAuthentication(newauth);
})
.catch((error) => {
//toast(error.toString());
console.log("New auth error: ", error.toString());
});
}
const getGeneratedWorkflow = () => {
// POST
// https://shuffler.io/api/v1/workflows/merge
// destination: {app_id: "b9c2feaf99b6309dabaeaa8518c61d3d", app_name: "Servicenow_API", app_version: "",}
// id: ""
// middle:[]
// name: "Email analysis"
// source:{app_id: "accdaaf2eeba6a6ed43b2efc0112032d", app_name
setWorkflowLoading(true)
// FIXME: Remove hardcoding here after testing, and user srcapp/dstapp
const newsrcapp = srcapp
const newdstapp = dstapp
const mergedata = {
name: title,
id: "",
source: {
app_name: newsrcapp,
},
middle: [],
destination: {
app_name: newdstapp,
},
}
fetch(globalUrl + "/api/v1/workflows/merge", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(mergedata),
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
setWorkflowLoading(false)
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
console.log("Error in workflow template: ", responseJson.error);
setErrorMessage("Failed to generate workflow for these tools - the Shuffle team has been notified. Click out of this window to continue. Contact support@shuffler.io for further assistance.")
setIsActive(true)
//setTimeout(() => {
// setModalOpen(false)
//}, 5000)
} else {
console.log("Success in workflow template: ", responseJson);
setIsActive(true)
if (responseJson.workflow_id === "") {
console.log("Failed to build workflow for these tools. Closing in 3 seconds.")
return
}
getWorkflow(responseJson.workflow_id)
}
})
.catch((error) => {
console.log("err in framework: ", error.toString());
setWorkflowLoading(false)
})
}
const isFinished = () => {
// Look for configuration fields being done in the current modal
// 1. Start by finding the modal
const template = document.getElementById("workflow-template")
if (template === null || template == undefined) {
return true
}
// Find item in template with id app-config
const appconfig = template.getElementsByClassName("app-config")
if (appconfig === null || appconfig == undefined) {
return true
}
console.log("APPCONFIG: ", appconfig)
return false
}
const ModalView = () => {
return (
<Drawer
anchor={"left"}
open={modalOpen}
onClose={() => {
setModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: "black",
color: "white",
minWidth: 700,
maxWidth: 700,
paddingTop: 75,
itemAlign: "center",
},
}}
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 14,
right: 14,
color: "white",
}}
onClick={() => {
setModalOpen(false);
}}
>
<CloseIcon />
</IconButton>
<DialogContent style={{marginTop: 0, marginLeft: 75, maxWidth: 470, }}>
<Typography variant="h4">
<b>Configure Workflow</b>
</Typography>
<Typography variant="body2" color="textSecondary" style={{marginTop: 25, }}>
Selected Workflow:
</Typography>
<div style={{marginBottom: 0, }} id="workflow-template">
<WorkflowTemplatePopup2
globalUrl={globalUrl}
img1={img1}
srcapp={srcapp}
img2={img2}
dstapp={dstapp}
title={title}
description={description}
visualOnly={true}
/>
</div>
{workflowLoading ?
<div style={{marginTop: 75, textAlign: "center", }}>
<Typography variant="h4"> Generating the Workflow...
</Typography>
<CircularProgress style={{marginLeft: 125, marginTop: 10, }}/>
</div>
:
<div>
<Typography variant="h6" style={{marginTop: 75, }}>
{errorMessage !== "" ? errorMessage : ""}
</Typography>
</div>
}
<ConfigureWorkflow
userdata={userdata}
theme={theme}
globalUrl={globalUrl}
workflow={workflow}
appAuthentication={appAuthentication}
setAppAuthentication={setAppAuthentication}
apps={apps}
/>
{errorMessage === "" ?
<Button
style={{marginTop: 50, }}
variant={isFinished() ? "contained" : "outlined"}
>
Done
</Button>
: null}
</DialogContent>
</Drawer>
)
}
var parsedTitle = title
const maxlength = 30
if (title.length > maxlength) {
parsedTitle = title.substring(0, maxlength) + "..."
}
parsedTitle.replaceAll("_", " ")
return (
<div style={{ display: "flex", maxWidth: 470, minWidth: 470, height: 78, borderRadius: 8 }}>
<ModalView />
<div
// variant={isActive === 1 ? "contained" : "outlined"}
color="secondary"
disabled={visualOnly === true}
style={{
margin: 4,
width: "100%",
borderRadius: 8,
textTransform: "none",
backgroundColor: theme.palette.inputColor,
border: isActive ? errorMessage !== "" ? "1px solid red" : `2px solid ${theme.palette.green}` : isHovered ? "1px solid #f85a3e" : "1px solid rgba(33, 33, 33, 1)",
cursor: isActive ? errorMessage !== "" ? "not-allowed" : "pointer" : "pointer",
padding: "10px 20px 10px 20px",
position: "relative",
}}
onMouseEnter={() => {
setIsHovered(true)
}}
onMouseLeave={() => {
setIsHovered(false)
}}
onClick={() => {
if (visualOnly === true) {
console.log("Not showing more than visuals.")
return
}
//setIsActive(!isActive)
if (errorMessage !== "") {
toast("Already failed to generate workflow for these apps. Please try again later or contact support@shuffler.io.")
} else if (isActive) {
toast("Workflow already generated. Please try another template, or continue to the next page.")
// FIXME: Remove these?
loadAppAuth()
setModalOpen(true)
//getGeneratedWorkflow()
} else {
loadAppAuth()
setModalOpen(true)
getGeneratedWorkflow()
}
}}
>
<div style={{ display: "flex", itemAlign: "left", textAlign: "left", }}>
<div style={{display: "flex", flex: 1, marginTop: 3, }}>
<img src={img1} style={imagestyle} />
{img2 !== "" ?
<span style={{display: "flex", }}>
<TrendingFlatIcon style={{ marginTop: 7 }} />
<img src={img2} style={imagestyle} />
</span>
:
<span style={{width: 50, }} />
}
</div>
<div style={{ flex: 3, marginLeft: 20, }}>
<Typography variant="body1" style={{ marginTop: description.length === 0 ? 10 : 0, }} color="rgba(241, 241, 241, 1)">
{parsedTitle}
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginTop: 0, marginRight: 0, maxHeight: 16, overflow: "hidden",}} color="rgba(158, 158, 158, 1)">
{description}
</Typography>
</div>
</div>
<div>
{isActive === true && errorMessage === "" ?
<CheckIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", color: theme.palette.green, top: 10, right: 10, }} />
: ""}
</div>
</div>
</div>
)
}
export default WorkflowTemplatePopup
+3 -6
View File
@@ -18,10 +18,12 @@ const theme = createTheme(adaptV4Theme({
secondary: "rgba(255,255,255,0.7)",
},
type: "dark",
inputColor: "#383B40",
inputColor: "rgba(39,41,45,1)",
//inputColor: "#383B40",
surfaceColor: "#27292d",
platformColor: "#1c1c1d",
backgroundColor: "#1a1a1a",
green: "#5cc879",
borderRadius: 5,
defaultBorder: "1px solid rgba(255,255,255,0.3)",
jsonTheme: "brewer",
@@ -70,11 +72,6 @@ const theme = createTheme(adaptV4Theme({
},
},
overrides: {
MuiPaper: {
root: {
backgroundColor: "#1c1c1d",
},
},
MuiMenu: {
list: {
backgroundColor: "#27292d",
+17 -9
View File
@@ -1692,15 +1692,24 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user
const userId = user.id;
const data = { user_id: userId };
fetch(globalUrl + "/api/v1/generateapikey", {
console.log(user, userdata)
var fetchdata = {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
}
if (userId === userdata.id) {
fetchdata.method = "GET"
} else {
fetchdata.body = JSON.stringify(data)
}
fetch(globalUrl + "/api/v1/generateapikey", fetchdata)
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
@@ -1889,8 +1898,11 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user
open={selectedUserModalOpen}
onClose={() => {
setSelectedUserModalOpen(false);
setImage2FA("");
setValue2FA("");
setSecret2FA("");
setShow2faSetup(false);
}}
PaperProps={{
style: {
@@ -2156,7 +2168,7 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user
<Card
style={{
margin: 4,
backgroundColor: theme.palette.inputColor,
backgroundColor: theme.palette.surfaceColor,
color: "white",
minHeight: expanded ? 250 : "inherit",
maxHeight: expanded ? 250 : "inherit",
@@ -2515,9 +2527,8 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user
</span>
/>
<Tab
disabled={!isCloud}
label=<span>
Billing
Billing (Beta)
</span>
/>
<Tab
@@ -3246,10 +3257,8 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user
selectedOrganization.child_orgs !== null &&
selectedOrganization.child_orgs.length > 0
) {
console.log("In here?");
var active = [];
for (var key in userdata.orgs) {
console.log("ORG: ", userdata.orgs[key]);
const found =
selectedOrganization.child_orgs.find(
(item) => item.id === userdata.orgs[key].id
@@ -3299,7 +3308,6 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user
) : null;
const run2FASetup = (data) => {
console.log("2fa: ", data, show2faSetup);
if (!show2faSetup) {
get2faCode(data.id);
} else {
+45 -13
View File
@@ -580,15 +580,39 @@ const AngularWorkflow = (defaultprops) => {
const cytoscapeWidth = isMobile ? bodyWidth - leftBarSize : bodyWidth - leftBarSize - 25
const [elements, setElements] = useState([]);
const [loopRunning, setLoopRunning] = useState(false)
const stop = () => {
setLoopRunning(false)
}
const start = () => {
setLoopRunning(true)
}
useEffect(() => {
console.log("In useeffect for loopRunning: ", loopRunning)
if (loopRunning) {
const intervalId = setInterval(() => {
if (!loopRunning) {
clearInterval(intervalId);
}
fetchUpdates()
}, 3000)
return () => clearInterval(intervalId);
}
}, [loopRunning])
// No point going as fast, as the nodes aren't realtime anymore, but bulk updated.
// Set it from 2500 to 6000 to reduce overall load
const { start, stop } = useInterval({
duration: 3000,
startImmediate: false,
callback: () => {
fetchUpdates();
},
});
//const { start, stop } = useInterval({
// duration: 3000,
// startImmediate: false,
// callback: () => {
// fetchUpdates();
// },
//});
const getAppDocs = (appname, location, version) => {
fetch(`${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`, {
@@ -924,7 +948,7 @@ const AngularWorkflow = (defaultprops) => {
};
const getWorkflowExecution = (id, execution_id) => {
fetch(`${globalUrl}/api/v1/workflows/${id}/executions`, {
fetch(`${globalUrl}/api/v2/workflows/${id}/executions`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -940,11 +964,10 @@ const AngularWorkflow = (defaultprops) => {
return response.json();
})
.then((responseJson) => {
if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) {
// FIXME: Sort this by time
if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null && responseJson.executions.length > 0) {
// - means it's opposite
const newkeys = sortByKey(responseJson, "-started_at");
const newkeys = sortByKey(responseJson.executions, "-started_at");
setWorkflowExecutions(newkeys);
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
@@ -1021,6 +1044,7 @@ const AngularWorkflow = (defaultprops) => {
},
body: JSON.stringify(executionRequest),
credentials: "include",
cors: "no-cors",
})
.then((response) => {
if (response.status !== 200) {
@@ -1279,7 +1303,7 @@ const AngularWorkflow = (defaultprops) => {
};
const sendStreamRequest = (body) => {
console.log("Stream not activated yet.")
//console.log("Stream not activated yet.")
return
// Session may be important here huh
@@ -15302,6 +15326,10 @@ const AngularWorkflow = (defaultprops) => {
overflow: "hidden",
}}
onMouseOver={() => {
if (cy == undefined || cy == null) {
return
}
var currentnode = cy.getElementById(data.action.id);
if (currentnode !== undefined && currentnode !== null && currentnode.length !== 0) {
currentnode.addClass("shuffle-hover-highlight");
@@ -15314,6 +15342,10 @@ const AngularWorkflow = (defaultprops) => {
//)
}}
onMouseOut={() => {
if (cy == undefined || cy == null) {
return
}
var currentnode = cy.getElementById(data.action.id);
if (currentnode.length !== 0) {
currentnode.removeClass("shuffle-hover-highlight");
+2 -1
View File
@@ -1354,7 +1354,8 @@ const Apps = (props) => {
border: hover ? "1px solid #f85a3e" : "1px solid rgba(255,255,255,0.3)",
cursor: hover ? "pointer" : "default",
textAlign: "center",
height: 125,
minHeight: 150,
maxHeight: 150,
}}
>
{icon}
+102 -42
View File
@@ -6,6 +6,7 @@ import { makeStyles, } from "@mui/styles";
import classNames from "classnames";
import theme from '../theme.jsx';
import { useNavigate, Link, useParams } from "react-router-dom";
import WorkflowTemplatePopup from "../components/WorkflowTemplatePopup.jsx";
// react plugin used to create charts
//import { Line, Bar } from "react-chartjs-2";
@@ -90,7 +91,7 @@ const useStyles = makeStyles({
},
});
const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLoggedIn, workflows, setWorkflows}) => {
const UsecaseListComponent = ({userdata, keys, isCloud, globalUrl, frameworkData, isLoggedIn, workflows, setWorkflows}) => {
const [expandedIndex, setExpandedIndex] = useState(-1);
const [expandedItem, setExpandedItem] = useState(-1);
const [inputUsecase, setInputUsecase] = useState({});
@@ -103,17 +104,69 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
const [selectedWorkflows, setSelectedWorkflows] = useState([])
const [firstLoad, setFirstLoad] = useState(true)
const [apps, setApps] = useState([])
const classes = useStyles();
let navigate = useNavigate();
const [mitreTags, setMitreTags] = useState([]);
const loadApps = () => {
fetch(`${globalUrl}/api/v1/apps`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
return response.json();
})
.then((responseJson) => {
if (responseJson === null) {
console.log("null-response from server")
const pretend_apps = [{
"name": "TBD",
"app_name": "TBD",
"app_version": "TBD",
"description": "TBD",
"version": "TBD",
"large_image": "",
}]
setApps(pretend_apps)
return
}
if (responseJson.success === false) {
console.log("error loading apps: ", responseJson)
return
}
setApps(responseJson);
})
.catch((error) => {
console.log("App loading error: " + error.toString());
})
}
useEffect(() => {
loadApps()
}, [])
if (keys === undefined || keys === null || keys.length === 0) {
return null
}
const getUsecase = (name, index, subindex) => {
fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(name.replaceAll(" ", "_"))}`, {
const parseUsecase = (parsedUsecase, subcase) => {
console.log("parseUsecase: ", parsedUsecase, subcase)
}
const getUsecase = (subcase, index, subindex) => {
console.log(subcase)
fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(subcase.name.replaceAll(" ", "_"))}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -129,14 +182,26 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
return response.json();
})
.then((responseJson) => {
var parsedUsecase = responseJson
if (responseJson.success === false) {
setInputUsecase({
"name": name,
})
parsedUsecase = subcase
//img1={inputUsecase.srcimg}
//srcapp={inputUsecase.srcapp}
//img2={inputUsecase.dstimg}
//dstapp={inputUsecase.dstapp}
//title={inputUsecase.name}
parsedUsecase = parseUsecase(parsedUsecase, subcase)
} else {
setInputUsecase(responseJson)
parsedUsecase = responseJson
parsedUsecase = parseUsecase(parsedUsecase, responseJson)
}
// Look for the type of app and fill in img1, srcapp...
setInputUsecase(parsedUsecase)
setExpandedIndex(index)
setExpandedItem(subindex)
@@ -289,6 +354,7 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
if (selectedItem && subcase.matches.length > 0 && selectedWorkflows.length === 0 && firstLoad === true) {
setFirstLoad(false)
setSelectedWorkflows(subcase.matches)
}
}
@@ -316,10 +382,8 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
}
}
//const backgroundColor = selectedItem ? "inherit" : finished ? "inherit" : usecase.color
const finished = subcase.matches.length > 0
const backgroundColor = theme.palette.surfaceColor
//"inherit"
const itemBorder = `${selectedItem ? "3px" : expandedItem >= 0 ? "0px" : "1px"} solid ${usecase.color}`
const fixedName = subcase.name.toLowerCase().replace("_", " ")
@@ -330,14 +394,14 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
//setSelectedWorkflows([])
if (selectedItem) {
} else {
getUsecase(subcase.name, index, subindex)
getUsecase(subcase, index, subindex)
navigate(`/usecases?selected_object=${fixedName}`)
//const newitem = removeParam("selected_object", cursearch);
//navigate(curpath + newitem)
}
}}>
<Paper style={{padding: 25, minHeight: 75, cursor: !selectedItem ? "pointer" : "default", border: itemBorder, backgroundColor: backgroundColor,}} onClick={() => {
<Paper style={{padding: 25, minHeight: isCloud ? 75 : 122, cursor: !selectedItem ? "pointer" : "default", border: itemBorder, backgroundColor: backgroundColor,}} onClick={() => {
}}>
{!selectedItem ?
<div style={{textAlign: "left", position: "relative",}}>
@@ -730,10 +794,8 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
setSelectedWorkflows(newValue)
//setUpdate(Math.random())
}}
renderOption={(props, option) => {
//console.log("In options?: ", props, option)
var newname = props.name
renderOption={(props, data, state) => {
var newname = data.name
if (newname === undefined || newname === null) {
newname = "placeholder"
}
@@ -745,8 +807,8 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
<li {...props}>
<Tooltip arrow placement="left" title={
<span style={{}}>
{props.image !== undefined && props.image !== null && props.image.length > 0 ?
<img src={props.image} alt={newname} style={{backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette.borderRadius, }} />
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={newname} style={{backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette.borderRadius, }} />
: null}
<Typography>
Choose {newname}
@@ -758,7 +820,7 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
checkedIcon={<CheckBoxIcon fontSize="small" />}
style={{ marginRight: 8 }}
checked={option.selected}
checked={selectedWorkflows.find(wf => wf.id === data.id) !== undefined}
/>
{newname}
</span>
@@ -781,24 +843,26 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
}}
/>
: null}
{/*subcase.matches.length > 0 ?
<Grid container style={{maxWidth: 325, marginTop: 10, }}>
{subcase.matches.map((workflow, workflowindex) => {
return (
<Grid key={workflowindex} item index={workflowindex} xs={12}>
<WorkflowPaper key={workflowindex} data={workflow} />
</Grid>
)
})}
</Grid>
:
<div>
<Typography variant="body1" color="textSecondary">
No workflow selected yet.
<span style={{top: 30, position: "relative",}}>
<Typography variant="body1" style={{marginTop: 0,}} onClick={() => {}}>
Try it out
</Typography>
</div>
*/}
<WorkflowTemplatePopup
userdata={userdata}
globalUrl={globalUrl}
img1={inputUsecase.srcimg}
srcapp={inputUsecase.srcapp}
img2={inputUsecase.dstimg}
dstapp={inputUsecase.dstapp}
title={inputUsecase.name}
description={inputUsecase.description}
apps={apps}
/>
</span>
{/*
{subcase.extra_buttons !== undefined && subcase.extra_buttons !== null && subcase.extra_buttons.length > 0 ?
<div style={{marginTop: 25, }}>
@@ -852,15 +916,9 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
<Typography variant="body1" style={{marginTop: 15, cursor: "pointer",}} onClick={() => {}}>
See other Public Workflows for {} <OpenInNewIcon style={{marginTop: 5, marginLeft: 15, }}/>
</Typography>
{/*
<div>
<Typography variant="body1" color="textSecondary">
No workflows yet.
</Typography>
</div>
*/}
</a>
</div>
*/}
</div>
}
<div style={{
@@ -868,6 +926,8 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
width: 350,
borderRadius: theme.palette.borderRadius,
border: "1px solid rgba(255,255,255,0.3)",
padding: 5,
backgroundColor: theme.palette.backgroundColor,
}}>
<AppFramework
inputUsecase={inputUsecase}
+13 -6
View File
@@ -305,7 +305,6 @@ const LoginDialog = (props) => {
paddingBottom: "30px",
paddingTop: "30px",
position: "relative",
backgroundColor: theme.palette.inputColor,
textAlign: "left",
marginTop: 15,
}}
@@ -331,17 +330,25 @@ const LoginDialog = (props) => {
variant="body2"
style={{ marginBottom: 20, color: "white" }}
>
<b>1.</b> Make sure shuffle-database folder has correct access:{" "}
<b>1.</b> Make sure shuffle-database folder has correct access, and that you have a minimum of <b>2Gb of RAM available</b>:{" "}
<br />
<br />
sudo chown 1000:1000 -R shuffle-database
</Typography>
<Typography
variant="body2"
style={{ marginBottom: 20, color: "white" }}
>
<b>2</b>. Restart docker-compose:
<b>2.</b> Disable memory swap on the host
<br />
<br />
sudo swapoff -a
</Typography>
<Typography
variant="body2"
style={{ marginBottom: 20, color: "white" }}
>
<b>3</b>. Restart docker-compose:
<br />
<br />
sudo docker-compose restart
@@ -513,7 +520,7 @@ const LoginDialog = (props) => {
//id="sso_button"
const ssoBtn = document.getElementById("sso_button");
if (ssoBtn !== undefined && ssoBtn !== null) {
console.log("SSO BTN: ", ssoBtn)
//console.log("SSO BTN: ", ssoBtn)
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
var tmpView = new URLSearchParams(cursearch).get("autologin");
if (tmpView !== undefined && tmpView !== null) {
@@ -527,7 +534,7 @@ const LoginDialog = (props) => {
}, 200);
}, [ssoUrl])
return <div>{loadedCheck}</div>;
return <div style={{paddingBottom: 150, }}>{loadedCheck}</div>;
};
export default LoginDialog;
+48 -36
View File
@@ -23,6 +23,7 @@ import {
} from '@mui/material';
import theme from '../theme.jsx';
import { useNavigate, Link } from "react-router-dom";
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import Drift from "react-driftjs";
const Welcome = (props) => {
@@ -301,44 +302,47 @@ const Welcome = (props) => {
flex: 1,
padding: 0,
textAlign: "center",
maxWidth: 300,
minWidth: 300,
maxWidth: 275,
minWidth: 275,
backgroundColor: theme.palette.surfaceColor,
color: "white",
borderRadius: theme.palette.borderRadius,
}
const actionObject = {
padding: "35px",
maxHeight: 300,
minHeight: 300,
padding: "25px",
maxHeight: 280,
minHeight: 280,
borderRadius: theme.palette.borderRadius,
}
const imageStyle = {
width: 150,
width: 70,
// height: 150,
// margin: "auto",
// marginTop: 10,
borderRadius: 75,
marginBottom: 18,
// borderRadius: 75,
objectFit: "scale-down",
}
const buttonStyle = {
borderRadius: 8,
borderRadius: 200,
height: 51,
width: 464,
fontSize: 16,
background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
// background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
background: "linear-gradient(90deg, #F86744 0%, #F34475 100%)",
padding: "16px 24px",
top: 75,
top: 105,
margin: "auto",
itemAlign: "center",
marginLeft: "65px",
}
const defaultImage = "/images/experienced.png"
const experienced_image = userdata !== undefined && userdata !== null && userdata.active_org !== undefined && userdata.active_org.image !== undefined && userdata.active_org.image !== null && userdata.active_org.image !== "" ? userdata.active_org.image : defaultImage
return (
<div style={{width: 1000, margin: "auto", paddingBottom: 150, minHeight: 1500, marginTop: 50, }}>
<div style={{ margin: "auto", paddingBottom: 150, minHeight: 1500, marginTop: 50, }}>
{/*
<div style={{position: "fixed", bottom: 110, right: 110, display: "flex", }}>
<img src="/images/Arrow.png" style={{width: 250, height: "100%",}} />
@@ -346,7 +350,7 @@ const Welcome = (props) => {
*/}
{showWelcome === true ?
<div>
<div style={{minWidth: 500, maxWidth: 500, margin: "auto", marginTop: isCloud ? "auto" : 20, }}>
{/* <div style={{minWidth: 500, maxWidth: 500, margin: "auto", marginTop: isCloud ? "auto" : 20, }}>
<Stepper
activeStep={activeStep}
color="primary"
@@ -379,8 +383,8 @@ const Welcome = (props) => {
)
})}
</Stepper>
</div>
<Grid container spacing={2} style={{ padding: 0, maxWidth: 1000, minWidth: 1000, margin: "auto", }}>
</div> */}
<Grid container spacing={1} style={{ padding: 0, maxWidth: 500, minWidth: 500, marginRight : "auto" , marginLeft: "auto" }}>
<Grid item xs={window.location.href.includes("tab=2") ? 6 : 12}>
<div>
{/*
@@ -414,7 +418,7 @@ const Welcome = (props) => {
/>
</div>
</Grid>
{frameworkData === undefined || window.location.href.includes("tab=1") || window.location.href.includes("tab=3") ? null :
{/* {frameworkData === undefined || window.location.href.includes("tab=1") || window.location.href.includes("tab=3") ? null :
<div style={{marginTop: 25, }}>
<Typography variant="h6" style={{textAlign: "center", marginBottom: 25, }}>
App Framework
@@ -439,32 +443,35 @@ const Welcome = (props) => {
/>
</Fade>
</div>
}
} */}
</Grid>
</div>
:
<Fade in={true}>
<div style={{maxWidth: 700, margin: "auto", marginTop: 50, }}>
{/*
<div style={{display:"flex"}}>
<ArrowBackIosIcon style={{color: "#9E9E9E",}} onClick={() => {
navigate("/login")
<div style={{maxWidth: 590, margin: "auto", marginTop: 50, }}>
{/* <div style={{display:"flex"}}>
<ArrowBackIosIcon style={{color: "#9E9E9E", width: "16px",}} onClick={() => {
navigate("/workflows")
}}/>
<Typography variant="body1" style={{color: "#9E9E9E",textAlign: "center", marginBottom: 50, paddingRight: "366px"}} onClick={() => {
navigate("/login")
<Typography variant="h8" style={{color: "#9E9E9E",textAlign: "center", marginBottom: 20, paddingRight: "366px"}} onClick={() => {
navigate("/workflows")
}}>
Back
</Typography>
</div>
*/}
<Typography variant="h4" style={{color: "#F1F1F1", textAlign: "center", marginTop: 50, }}>
</div> */}
<Typography variant="h4" style={{color: "#F1F1F1", marginTop: 50, fontSize: 32}}>
Help us get to know you
</Typography>
<Typography variant="body1" style={{color: "#9E9E9E", textAlign: "center", marginBottom: 50,}}>
We will use this information to personalize your automation
<Typography variant="body1" style={{color: "#9E9E9E", marginTop: 12 ,marginBottom: 50, fontSize: 16}}>
Let us help you create a smoother journey.
</Typography>
{/* <Typography variant="body1" style={{color: "#9E9E9E", marginTop: 12 ,marginBottom: 50, fontSize: 16}}>
We will use this information to personalize your automation
</Typography> */}
<div style={{display: "flex", marginTop: 70, width: 700, margin: "auto",}}>
<div style={{border: "1px solid #806BFF", borderRadius: theme.palette.borderRadius, }}>
<div style={{border: "2px solid #806BFF", borderRadius: theme.palette.borderRadius, }}>
<Card style={paperObject} onClick={() => {
if (isCloud) {
ReactGA.event({
@@ -506,7 +513,7 @@ const Welcome = (props) => {
navigate("/workflows?message=Skipped intro")
}}>
<CardActionArea style={actionObject}>
<img src={experienced_image} style={{padding: experienced_image === defaultImage ? 38 : 10, objectFit: "scale-down", minHeight: experienced_image === defaultImage ? 40 : 70, maxHeight: experienced_image === defaultImage ? 40 : 70, bordeRadius: theme.palette.borderRadius, }} />
<img src={experienced_image} style={{padding: experienced_image === defaultImage ? 2 : 10, objectFit: "scale-down", minHeight: experienced_image === defaultImage ? 40 : 70, maxHeight: experienced_image === defaultImage ? 40 : 70, bordeRadius: theme.palette.borderRadius, marginBottom: experienced_image === defaultImage ? 24 : 2 }} />
<Typography variant="h4" style={{color: "#F1F1F1"}}>
Experienced
</Typography>
@@ -517,17 +524,22 @@ const Welcome = (props) => {
</Card>
</div>
{/*
<div style={{display: "flex", flexDirection: "row", }}>
<div style={{ flexDirection: "row", }}>
<Button variant="contained" type="submit" fullWidth style={buttonStyle} onClick={() => {
navigate("/workflows?message=Skipped intro continue")
ReactGA.event({
category: "welcome",
action: "click_welcome_continue",
label: "",
})
}}>
Continue
</Button>
</div>
*/}
<div style={{margin: "auto", borderRadius: theme.palette.borderRadius, marginTop: 50, width: 200, overflow: "wrap", padding: 25, cursor: "pointer", }} onClick={() => {
{/* <div style={{margin: "auto", borderRadius: theme.palette.borderRadius, marginTop: 50, width: 200, overflow: "wrap", padding: 25, cursor: "pointer", }} onClick={() => {
if (window.drift !== undefined) {
window.drift.api.startInteraction({ interactionId: 340045 })
} else {
@@ -537,7 +549,7 @@ const Welcome = (props) => {
<Typography variant="body1" style={{margin: "auto", textAlign: "center"}}>
Want a demo instead?
</Typography>
</div>
</div> */}
</div>
</Fade>
}
+92 -102
View File
@@ -533,7 +533,6 @@ const Workflows = (props) => {
document.title = "Shuffle - Workflows";
let navigate = useNavigate();
//const alert = useAlert();
const classes = useStyles(theme);
const imgSize = 60;
@@ -590,6 +589,9 @@ const Workflows = (props) => {
const [drawerOpen, setDrawerOpen] = React.useState(false)
const [videoViewOpen, setVideoViewOpen] = React.useState(false)
const [gettingStartedItems, setGettingStartedItems] = React.useState([])
const [selectedWorkflowIndexes, setSelectedWorkflowIndexes] = React.useState([])
const [apps, setApps] = React.useState([]);
const drawerWidth = drawerOpen ? 325 : 0
@@ -725,6 +727,30 @@ const Workflows = (props) => {
}
};
const getApps = () => {
fetch(`${globalUrl}/api/v1/apps`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
}
return response.json();
})
.then((responseJson) => {
setApps(responseJson);
})
.catch((error) => {
console.log("App loading error: "+error.toString());
});
}
const addFilter = (data) => {
if (data === null || data === undefined) {
console.log("No filter data")
@@ -891,13 +917,15 @@ const Workflows = (props) => {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: 500,
padding: 50,
},
}}
>
<DialogTitle>
<div style={{ textAlign: "center", color: "rgba(255,255,255,0.9)" }}>
Are you sure you want to delete this workflow? <div />
Other workflows relying on this one may stop working
Are you sure you want to delete {selectedWorkflowId.length > 0 ? filteredWorkflows.find((w) => w.id === selectedWorkflowId).name : `${selectedWorkflowIndexes.length} workflow${selectedWorkflowIndexes.length === 1 ? '' : 's'}`}? <div />
Other workflows relying on {selectedWorkflowIndexes.length > 0 ? "them" : "it"} one will stop working
</div>
</DialogTitle>
<DialogContent
@@ -912,6 +940,20 @@ const Workflows = (props) => {
setTimeout(() => {
getAvailableWorkflows();
}, 1000);
} else if (selectedWorkflowIndexes.length > 0) {
// Do backwards so it doesn't change
for (var i = selectedWorkflowIndexes.length - 1; i >= 0; i--) {
const workflow = filteredWorkflows[selectedWorkflowIndexes[i]-1]
if (workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null) {
deleteWorkflow(workflow.id);
}
}
setTimeout(() => {
getAvailableWorkflows();
}, 1000);
setSelectedWorkflowIndexes([]);
}
setDeleteModalOpen(false);
}}
@@ -1223,6 +1265,7 @@ const Workflows = (props) => {
setView(tmpView);
}
getApps()
getAvailableWorkflows();
getFramework()
}
@@ -1280,6 +1323,7 @@ const Workflows = (props) => {
color: "white",
margin: "10px",
backgroundColor: theme.palette.surfaceColor,
position: "relative",
};
const workflowActionStyle = {
@@ -1710,6 +1754,7 @@ const Workflows = (props) => {
onClick={(event) => {
event.stopPropagation()
ReactDOM.unstable_batchedUpdates(() => {
setIsEditing(true)
setModalOpen(true);
setEditingWorkflow(JSON.parse(JSON.stringify(data)));
setNewWorkflowName(data.name);
@@ -2654,6 +2699,7 @@ const Workflows = (props) => {
renderCell: (params) => {},
},
];
let rows = [];
rows = filteredWorkflows.map((data, index) => {
let obj = {
@@ -2671,17 +2717,55 @@ const Workflows = (props) => {
className={classes.datagrid}
rows={rows}
columns={columns}
pageSize={20}
pageSize={25}
checkboxSelection
autoHeight
density="standard"
onSelectionModelChange={(newSelection) => {
//setSelectedWorkflows(newSelection.selectionModel);
console.log(newSelection)
setSelectedWorkflowIndexes(newSelection)
}}
selectionModel={selectedWorkflowIndexes}
components={{
Toolbar: GridToolbar,
}}
/>
);
}
return <div style={gridContainer}>{workflowData}</div>;
return (
<div style={gridContainer}>
<Tooltip title={`New Workflow`} placement="bottom">
<IconButton
style={{position: "absolute", top: 10, right: 50, zIndex: 1000}}
onClick={() => {
setModalOpen(true)
setIsEditing(false)
}}
>
<AddCircleIcon style={{}} />
</IconButton>
</Tooltip>
{filteredWorkflows.length === 0 ? null :
<IconButton
style={{position: "absolute", top: 10, right: 10, zIndex: 1000}}
disabled={selectedWorkflowIndexes.length === 0}
onClick={() => {
setDeleteModalOpen(true)
}}
>
<Tooltip
title={`Delete ${selectedWorkflowIndexes.length} workflows`}
placement="top"
>
<DeleteIcon />
</Tooltip>
</IconButton>
}
{workflowData}
</div>
)
};
var total_count = 0
@@ -3152,56 +3236,6 @@ const Workflows = (props) => {
const WorkflowView = () => {
if (workflows.length === 0) {
// Not going there yet
//if ((userdata.tutorials !== undefined && userdata.tutorials !== null && !userdata.tutorials.includes("getting-started")) || userdata.tutorials === null) {
// return <Navigate to="/getting-started" replace />;
//}
//return (
// <div style={emptyWorkflowStyle}>
// <Paper style={boxStyle}>
// <div>
// <h2>Welcome to Shuffle</h2>
// </div>
// <div>
// <p>
// <b>Shuffle</b> is a flexible, easy to use, automation platform
// allowing users to integrate their services and devices freely.
// It's made to significantly reduce the amount of manual labor,
// and is focused on security applications.{" "}
// <a
// href="/docs/about"
// style={{ textDecoration: "none", color: "#f85a3e" }}
// >
// Click here to learn more.
// </a>
// </p>
// </div>
// <div>
// If you want to jump straight into it, click here to create your
// first workflow:
// </div>
// <div style={{ display: "flex" }}>
// <Button
// id="second-step"
// color="primary"
// style={{ marginTop: "20px" }}
// variant="outlined"
// onClick={() => setModalOpen(true)}
// >
// New workflow
// </Button>
// <span style={{ paddingTop: 20, display: "flex" }}>
// <Typography
// style={{ marginTop: 5, marginLeft: 30, marginRight: 15 }}
// >
// ..OR
// </Typography>
// {workflowButtons}
// </span>
// </div>
// </Paper>
// </div>
//)
}
var workflowDelay = -150
@@ -3243,6 +3277,7 @@ const Workflows = (props) => {
minWidth: 275,
},
}}
rows={1}
placeholder="Filter Workflows"
color="primary"
fullWidth
@@ -3268,53 +3303,6 @@ const Workflows = (props) => {
{workflowButtons}
</div>
</div>
{/*
<div style={flexContainerStyle}>
<div style={{...flexBoxStyle, ...activeWorkflowStyle}}>
<div style={flexContentStyle}>
<div><img src={mobileImage} style={iconStyle} /></div>
<div style={ blockRightStyle }>
<div style={counterStyle}>{workflows.length}</div>
<div style={fontSize_16}>ACTIVE WORKFLOWS</div>
</div>
</div>
</div>
<div style={{...flexBoxStyle, ...availableWorkflowStyle}}>
<div style={flexContentStyle}>
<div><img src={bookImage} style={iconStyle} /></div>
<div style={ blockRightStyle }>
<div style={counterStyle}>{workflows.length}</div>
<div style={fontSize_16}>AVAILABE WORKFLOWS</div>
</div>
</div>
</div>
<div style={{...flexBoxStyle, ...notificationStyle}}>
<div style={flexContentStyle}>
<div><img src={bagImage} style={iconStyle} /></div>
<div style={ blockRightStyle }>
<div style={counterStyle}>{workflows.length}</div>
<div style={fontSize_16}>NOTIFICATIONS</div>
</div>
</div>
</div>
</div>
*/}
{/*
chipRenderer={({ value, isFocused, isDisabled, handleClick, handleRequestDelete }, key) => {
console.log("VALUE: ", value)
return (
<Chip
key={key}
style={chipStyle}
>
{value}
</Chip>
)
}}
*/}
<div style={{width: "100%", minHeight: isMobile ? 0 : 51, maxHeight: isMobile ? 0 : 51, marginTop: 10, }}>
{!isMobile && usecases !== null && usecases !== undefined && usecases.length > 0 ?
@@ -3928,6 +3916,7 @@ const Workflows = (props) => {
{modalOpen === true ?
<EditWorkflow
globalUrl={globalUrl}
userdata={userdata}
workflow={editingWorkflow}
setWorkflow={setEditingWorkflow}
modalOpen={modalOpen}
@@ -3938,6 +3927,7 @@ const Workflows = (props) => {
isEditing={isEditing}
workflows={workflows}
apps={apps}
setWorkflows={setWorkflows}
/>
: null}
+8 -8
View File
@@ -1,5 +1,5 @@
NAME=shuffle-worker
VERSION=1.2.1
VERSION=1.2.2
echo "Running docker build with $NAME:$VERSION"
#CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
@@ -11,10 +11,10 @@ docker build . -t frikky/shuffle:$NAME -t frikky/shuffle:$NAME_$VERSION -t docke
#docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION
#docker tag frikky/shuffle:0.8.51 ghcr.io/frikky/shuffle-worker:0.8.5
#docker tag frikky/shuffle:$NAME ghcr.io/frikky/shuffle-worker:0.8.52
docker push frikky/shuffle:$NAME
docker push ghcr.io/frikky/$NAME:$VERSION
docker push ghcr.io/frikky/$NAME:nightly
docker push shuffle/shuffle:$NAME
docker push ghcr.io/shuffle/$NAME:$VERSION
docker push ghcr.io/shuffle/$NAME:nightly
#docker push frikky/shuffle:$NAME
#docker push ghcr.io/frikky/$NAME:$VERSION
#docker push ghcr.io/frikky/$NAME:nightly
#
#docker push shuffle/shuffle:$NAME
#docker push ghcr.io/shuffle/$NAME:$VERSION
#docker push ghcr.io/shuffle/$NAME:nightly
+7 -5
View File
@@ -11,7 +11,7 @@ require (
github.com/gorilla/mux v1.8.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.4.17
github.com/shuffle/shuffle-shared v0.4.50
)
require (
@@ -47,16 +47,18 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opensearch-project/opensearch-go v1.1.0 // indirect
github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
go.opencensus.io v0.24.0 // indirect
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.106.0 // indirect
+53
View File
@@ -60,17 +60,34 @@ github.com/adrg/strutil v0.2.3 h1:WZVn3ItPBovFmP4wMHHVXUr8luRaHrbyIuLlHt32GZQ=
github.com/adrg/strutil v0.2.3/go.mod h1:+SNxbiH6t+O+5SZqIj5n/9i5yUjR+S3XXVrjEcN2mxg=
github.com/algolia/algoliasearch-client-go/v3 v3.18.1 h1:FP2Xtqqs/sefR5Qluygp+jVV+juXzEdJaPrZTCDLhDQ=
github.com/algolia/algoliasearch-client-go/v3 v3.18.1/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk=
github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
github.com/aws/aws-sdk-go v1.44.263/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4=
github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 h1:hjXJeBcAMS1WGENGqDpzvmgS43oECTx8UXq31UBu0Jw=
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 h1:/P9/RL0xgWE+ehnCUUN5h3RpG3dmoMCOONO1CCvq23Y=
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013/go.mod h1:pccXHIvs3TV/TUqSNyEvF99sxjX2r4FFRIyw6TZY9+w=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -86,6 +103,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frikky/go-elasticsearch/v8 v8.13.1 h1:GB+Wr0Yx8efG7D1jc9fGGiqjjRRngWJTcMSua3QIDaM=
github.com/frikky/go-elasticsearch/v8 v8.13.1/go.mod h1:RPq0JXPQVVSFHTlPwj/go8BZ1hegRf+StaSpT2iGIoQ=
@@ -144,6 +162,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo=
@@ -155,6 +174,7 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -181,6 +201,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -201,6 +223,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M=
github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo=
github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ=
github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -222,6 +248,8 @@ github.com/shuffle/shuffle-shared v0.4.9 h1:mGCaLcSbrsQCy26pJXPlZAtitEzEEwqotGNj
github.com/shuffle/shuffle-shared v0.4.9/go.mod h1:jQrYySmvp/0De5ftrAaY6xwwr7TMfqBmBxQ2AX9yrjQ=
github.com/shuffle/shuffle-shared v0.4.17 h1:56ll366bdmIJu/7GFqNC2XTjjl0SGBf430PSq+EB6Ro=
github.com/shuffle/shuffle-shared v0.4.17/go.mod h1:jQrYySmvp/0De5ftrAaY6xwwr7TMfqBmBxQ2AX9yrjQ=
github.com/shuffle/shuffle-shared v0.4.50 h1:fJLfhWIJ5mYap4JwHnD/B5aaLyIULwylFSl3FoWlajM=
github.com/shuffle/shuffle-shared v0.4.50/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -235,10 +263,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -256,6 +286,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -322,8 +354,14 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -344,6 +382,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -375,17 +414,31 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=