diff --git a/backend/go-app/main.go b/backend/go-app/main.go index efc61caf..ccba422d 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -969,6 +969,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { tutorialsFinished = append(tutorialsFinished, tutorial) } + licensed := shuffle.IsLicensed(ctx, *currentOrg) returnValue := shuffle.HandleInfo{ Success: true, @@ -989,6 +990,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { Tutorials: tutorialsFinished, Priorities: orgPriorities, + Licensed: licensed, } returnData, err := json.Marshal(returnValue) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 40f9ba3a..634e3acf 100755 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -293,12 +293,35 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { env, err := shuffle.GetEnvironment(ctx, orgId, "") timeNow := time.Now().Unix() if err == nil && len(env.Id) > 0 && len(env.Name) > 0 { + // Updates every 60 seconds~ if time.Now().Unix() > env.Edited+60 { - env.RunningIp = request.RemoteAddr + env.RunningIp = shuffle.GetRequestIp(request) + if len(orborusLabel) > 0 { + env.RunningIp = orborusLabel + } + + if request.Method == "POST" { + body, err := ioutil.ReadAll(request.Body) + if err == nil { + var envData shuffle.OrborusStats + err = json.Unmarshal(body, &envData) + if err == nil { + if envData.Swarm { + env.Licensed = true + env.RunType = "docker" + } + + if envData.Kubernetes { + env.RunType = "k8s" + } + } + } + } + env.Checkin = timeNow - err = shuffle.SetEnvironment(ctx, env) + err = shuffle.SetEnvironment(ctx, &env) if err != nil { - log.Printf("[WARNING] Failed updating environment: %s", err) + log.Printf("[ERROR] Failed updating environment: %s", err) } } } diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 817e6295..c3687489 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -120,10 +120,11 @@ const Billing = (props) => { minHeight: 280, maxWidth: 400, width: "100%", - backgroundColor: theme.palette.surfaceColor, - borderRadius: theme.palette.borderRadius, + backgroundColor: theme.palette.platformColor, + borderRadius: theme.palette.borderRadius*2, border: "1px solid rgba(255,255,255,0.3)", marginRight: 10, + marginTop: 15, } const isCloud = @@ -273,6 +274,7 @@ const Billing = (props) => { const [signatureOpen, setSignatureOpen] = React.useState(false); const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed) + const [hovered, setHovered] = React.useState(false) var top_text = "Base Cloud Access" if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) { @@ -321,8 +323,16 @@ const Billing = (props) => { newPaperstyle.border = "1px solid #f85a3e" } + if (hovered) { + newPaperstyle.backgroundColor = theme.palette.surfaceColor + } + return ( - + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > { const isChildOrg = userdata.active_org.creator_org !== "" && userdata.active_org.creator_org !== undefined && userdata.active_org.creator_org !== null return ( -
+
{addDealModal} - Billing + Billing & Licensing {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." + "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. 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." + "Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." } @@ -1315,7 +1325,7 @@ const Billing = (props) => {
) : null*/} -
+
{ const data = (
- + All shown statistics are gathered from Your Organization Statistics - This is a feature to help give you more insight into Shuffle, and to understand your utilization of the Shuffle platform. The billing tracker is in Beta, and is always calculated manually before being invoiced. + >Your Organisation Statistics. + It exists to give you more insight into your workflows, and to understand your utilization of the Shuffle platform. The billing tracker is in Beta, and is always calculated manually before being invoiced.
diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index 07e21578..298c0511 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -57,6 +57,7 @@ import { Publish as PublishIcon, OpenInNew as OpenInNewIcon, Add as AddIcon, + Remove as RemoveIcon, } from "@mui/icons-material"; const EditWorkflow = (props) => { @@ -76,7 +77,8 @@ const EditWorkflow = (props) => { const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "") const [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date*1000) : dayjs().subtract(1, 'day')) - const [inputFields, setInputFields] = React.useState([]) + console.log("WORKFLOW: ", workflow) + const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : []) const classes = useStyles(); @@ -232,7 +234,91 @@ const EditWorkflow = (props) => {
- +
+ {/* + + */} + +
+ +
{ @@ -477,41 +563,21 @@ const EditWorkflow = (props) => { fullWidth /> - {/* - + Input fields - + Input fields are fields that will be used during the startup of the workflow. These will be formatted in JSON and is most commonly used from the workflow run page. - - - {inputFields.length === 0 ? - - : null} - {inputFields.map((data, index) => { + {inputQuestions.map((data, index) => { console.log("Inputfield: ", data) return (
{ marginRight: 5, }} fullWidth={true} - placeholder="Name" + placeholder="Question" + id="standard-required" + margin="normal" + variant="outlined" + defaultValue={data.name} + onChange={(e) => { + inputQuestions[index].name = e.target.value + setInputQuestions(inputQuestions) + setUpdate(Math.random()); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + minHeight: 50, + }, + }} + /> + { - inputFields[index].name = e.target.value - setInputFields(inputFields) + inputQuestions[index].value = e.target.value + setInputQuestions(inputQuestions) setUpdate(Math.random()); }} InputProps={{ @@ -544,28 +641,44 @@ const EditWorkflow = (props) => {
) })} - */} + + : null} { setShowMoreClicked(!showMoreClicked); }} @@ -577,71 +690,7 @@ const EditWorkflow = (props) => { - - - - + {newWorkflow === true ? diff --git a/frontend/src/components/NewHeader.jsx b/frontend/src/components/NewHeader.jsx index c0cf173d..4f6ecbdd 100644 --- a/frontend/src/components/NewHeader.jsx +++ b/frontend/src/components/NewHeader.jsx @@ -70,6 +70,7 @@ const Header = (props) => { const [anchorEl, setAnchorEl] = React.useState(null); const [anchorElAvatar, setAnchorElAvatar] = React.useState(null); const [subAnchorEl, setSubAnchorEl] = React.useState(null); + const [upgradeHovered, setUpgradeHovered] = React.useState(false); let navigate = useNavigate(); const handleClick = (event) => { @@ -1097,10 +1098,10 @@ const Header = (props) => { )} {/* Show on cloud, if not suborg and if not customer/pov/internal */} - {isCloud && - (userdata.org_status === undefined || - userdata.org_status === null || - userdata.org_status.length === 0) ? ( + { + userdata.licensed !== undefined && + userdata.licensed !== null && + userdata.licensed === false ? { marginTop: 0, }} > - + - ) : null} + : null} {userdata === undefined || userdata.app_execution_limit === undefined || @@ -1145,7 +1165,6 @@ const Header = (props) => { textAlign: "center", cursor: "pointer", borderRadius: theme.palette.borderRadius, - marginRight: 10, marginTop: 5, backgroundColor: theme.palette.surfaceColor, minWidth: 60, @@ -1154,7 +1173,7 @@ const Header = (props) => { userdata.app_execution_usage / userdata.app_execution_limit >= 0.9 - ? "#f86a3e" + ? "2px solid #f86a3e" : null, }} onClick={() => { diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index 476fab54..016670c2 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -3258,7 +3258,6 @@ const ParsedAction = (props) => { } } - console.log("DID NAME REPLACE ACTUALLY WORK? - may be missing it in certain triggers"); setWorkflow(workflow); setUpdate(Math.random()); baselabel = name diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index e4559e69..4890918f 100755 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -70,6 +70,9 @@ import { Business as BusinessIcon, Visibility as VisibilityIcon, VisibilityOff as VisibilityOffIcon, + Cancel as CancelIcon, + Dns as DnsIcon, + Help as HelpIcon, Flag as FlagIcon, FmdGood as FmdGoodIcon, @@ -193,9 +196,26 @@ const Admin = (props) => { const [, forceUpdate] = React.useState(); useEffect(() => { - getUsers() + getUsers() + + setTimeout(() => { + if (adminTab === 3) { + window.scroll({ + top: 450, + left: 0, + behavior: 'smooth' + }) + } + }, 1500) }, []); + useEffect(() => { + window.scroll({ + top: 450, + left: 0, + behavior: 'smooth' + }) + }, [adminTab]); useEffect(() => { if (isDropzone) { @@ -970,6 +990,8 @@ If you're interested, please let me know a time that works for you, or set up a .then((responseJson) => { if (!responseJson.success && responseJson.reason !== undefined) { toast("Failed to deactivate user: " + responseJson.reason); + } else if (responseJson.success === false) { + toast("Failed to deactivate user. Please contact support@shuffler.io if this persists.") } else { toast("Changed activation for user " + data.id); } @@ -1146,55 +1168,54 @@ If you're interested, please let me know a time that works for you, or set up a }); }; -const handleClickChangeOrg = (orgId) => { - // Don't really care about the logout - //name: org.name, - //orgId = "asd" - const data = { - org_id: orgId, - } - - localStorage.setItem("globalUrl", "") - localStorage.setItem("getting_started_sidebar", "open"); - - fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { - mode: 'cors', - credentials: 'include', - crossDomain: true, - method: 'POST', - body: JSON.stringify(data), - 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) { - if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) { - console.log("Region Change: ", responseJson.region_url) - localStorage.setItem("globalUrl", responseJson.region_url) - //globalUrl = responseJson.region_url - } - - setTimeout(() => { - window.location.reload() - }, 2000) - toast("Successfully changed active organization - refreshing!") - } else { - toast("Failed changing org: ", responseJson.reason) - } - }) - .catch(error => { - console.log("error changing: ", error) - //removeCookie("session_token", {path: "/"}) - }) -} + const handleClickChangeOrg = (orgId) => { + // Don't really care about the logout + //name: org.name, + //orgId = "asd" + const data = { + org_id: orgId, + } + + localStorage.setItem("globalUrl", "") + localStorage.setItem("getting_started_sidebar", "open"); + + fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { + mode: 'cors', + credentials: 'include', + crossDomain: true, + method: 'POST', + body: JSON.stringify(data), + 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) { + if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) { + localStorage.setItem("globalUrl", responseJson.region_url) + //globalUrl = responseJson.region_url + } + + setTimeout(() => { + window.location.reload() + }, 2000) + toast("Successfully changed active organization - refreshing!") + } else { + toast("Failed changing org: "+responseJson.reason) + } + }) + .catch(error => { + console.log("error changing: ", error) + //removeCookie("session_token", {path: "/"}) + }) + } const inviteUser = (data) => { @@ -1850,6 +1871,8 @@ const handleClickChangeOrg = (orgId) => { .then((responseJson) => { if (!responseJson.success && responseJson.reason !== undefined) { toast("Failed setting user: " + responseJson.reason); + } else if (responseJson.success === false) { + toast("Failed to update user") } else { //toast("Set the user field " + field + " to " + value); toast("Successfully updated user field " + field) @@ -2912,6 +2935,7 @@ const handleClickChangeOrg = (orgId) => { )} { }} /> - {adminTab === 0 ? ( - - ) - : adminTab === 1 ? ( -
+ {adminTab === 0 ? ( + + ) + : adminTab === 1 ? ( +
{

Environments

- Decides what Orborus environment to execute an action in a workflow - in.{" "} + Decides what Orborus environment to run your workflow actions. If you have scale problems, talk to our team: support@shuffler.io.  { }} /> - + + + { const queueSize = environment.queue !== undefined && environment.queue !== null ? environment.queue < 0 ? 0 : environment.queue > 1000 ? ">1000" : environment.queue : 0 return ( - - + + + + + + : environment.run_type === "docker" ? + + + + : environment.run_type === "k8s" ? + + + + : + + + + } + style={{ + minWidth: 50, + maxWidth: 50, + overflow: "hidden", + }} + /> + + + + : + + + + + + } + style={{ + minWidth: 85, + maxWidth: 85, + overflow: "hidden", + }} + /> { environment.running_ip === null || environment.running_ip.length === 0 ? -
- Not running -
+
+ Not running +
: environment.running_ip.split(":")[0] : "N/A" } style={{ - minWidth: 200, - maxWidth: 200, + minWidth: 150, + maxWidth: 150, overflow: "hidden", }} /> @@ -4849,11 +4926,7 @@ const handleClickChangeOrg = (orgId) => { /> - { /> - { @@ -4920,7 +4990,7 @@ const handleClickChangeOrg = (orgId) => { letterSpacing: "1px", }} > - Sub Organizations of the Current Organization + Sub Organizations of the Current Organization ({subOrgs.length})
@@ -4940,11 +5010,7 @@ const handleClickChangeOrg = (orgId) => { /> - @@ -4974,11 +5040,7 @@ const handleClickChangeOrg = (orgId) => { /> - { />
) - })} + })*/}
) } @@ -321,8 +340,6 @@ const RunWorkflow = (defaultprops) => { event.preventDefault() } - console.log("In submit!") - stop() setMessage("") setExecutionLoading(true) @@ -330,12 +347,22 @@ const RunWorkflow = (defaultprops) => { setExecutionInfo("") var data = { - "execution_argument": executionArgument + "execution_argument": executionArgument, + "execution_source": "questions", + } + + if (workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) { + try { + data["execution_argument"] = JSON.stringify(executionArgument) + } catch (e) { + console.log("Error parsing execution argument: ", e) + } } if (workflow.start !== undefined && workflow.start !== null && workflow.start.length > 0) { - data.start = workflow.start + //data.start = workflow.start } else { + /* if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { for (let actionkey in workflow.actions) { if (workflow.actions[actionkey].isStartNode) { @@ -344,6 +371,7 @@ const RunWorkflow = (defaultprops) => { } } } + */ } var url = `${globalUrl}/api/v1/workflows/${props.match.params.key}/execute` @@ -369,7 +397,6 @@ const RunWorkflow = (defaultprops) => { console.log("Pre request: ", url, fetchBody) fetch(url, fetchBody) .then((response) => { - console.log("Got answer 1") if (response.status !== 200 && response.status !== 201) { if (answer !== undefined && execution_id !== undefined && authorization !== undefined) { @@ -385,11 +412,9 @@ const RunWorkflow = (defaultprops) => { } } - console.log("Got answer 2") return response.json(); }) .then(responseJson => { - console.log("Got answer 3") setExecutionLoading(false) if (responseJson["success"] === false) { console.log("Failed sending execution request") @@ -404,7 +429,6 @@ const RunWorkflow = (defaultprops) => { start(); } } - console.log("Got answer 4") }) .catch(error => { //setExecutionInfo("Error in workflow startup: " + error) @@ -446,8 +470,18 @@ const RunWorkflow = (defaultprops) => { responseJson.triggers = []; } - handleGetOrg(responseJson.org_id) - setWorkflow(responseJson); + if (responseJson.input_questions !== undefined && responseJson.input_questions !== null && responseJson.input_questions.length > 0) { + var newexec = {} + for (let questionkey in responseJson.input_questions) { + const question = responseJson.input_questions[questionkey] + newexec[question.value] = "" + } + + setExecutionArgument(newexec) + } + + handleGetOrg(responseJson.org_id) + setWorkflow(responseJson); }) .catch((error) => { console.log("Get workflow error: ", error.toString()); @@ -475,6 +509,18 @@ const RunWorkflow = (defaultprops) => { // Doesn't work because this is some async garbage if (executionData.execution_id === undefined || (responseJson.execution_id === executionData.execution_id && responseJson.results !== undefined && responseJson.results !== null)) { if (executionData.status !== responseJson.status || executionData.result !== responseJson.result || (executionData.results !== undefined && responseJson.results !== null && executionData.results.length !== responseJson.results.length)) { + + if (responseJson.result !== undefined && responseJson.result !== null && responseJson.result.length > 0) { + if (responseJson.result.startsWith("[") && responseJson.result.endsWith("]")) { + try { + responseJson.result = JSON.parse(responseJson.result).length + console.log("Set length to: ", responseJson.result) + } catch (e) { + console.log("Error parsing length: ", e) + } + } + } + //console.log("Updating data!") setExecutionData(responseJson) @@ -685,27 +731,50 @@ const RunWorkflow = (defaultprops) => { {message} {answer !== undefined && answer !== null ? null : - Workflow: {workflow.name} + {workflow.name} } - {executionData !== undefined && executionData !== null && executionData !== {} && executionData.status !== undefined && (answer === undefined || answer === null) ? -
- - Status  - - - {executionData.status} - -
- : null} - {workflowQuestion.length > 0 ? {workflowQuestion} : null} - {answer !== undefined && answer !== null ? null : + {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? +
+ {workflow.input_questions.map((question, index) => { + + return ( +
+ {question.name} + { + //setExecutionArgument(e.target.value) + executionArgument[question.value] = e.target.value + }} + /> +
+ ) + })} +
+ : + answer !== undefined && answer !== null ? null : Runtime Argument
@@ -733,6 +802,7 @@ const RunWorkflow = (defaultprops) => {
} + {executionRunning ? diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index 72b0a217..7f236c89 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -7,7 +7,7 @@ go 1.19 require ( github.com/docker/docker v23.0.3+incompatible github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.5.68 + github.com/shuffle/shuffle-shared v0.5.93 k8s.io/api v0.28.1 k8s.io/apimachinery v0.28.1 k8s.io/client-go v0.28.1 diff --git a/functions/onprem/orborus/go.sum b/functions/onprem/orborus/go.sum index 82ba3db3..713c5ad1 100644 --- a/functions/onprem/orborus/go.sum +++ b/functions/onprem/orborus/go.sum @@ -361,6 +361,8 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shuffle/shuffle-shared v0.5.68 h1:2ok4SsLojHvFIQMfe2upm3CLEvSbUPMJPIBaBZO1bL8= github.com/shuffle/shuffle-shared v0.5.68/go.mod h1:oIZkx93Z7EvtiTXty7xO+ax63Fjz8MQvjXMxDN0Qws0= +github.com/shuffle/shuffle-shared v0.5.93 h1:fZf9s2cEgDoyYXXvPYVeNh8UysuSlywQkc54IXkNL0k= +github.com/shuffle/shuffle-shared v0.5.93/go.mod h1:Lg6/+qjQlWzNKwj4/4ATpvScyP2JQGLkTPlNlRM6RJk= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 2d761857..25412d05 100755 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -1065,26 +1065,10 @@ func getOrborusStats(ctx context.Context) shuffle.OrborusStats { Timestamp: time.Now().Unix(), } - // FIXME: Returning for now due to this causing network congestion - // and database fillup. The backend api also has it disabled. - return newStats - - // Disable orborus stats - if os.Getenv("SHUFFLE_STATS_DISABLED") == "true" { - return newStats - } - - - if swarmConfig == "run" || swarmConfig == "swarm" { + if (swarmConfig == "run" || swarmConfig == "swarm") && strings.Contains(newWorkerImage, "scale") { newStats.Swarm = true } - - // Run this 1/10 times - //if rand.Intn(10) != 1 { - // return newStats - //} - newStats.PollTime = sleepTime newStats.MaxQueue = maxConcurrency newStats.Queue = executionCount @@ -1094,6 +1078,15 @@ func getOrborusStats(ctx context.Context) shuffle.OrborusStats { return newStats } + // Disable orborus stats + if os.Getenv("SHUFFLE_STATS_DISABLED") == "true" { + return newStats + } + + // FIXME: Returning for now due to this causing network congestion + // and database fillup. The backend api also has it disabled. + return newStats + // Use the docker API to get the CPU usage of the docker engine machine pers, err := dockercli.Info(ctx) if err != nil {