Merge branch '2.0.0' of github.com:Shuffle/Shuffle into 2.0.0
This commit is contained in:
@@ -11,11 +11,13 @@ Shuffle Automation
|
||||
|
||||
[Shuffle](https://shuffler.io) is an automation platform for and by the community, focusing on accessibility for anyone to automate. Security operations is complex, but it doesn't have to be.
|
||||
|
||||
[ Get training ](https://shuffler.io/training)
|
||||
[_Key Features_](https://shuffler.io/docs/features) —
|
||||
[_Community & Support_](https://discord.gg/B2CBzUm) —
|
||||
[_Documentation_](https://shuffler.io/docs) —
|
||||
[_Getting Started_](https://shuffler.io/docs/getting_started) —
|
||||
[_Development_](https://github.com/shuffle/Shuffle/blob/master/.github/CONTRIBUTING.md)
|
||||
[ Set up a demo call ](https://shuffler.io/contact)
|
||||
|
||||
Follow us on Twitter at [@shuffleio](https://twitter.com/shuffleio).
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ def md5_base64(a):
|
||||
foundhash = hashlib.md5(a.encode('utf-8')).hexdigest()
|
||||
return base64.b64encode(foundhash.encode('utf-8'))
|
||||
|
||||
|
||||
@shuffle_filters.register
|
||||
def base64_encode(a):
|
||||
a = str(a)
|
||||
@@ -107,6 +108,17 @@ def base64_encode(a):
|
||||
except:
|
||||
return base64.b64encode(a).decode()
|
||||
|
||||
@shuffle_filters.register
|
||||
def random_element(a):
|
||||
# Choose a random item from an array
|
||||
a = list(a)
|
||||
|
||||
if len(a) == 0:
|
||||
return ""
|
||||
|
||||
return random.choice(a)
|
||||
|
||||
|
||||
@shuffle_filters.register
|
||||
def base64_decode(a):
|
||||
a = str(a)
|
||||
|
||||
@@ -20,15 +20,15 @@ require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/shuffle/shuffle-shared v0.6.40
|
||||
github.com/shuffle/shuffle-shared v0.6.50
|
||||
golang.org/x/crypto v0.22.0
|
||||
google.golang.org/api v0.176.1
|
||||
google.golang.org/grpc v1.63.2
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.30.0
|
||||
k8s.io/apimachinery v0.30.0
|
||||
k8s.io/client-go v0.30.0
|
||||
k8s.io/api v0.30.2
|
||||
k8s.io/apimachinery v0.30.2
|
||||
k8s.io/client-go v0.30.2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -58,7 +58,7 @@ require (
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/frikky/schemaless v0.0.11 // indirect
|
||||
github.com/frikky/schemaless v0.0.13 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -100,6 +100,8 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sashabaranov/go-openai v1.19.2 // indirect
|
||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||
|
||||
@@ -160,6 +160,8 @@ github.com/frikky/schemaless v0.0.9 h1:RzNLPkJq5c4nlm5iLiTndFcbeQxdMGJIj266wSGt2
|
||||
github.com/frikky/schemaless v0.0.9/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ=
|
||||
github.com/frikky/schemaless v0.0.11 h1:c4r6CJX30XI+SoJdT9RlUd9qYSQlx6hvwGRtsypu+uM=
|
||||
github.com/frikky/schemaless v0.0.11/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ=
|
||||
github.com/frikky/schemaless v0.0.13 h1:ARiN9V7wr2VZXAr9JK5wvTbyPgpGrgeiL1VhR5MlgaQ=
|
||||
github.com/frikky/schemaless v0.0.13/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsouza/go-dockerclient v1.11.0 h1:4ZAk6W7rPAtPXm7198EFqA5S68rwnNQORxlOA5OurCA=
|
||||
@@ -303,6 +305,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
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/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -427,6 +431,10 @@ github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6
|
||||
github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
|
||||
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
@@ -443,6 +451,12 @@ github.com/shuffle/shuffle-shared v0.6.27 h1:q4qZD6bGZFIvZ5Y10unGr3N3rZ7OryWyvva
|
||||
github.com/shuffle/shuffle-shared v0.6.27/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4=
|
||||
github.com/shuffle/shuffle-shared v0.6.31 h1:MK1SW1pwjIP7hznq+mMlTPM1R3LIOfi1/bUL5xFSg/8=
|
||||
github.com/shuffle/shuffle-shared v0.6.31/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4=
|
||||
github.com/shuffle/shuffle-shared v0.6.46 h1:v/IXc+4V8DCWflGKGgaI37uuGnsylMjqKRylwNoXVrA=
|
||||
github.com/shuffle/shuffle-shared v0.6.46/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4=
|
||||
github.com/shuffle/shuffle-shared v0.6.47 h1:EOalfIIBX97uGkgRRPsdUApts6yCCcXf1iSphm6UoE0=
|
||||
github.com/shuffle/shuffle-shared v0.6.47/go.mod h1:XVIcR2/GyIk+6qmlpOG3NvIao6kCOma36EdMym4AeSY=
|
||||
github.com/shuffle/shuffle-shared v0.6.50 h1:MBeGAiBNkw9Eg+3YTJIlBOuskWntGvT0uefFUYOBhbY=
|
||||
github.com/shuffle/shuffle-shared v0.6.50/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
@@ -977,10 +991,16 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA=
|
||||
k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE=
|
||||
k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI=
|
||||
k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI=
|
||||
k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA=
|
||||
k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
|
||||
k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg=
|
||||
k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
|
||||
k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ=
|
||||
k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY=
|
||||
k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50=
|
||||
k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs=
|
||||
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
|
||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
|
||||
@@ -3616,7 +3616,7 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error {
|
||||
}
|
||||
|
||||
if org.SyncConfig.WorkflowBackup {
|
||||
workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser)
|
||||
workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser, 250, "")
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed getting backup workflows for org %s: %s", org.Id, err)
|
||||
} else {
|
||||
|
||||
@@ -1076,7 +1076,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
|
||||
// return workflowExecution, fmt.Sprintf("%s", err), nil
|
||||
} else {
|
||||
log.Printf("[ERROR] Failed in prepareExecution: '%s'", err)
|
||||
return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed starting workflow: %s", err), err
|
||||
return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed running: %s", err), err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3382,6 +3382,8 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
debugUrl := fmt.Sprintf("/workflows/%s?execution_id=%s", workflowExecution.Workflow.ID, workflowExecution.ExecutionId)
|
||||
resp.Header().Add("X-Debug-Url", debugUrl)
|
||||
|
||||
workflowExecution.Priority = 11
|
||||
environments, err := shuffle.GetEnvironments(ctx, user.ActiveOrg.Id)
|
||||
|
||||
@@ -232,6 +232,11 @@ const AppGrid = (props) => {
|
||||
removeQuery("q");
|
||||
refine(event.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
@@ -994,6 +999,11 @@ const AppGrid = (props) => {
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
|
||||
@@ -24,9 +24,12 @@ import {
|
||||
Chip,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
LinearProgress
|
||||
} from "@mui/material";
|
||||
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useNavigate, Link, json } from "react-router-dom";
|
||||
import { Autocomplete } from "@mui/material";
|
||||
import { toast } from "react-toastify"
|
||||
|
||||
@@ -35,12 +38,15 @@ import {
|
||||
ContentCopy as ContentCopyIcon,
|
||||
Draw as DrawIcon,
|
||||
Close as CloseIcon,
|
||||
Delete,
|
||||
RestaurantRounded,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
//import { useAlert
|
||||
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
|
||||
import BillingStats from "../components/BillingStats.jsx";
|
||||
import { handlePayasyougo } from "../views/HandlePaymentNew.jsx"
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
const Billing = (props) => {
|
||||
const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, clickedFromOrgTab } = props;
|
||||
@@ -58,6 +64,56 @@ const Billing = (props) => {
|
||||
const [dealValue, setDealValue] = React.useState("");
|
||||
const [dealDiscount, setDealDiscount] = React.useState("");
|
||||
const [dealerror, setDealerror] = React.useState("");
|
||||
const [openChangeEmailBox, setOpenChangeEmailBox] = useState(false);
|
||||
const [isMouseOverOnChangeEmail, setIsMouseOverOnChangeEmail] = useState(false);
|
||||
const [currentAppRunsInPercentage, setCurrentAppRunsInPercentage] = useState(0);
|
||||
const [currentAppRunsInNumber, setCurrentAppRunsInNumber] = useState(0);
|
||||
const [alertThresholds, setAlertThresholds] = useState(selectedOrganization.Billing !== undefined && selectedOrganization.Billing.AlertThreshold !== undefined && selectedOrganization.Billing.AlertThreshold !== null ? selectedOrganization.Billing.AlertThreshold : [{ percentage: '', count: '', Email_send: false }]);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) {
|
||||
const percentage = (userdata.app_execution_usage / userdata.app_execution_limit) * 100;
|
||||
setCurrentAppRunsInPercentage(Math.round(percentage));
|
||||
setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage);
|
||||
}
|
||||
}, [userdata]);
|
||||
|
||||
|
||||
const [BillingEmail, setBillingEmail] = useState(selectedOrganization.Billing !== undefined && selectedOrganization.Billing.Email !== undefined && selectedOrganization.Billing.Email != null && selectedOrganization.Billing.Email.length > 0 ? selectedOrganization.Billing.Email : selectedOrganization.org);
|
||||
|
||||
useState(() => {
|
||||
// Set the billing email
|
||||
setBillingEmail(
|
||||
selectedOrganization.Billing !== undefined &&
|
||||
selectedOrganization.Billing.Email !== undefined &&
|
||||
selectedOrganization.Billing.Email.length > 0
|
||||
? selectedOrganization.Billing.Email
|
||||
: selectedOrganization.org
|
||||
);
|
||||
|
||||
// Set and sort the alert thresholds
|
||||
const alertThresholds = selectedOrganization.Billing !== undefined &&
|
||||
selectedOrganization.Billing.AlertThreshold !== undefined &&
|
||||
selectedOrganization.Billing.AlertThreshold !== null
|
||||
? selectedOrganization.Billing.AlertThreshold
|
||||
: [{ percentage: '', count: '', Email_send: false }];
|
||||
|
||||
const sortedAlertThresholds = alertThresholds.sort((a, b) => {
|
||||
const countA = parseFloat(a.count);
|
||||
const countB = parseFloat(b.count);
|
||||
if (isNaN(countA)) return 1;
|
||||
if (isNaN(countB)) return -1;
|
||||
|
||||
return countA - countB;
|
||||
});
|
||||
|
||||
setAlertThresholds(sortedAlertThresholds);
|
||||
|
||||
const findCurrentIndex = sortedAlertThresholds.some(threshold => threshold.Email_send === false);
|
||||
setCurrentIndex(findCurrentIndex ? sortedAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1);
|
||||
|
||||
}, [selectedOrganization]);
|
||||
|
||||
const stripe = typeof window === 'undefined' || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : ""
|
||||
const products = [
|
||||
@@ -238,6 +294,7 @@ const Billing = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const sendSignatureRequest = (subscription) => {
|
||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`;
|
||||
|
||||
@@ -275,6 +332,7 @@ const Billing = (props) => {
|
||||
const [signatureOpen, setSignatureOpen] = React.useState(false);
|
||||
const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed)
|
||||
const [hovered, setHovered] = React.useState(false)
|
||||
const [newBillingEmail, setNewBillingEmail] = useState('');
|
||||
|
||||
var top_text = "Base Cloud Access"
|
||||
if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) {
|
||||
@@ -327,6 +385,78 @@ const Billing = (props) => {
|
||||
newPaperstyle.backgroundColor = theme.palette.surfaceColor
|
||||
}
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpenChangeEmailBox(true);
|
||||
};
|
||||
|
||||
const handleCloseChangeEmailBox = () => {
|
||||
setOpenChangeEmailBox(false);
|
||||
};
|
||||
|
||||
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return;
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
const HandleChangeBillingEmail = (orgId) => {
|
||||
const email = newBillingEmail;
|
||||
const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
|
||||
console.log("Pattern matches: ", emailPattern.test(email));
|
||||
if (!emailPattern.test(email)) {
|
||||
toast("Please enter a valid email address");
|
||||
return;
|
||||
} else {
|
||||
setNewBillingEmail(email);
|
||||
}
|
||||
|
||||
toast("Updating billing email. Please Wait")
|
||||
|
||||
const data = {
|
||||
org_id: orgId,
|
||||
email: newBillingEmail,
|
||||
billing: {
|
||||
email: newBillingEmail,
|
||||
},
|
||||
};
|
||||
|
||||
const url = `${globalUrl}/api/v1/orgs/${orgId}/billing`;
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Bad status code in get org:", response.status);
|
||||
}
|
||||
return response.json();
|
||||
}).then((responseJson) => {
|
||||
console.log("Got org:", responseJson);
|
||||
if (responseJson.success === true) {
|
||||
toast.success("Successfully updated billing email");
|
||||
setBillingEmail(newBillingEmail);
|
||||
setOpenChangeEmailBox(false);
|
||||
} else {
|
||||
toast.error("Failed to update billing email. Please try again.");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error getting org:", error);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper
|
||||
style={newPaperstyle}
|
||||
@@ -584,10 +714,85 @@ const Billing = (props) => {
|
||||
userdata.has_card_available === true ?
|
||||
"While you have a card attached to your account, Shuffle will no longer prevent workflows from running. Billing will occur at the start of each month."
|
||||
:
|
||||
isCloud ?
|
||||
`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.`
|
||||
:
|
||||
`You are not subscribed to any plan and are using the free, open source plan. This plan has no enforced limits, but scale issues may occur due to CPU congestion.`
|
||||
}
|
||||
</Typography>
|
||||
Billing email: {selectedOrganization.org}
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography variant="body2" style={{}}>
|
||||
Billing email: {BillingEmail}
|
||||
</Typography>
|
||||
{userdata.has_card_available === true && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleClickOpen}
|
||||
onMouseOver={() => setIsMouseOverOnChangeEmail(true)}
|
||||
onMouseOut={() => setIsMouseOverOnChangeEmail(false)}
|
||||
style={{
|
||||
width: 'auto',
|
||||
height: 20,
|
||||
textTransform: 'none',
|
||||
marginLeft: 'auto',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#f86743',
|
||||
boxShadow: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.3s, color 0.3s'
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
)}
|
||||
<Dialog
|
||||
open={openChangeEmailBox}
|
||||
onClose={handleCloseChangeEmailBox}
|
||||
PaperProps={{
|
||||
style: {
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '500px',
|
||||
width: '100%'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>Change Billing Email</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText style={{ marginBottom: '20px' }}>
|
||||
Enter the new billing email address.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="email"
|
||||
label="New Billing Email"
|
||||
type="email"
|
||||
fullWidth
|
||||
value={newBillingEmail}
|
||||
onKeyPress={(event) => { if (event.key === 'Enter') HandleChangeBillingEmail(selectedOrganization.id) }}
|
||||
onChange={(e) => setNewBillingEmail(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={handleCloseChangeEmailBox}
|
||||
color="primary"
|
||||
style={{ textTransform: 'none', marginRight: '10px' }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
style={{ textTransform: 'none' }}
|
||||
onClick={(event) => HandleChangeBillingEmail(selectedOrganization.id)}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
{/*isCloud ?
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -617,7 +822,7 @@ const Billing = (props) => {
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
handlePayasyougo(userdata)
|
||||
handlePayasyougo(userdata, selectedOrganization, BillingEmail)
|
||||
//navigate("/pricing?tab=cloud&highlight=true")
|
||||
} else {
|
||||
//window.open("https://shuffler.io/pricing?tab=onprem&highlight=true", "_blank")
|
||||
@@ -967,6 +1172,130 @@ const Billing = (props) => {
|
||||
toast("Failed adding deal reg: ", error);
|
||||
});
|
||||
};
|
||||
const addAlertThreshold = () => {
|
||||
setAlertThresholds([...alertThresholds, { percentage: '', count: '', Email_send: false }]);
|
||||
};
|
||||
|
||||
const updateAlertThreshold = (index, field, value) => {
|
||||
|
||||
if (field === 'percentage') {
|
||||
if (value > 100 || value < 0) {
|
||||
value = 0
|
||||
toast("The percentage value should be between 0 and 100")
|
||||
}
|
||||
} else if (field === 'count') {
|
||||
if (value < 0 || value >= userdata.app_execution_limit) {
|
||||
value = 0
|
||||
toast("The count value should be greater than 0 and less than the total app execution limit")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const totalValue = userdata.app_execution_limit;
|
||||
const newAlertThresholds = alertThresholds.map((threshold, i) => {
|
||||
if (i === index) {
|
||||
const newValue = parseFloat(value);
|
||||
if (field === 'percentage') {
|
||||
const newCount = (newValue / 100) * totalValue;
|
||||
return {
|
||||
...threshold,
|
||||
percentage: isNaN(newValue) ? '' : Math.round(newValue),
|
||||
count: isNaN(newCount) ? '' : Math.round(newCount),
|
||||
Email_send: false
|
||||
};
|
||||
} else if (field === 'count') {
|
||||
const newPercentage = (newValue / totalValue) * 100;
|
||||
return {
|
||||
...threshold,
|
||||
count: newValue,
|
||||
percentage: isNaN(newPercentage) ? '' : Math.round(newPercentage),
|
||||
Email_send: false
|
||||
};
|
||||
}
|
||||
}
|
||||
return threshold;
|
||||
});
|
||||
setAlertThresholds(newAlertThresholds);
|
||||
};
|
||||
|
||||
|
||||
const handleDeleteAlertThreshold = (index) => {
|
||||
const newAlertThresholds = alertThresholds.filter((_, i) => i !== index);
|
||||
setAlertThresholds(newAlertThresholds);
|
||||
|
||||
// Update currentIndex based on remaining elements
|
||||
const findCurrentIndex = newAlertThresholds.some(threshold => threshold.Email_send === false);
|
||||
setCurrentIndex(findCurrentIndex ? newAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1);
|
||||
};
|
||||
|
||||
const HandleEditOrgForAlertThreshold = (orgId) => {
|
||||
|
||||
// Use the `some` method to check for invalid counts
|
||||
const invalidCount = alertThresholds.some((threshold) => {
|
||||
if (threshold.count === '') {
|
||||
toast("Please enter a valid Count or Percentage value");
|
||||
return true; // Stop checking further and return true if invalid
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// If any invalid count is found, return early
|
||||
if (invalidCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast("Updating Email Alert Threshold. Please wait...");
|
||||
|
||||
const data = {
|
||||
org_id: orgId,
|
||||
billing: {
|
||||
email: BillingEmail,
|
||||
AlertThreshold: alertThresholds.map(threshold => ({
|
||||
...threshold,
|
||||
percentage: parseInt(threshold.percentage, 10),
|
||||
count: parseInt(threshold.count, 10),
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
const url = `${globalUrl}/api/v1/orgs/${orgId}`;
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Bad status code in get org:", response.status);
|
||||
}
|
||||
return response.json();
|
||||
}).then((responseJson) => {
|
||||
console.log("Got org:", responseJson);
|
||||
if (responseJson.success === true) {
|
||||
toast.success("Successfully updated Email Alert Thresholds");
|
||||
const findCurrentIndex = alertThresholds.some(threshold => threshold.Email_send === false);
|
||||
setCurrentIndex(findCurrentIndex ? alertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1);
|
||||
} else {
|
||||
toast.error("Failed to update Email Alert Thresholds. Please try again.");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error getting org:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const getSafeValue = (value) => {
|
||||
|
||||
if (value === undefined || value === null || isNaN(value)) {
|
||||
return 0;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const isChildOrg = userdata.active_org.creator_org !== "" && userdata.active_org.creator_org !== undefined && userdata.active_org.creator_org !== null
|
||||
return (
|
||||
@@ -1009,8 +1338,6 @@ const Billing = (props) => {
|
||||
<a href={"https://github.com/Shuffle/Shuffle-docs/tree/master/handbook/Sales"} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#f85a3e" }}>
|
||||
Sales Process
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
:
|
||||
null
|
||||
@@ -1333,9 +1660,144 @@ const Billing = (props) => {
|
||||
|
||||
</div>
|
||||
) : null*/}
|
||||
<div style={{ marginTop: 100, }}>
|
||||
<div style={{ marginTop: 50, marginLeft: 10 }}>
|
||||
<Typography
|
||||
style={{ marginTop: 40, marginLeft: 10, marginBottom: 5 }}
|
||||
style={{ marginTop: 40, marginBottom: 5 }}
|
||||
variant="h4"
|
||||
>
|
||||
Manage Billing
|
||||
</Typography>
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10 }}>
|
||||
Manage your billing and licensing information below. When you reach the certain thresholds of your subscription limit, you will be notified by email.
|
||||
</Typography>
|
||||
<Typography variant="body1">Current Usage:</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={currentAppRunsInPercentage}
|
||||
style={{
|
||||
width: "50%",
|
||||
height: 15,
|
||||
borderRadius: 10,
|
||||
margin: '10px 0',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
You have used <strong>{currentAppRunsInPercentage}%</strong> of total app execution limit or <strong>{userdata.app_execution_usage}</strong> app runs out of <strong>{userdata.app_execution_limit}</strong> app runs.
|
||||
</Typography>
|
||||
|
||||
<div>
|
||||
<Typography variant="body1" style={{ marginTop: 20 }}>
|
||||
Set email alert thresholds for app runs
|
||||
</Typography>
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 10 }}>
|
||||
You will be notified by email when you reach the
|
||||
{currentIndex !== -1
|
||||
? " " + getSafeValue(alertThresholds[currentIndex].percentage) + '%' + " "
|
||||
: " " + '0%' + " "
|
||||
}
|
||||
of your total app execution limit or
|
||||
{currentIndex !== -1
|
||||
? " " + getSafeValue(alertThresholds[currentIndex].count) + " "
|
||||
: " " + 0 + " "}
|
||||
app runs.
|
||||
</Typography>
|
||||
|
||||
<div style={{ marginTop: 15 }}>
|
||||
{alertThresholds.map((threshold, index) => (
|
||||
<div key={index} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<TextField
|
||||
style={{
|
||||
marginTop: 10,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
width: 250,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: 50,
|
||||
color: 'white',
|
||||
},
|
||||
endAdornment: '%',
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth
|
||||
label="Alert threshold (%)"
|
||||
type="number"
|
||||
value={threshold.percentage}
|
||||
onChange={(e) => updateAlertThreshold(index, 'percentage', e.target.value)}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
inputProps={{
|
||||
max: 100,
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
style={{
|
||||
marginTop: 10,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
width: 250,
|
||||
marginLeft: 15,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: 50,
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth
|
||||
label="Alert threshold (count)"
|
||||
type="number"
|
||||
value={threshold.count}
|
||||
onChange={(e) => updateAlertThreshold(index, 'count', e.target.value)}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
/>
|
||||
{
|
||||
alertThresholds.length > 1 &&
|
||||
(
|
||||
<Button
|
||||
disableRipple
|
||||
disableElevation
|
||||
sx={{
|
||||
padding: 0,
|
||||
color: 'red',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
|
||||
onClick={() => { handleDeleteAlertThreshold(index) }}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{ marginTop: 15, textTransform: 'none' }}
|
||||
onClick={addAlertThreshold}
|
||||
>
|
||||
Add Threshold
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{ marginTop: 15, textTransform: 'none', marginLeft: 20 }}
|
||||
onClick={() => { HandleEditOrgForAlertThreshold(selectedOrganization.id) }}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
<div style={{ marginTop: 40, display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography
|
||||
style={{ marginTop: 10, marginLeft: 10, marginBottom: 5 }}
|
||||
variant="h4"
|
||||
>
|
||||
Utilization & Stats
|
||||
|
||||
@@ -134,6 +134,9 @@ const AppStats = (defaultprops) => {
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
}).catch((error) => {
|
||||
console.log("Error getting workflow stats: " + error);
|
||||
return workflow
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
|
||||
@@ -428,7 +428,6 @@ const ConfigureWorkflow = (props) => {
|
||||
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()
|
||||
const foundindex = newactions.findIndex(action => action.app_name.toLowerCase() === findapp)
|
||||
|
||||
@@ -449,9 +448,6 @@ const ConfigureWorkflow = (props) => {
|
||||
|
||||
newactions[foundindex].show_steps = true
|
||||
|
||||
console.log("CHANGED ACTION: ", newactions[foundindex])
|
||||
//console.log("Index: ", newactions[foundindex])
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -1321,7 +1317,6 @@ const ConfigureWorkflow = (props) => {
|
||||
}
|
||||
|
||||
if (step.type === "authenticate") {
|
||||
console.log("AUTH STEP: ", step)
|
||||
if (data.must_authenticate === true ) {
|
||||
filled = false
|
||||
} else {
|
||||
|
||||
@@ -136,6 +136,11 @@ const CreatorGrid = props => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
@@ -147,6 +152,7 @@ const CreatorGrid = props => {
|
||||
flexWrap: "wrap",
|
||||
alignContent: "space-between",
|
||||
marginTop: 5,
|
||||
padding:"0px 180px",
|
||||
}
|
||||
|
||||
const Hits = ({ hits }) => {
|
||||
|
||||
@@ -75,7 +75,12 @@ const DiscordChat = props => {
|
||||
fullWidth
|
||||
value={currentRefinement}
|
||||
onChange={(event) => refine(event.currentTarget.value)}
|
||||
placeholder="Search Discord Chats"
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
placeholder="Search Discord Chats..."
|
||||
style={{ backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%", }}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -143,7 +148,7 @@ const DiscordChat = props => {
|
||||
const CustomHits = connectHits(Hits);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", textAlign:"center", position: "relative", height: "100%", }}>
|
||||
<div style={{textAlign:"center", position: "relative", height: "100%", padding:"0px 240px" }}>
|
||||
<InstantSearch searchClient={searchClient} indexName="discord_chat">
|
||||
<div style={{ maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 5, }}>
|
||||
<CustomSearchBox />
|
||||
|
||||
@@ -124,6 +124,11 @@ const DocsGrid = props => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
@@ -274,7 +279,7 @@ const DocsGrid = props => {
|
||||
</Button>
|
||||
</div>
|
||||
*/}
|
||||
<div style={{width: "100%", position: "relative", height: "100%",}}>
|
||||
<div style={{width: "100%", position: "relative", height: "100%", padding: "0px 180px"}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="documentation">
|
||||
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
|
||||
<CustomSearchBox />
|
||||
|
||||
@@ -78,7 +78,6 @@ 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'))
|
||||
|
||||
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();
|
||||
@@ -235,7 +234,7 @@ const EditWorkflow = (props) => {
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<FormControl>
|
||||
<div style={{width: 600, position: "fixed", left: 0, bottom: 0, zIndex: 1002, backgroundColor: "rgba(53,53,53,1)", height: 75, paddingTop: 20, paddingLeft: 75, }}>
|
||||
<div style={{borderTop: "1px solid rgba(255,255,255,0.5)", width: 600, position: "fixed", left: 0, bottom: 0, zIndex: 1002, backgroundColor: "rgba(53,53,53,1)", height: 75, paddingTop: 20, paddingLeft: 75, }}>
|
||||
{/*
|
||||
<Button
|
||||
style={{}}
|
||||
@@ -315,7 +314,7 @@ const EditWorkflow = (props) => {
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{submitLoading ? <CircularProgress color="secondary" /> : "Done"}
|
||||
{submitLoading ? <CircularProgress color="secondary" /> : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -339,26 +338,6 @@ const EditWorkflow = (props) => {
|
||||
autoFocus
|
||||
fullWidth
|
||||
/>
|
||||
<div style={{display: "flex", }}>
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
setDescription(event.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
maxRows={4}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.description}
|
||||
placeholder="Description"
|
||||
multiline
|
||||
label="Description"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div style={{display: "flex", marginTop: 10, }}>
|
||||
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
|
||||
<FormControl style={{flex: 1, marginRight: 5,}}>
|
||||
@@ -426,27 +405,56 @@ const EditWorkflow = (props) => {
|
||||
fullWidth
|
||||
value={newWorkflowTags}
|
||||
onChange={(chip) => {
|
||||
console.log("Chip: ", chip)
|
||||
//newWorkflowTags.push(chip);
|
||||
setNewWorkflowTags(chip);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (event.target.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (newWorkflowTags.includes(event.target.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
newWorkflowTags.push(event.target.value)
|
||||
setNewWorkflowTags(newWorkflowTags)
|
||||
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
onAdd={(chip) => {
|
||||
newWorkflowTags.push(chip);
|
||||
setNewWorkflowTags(newWorkflowTags);
|
||||
newWorkflowTags.push(chip)
|
||||
setNewWorkflowTags(newWorkflowTags)
|
||||
}}
|
||||
onDelete={(chip, index) => {
|
||||
console.log("Deleting: ", chip, index)
|
||||
newWorkflowTags.splice(index, 1);
|
||||
setNewWorkflowTags(newWorkflowTags);
|
||||
setUpdate(Math.random());
|
||||
newWorkflowTags.splice(index, 1)
|
||||
setNewWorkflowTags(newWorkflowTags)
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showMoreClicked === true ?
|
||||
<span style={{marginTop: 25, }}>
|
||||
|
||||
|
||||
<div style={{marginTop: 50, }}>
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
setDescription(event.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
multiLine
|
||||
rows={3}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.description}
|
||||
placeholder="Description"
|
||||
multiline
|
||||
label="Description"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<div style={{display: "flex"}}>
|
||||
<FormControl style={{marginTop: 15, }}>
|
||||
@@ -565,12 +573,22 @@ const EditWorkflow = (props) => {
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Typography variant="body2" style={{marginTop: 50, }}>
|
||||
MSSP Suborg Distribution (beta - contact support@shuffler.io)
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }} />
|
||||
|
||||
<Typography variant="body1" style={{marginTop: 50, }}>
|
||||
MSSP Suborg Distribution (beta - contact support@shuffler.io for more info)
|
||||
</Typography>
|
||||
{userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ?
|
||||
userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ?
|
||||
userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ?
|
||||
<Typography variant="body2" style={{marginTop: 10, color: "rgba(255,255,255,0.7)"}}>
|
||||
Your organization does not have any suborgs yet. Please <a href="/admin?tab=suborgs" style={{textDecoration: "none", color: "#f86a3e"}} target="_blank">make one</a>, then try again.
|
||||
</Typography>
|
||||
:
|
||||
<Typography variant="body2" style={{marginTop: 10, color: "rgba(255,255,255,0.7)"}}>
|
||||
{innerWorkflow.parentorg_workflow !== undefined && innerWorkflow.parentorg_workflow !== null && innerWorkflow.parentorg_workflow.length > 0 ? <span>This workflow is distributed from <a href={`/workflows/${innerWorkflow.parentorg_workflow}`} style={{textDecoration: "none", color: "#f86a3e"}} target="_blank">your parent workflow</a> (you may not have access).</span> : null}
|
||||
<br />
|
||||
<br />
|
||||
You can only distribute to suborgs from a parent org.
|
||||
</Typography>
|
||||
:
|
||||
@@ -584,8 +602,6 @@ const EditWorkflow = (props) => {
|
||||
newvalue = newvalue.filter(value => value !== "none")
|
||||
}
|
||||
|
||||
console.log("NEWVALUE: ", newvalue)
|
||||
|
||||
if (newvalue.includes("none")) {
|
||||
newvalue = ["none"]
|
||||
} else if (newvalue.includes("all")) {
|
||||
@@ -664,12 +680,13 @@ const EditWorkflow = (props) => {
|
||||
</Link>
|
||||
}
|
||||
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }} />
|
||||
|
||||
<Typography variant="h6" style={{marginTop: 50, }}>
|
||||
Input fields
|
||||
</Typography>
|
||||
<Typography variant="body1" color="textSecondary" style={{marginBottom: 20, }}>
|
||||
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 <a href={`/workflows/${workflow.id}/run`} rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>workflow run page</a>.
|
||||
<Typography variant="body2" color="textSecondary" style={{marginBottom: 20, }}>
|
||||
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 <a href={`/workflows/${workflow.id}/run`} rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>workflow run page</a>. If chosen in the User Input node, these will be required fields.
|
||||
</Typography>
|
||||
|
||||
|
||||
@@ -775,18 +792,173 @@ const EditWorkflow = (props) => {
|
||||
>
|
||||
<AddIcon style={{}} />
|
||||
</Button>
|
||||
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }} />
|
||||
|
||||
<Typography variant="body1" style={{marginTop: 50, }}>
|
||||
Git Backup Repository
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary">
|
||||
Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. <b>The repository and branch must already have been initialized</b>. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' without images. Overrides the <a href="/admin?admin_tab=organization" style={{textDecoration: "none", color: "#f86a3e"}} target="_blank">default backup repository</a> your org has chosen. All fields must be filled in for the backup to work.
|
||||
<br />
|
||||
PS: This is a beta feature, and might not work as expected. Credentials are NOT encrypted.
|
||||
</Typography>
|
||||
<Grid container style={{ marginTop: 10, }} spacing={2}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Workflow Backup Repository</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={1}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="github/com/shuffle/workflowbackup "
|
||||
defaultValue={innerWorkflow.backup_config === undefined || innerWorkflow.backup_config.upload_repo === undefined || innerWorkflow.backup_config.upload_repo === null || innerWorkflow.backup_config.upload_repo === "" ? "" : innerWorkflow.backup_config.upload_repo}
|
||||
onChange={(e) => {
|
||||
//setUploadRepo(e.target.value);
|
||||
innerWorkflow.backup_config.upload_repo = e.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Branch</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={1}
|
||||
placeholder="The branch to use"
|
||||
defaultValue={innerWorkflow.backup_config === undefined || innerWorkflow.backup_config.upload_branch === undefined || innerWorkflow.backup_config.upload_branch === null || innerWorkflow.backup_config.upload_branch === "" ? "" : innerWorkflow.backup_config.upload_branch}
|
||||
onChange={(e) => {
|
||||
innerWorkflow.backup_config.upload_branch = e.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container style={{ }} spacing={2}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Username</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={1}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The username to use"
|
||||
defaultValue={innerWorkflow.backup_config === undefined || innerWorkflow.backup_config.upload_username === undefined || innerWorkflow.backup_config.upload_username === null || innerWorkflow.backup_config.upload_username === "" ? "" : innerWorkflow.backup_config.upload_username}
|
||||
onChange={(e) => {
|
||||
innerWorkflow.backup_config.upload_username = e.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Git token/password</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
}}
|
||||
fullWidth={true}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={1}
|
||||
placeholder="The API token to use"
|
||||
defaultValue={innerWorkflow.backup_config === undefined || innerWorkflow.backup_config.upload_token === undefined || innerWorkflow.backup_config.upload_token === null || innerWorkflow.backup_config.upload_token === "" ? "" : innerWorkflow.backup_config.upload_token}
|
||||
onChange={(e) => {
|
||||
innerWorkflow.backup_config.upload_token = e.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
type="password"
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
|
||||
<Tooltip color="primary" title={"Add more details"} placement="top">
|
||||
<IconButton
|
||||
style={{ color: "white", margin: "auto", textAlign: "center", width: 50, marginTop: 50, }}
|
||||
<Button
|
||||
style={{ margin: "auto", marginTop: 50, textAlign: "center", }}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
setShowMoreClicked(!showMoreClicked);
|
||||
}}
|
||||
>
|
||||
{showMoreClicked ? <ExpandLessIcon /> : <ExpandMoreIcon/>}
|
||||
</IconButton>
|
||||
{showMoreClicked ? "Collapse": "Expand"}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ const Files = (props) => {
|
||||
const { globalUrl, userdata, serverside, selectedOrganization, isCloud,isSelectedFiles } = props;
|
||||
|
||||
const [files, setFiles] = React.useState([]);
|
||||
const [selectedNamespace, setSelectedNamespace] = React.useState("default");
|
||||
const [selectedCategory, setSelectedCategory] = React.useState("default");
|
||||
const [openFileId, setOpenFileId] = React.useState(false);
|
||||
const [fileNamespaces, setFileNamespaces] = React.useState([]);
|
||||
const [fileCategories, setFileCategories] = React.useState([]);
|
||||
const [fileContent, setFileContent] = React.useState("");
|
||||
const [openEditor, setOpenEditor] = React.useState(false);
|
||||
const [renderTextBox, setRenderTextBox] = React.useState(false);
|
||||
@@ -57,6 +57,7 @@ const Files = (props) => {
|
||||
const [downloadUrl, setDownloadUrl] = React.useState("https://github.com/shuffle/standards")
|
||||
const [downloadBranch, setDownloadBranch] = React.useState("main");
|
||||
const [downloadFolder, setDownloadFolder] = React.useState("translation_standards");
|
||||
const [contentLoading, setContentLoading] = React.useState(false)
|
||||
|
||||
//const alert = useAlert();
|
||||
const allowedFileTypes = ["txt", "py", "yaml", "yml","json", "html", "js", "csv", "log", "eml", "msg", "md", "xml", "sh", "bat", "ps1", "psm1", "psd1", "ps1xml", "pssc", "psc1", "response"]
|
||||
@@ -67,8 +68,8 @@ const Files = (props) => {
|
||||
|
||||
console.log('do validate')
|
||||
console.log("new namespace name->",event.target.value);
|
||||
fileNamespaces.push(event.target.value);
|
||||
setSelectedNamespace(event.target.value);
|
||||
fileCategories.push(event.target.value);
|
||||
setSelectedCategory(event.target.value);
|
||||
setRenderTextBox(false);
|
||||
}
|
||||
|
||||
@@ -107,12 +108,13 @@ const Files = (props) => {
|
||||
|
||||
const getFiles = (namespace) => {
|
||||
var parsedurl = `${globalUrl}/api/v1/files`
|
||||
if (namespace === undefined || namespace === "default") {
|
||||
|
||||
if (namespace === undefined || namespace === null || namespace === "default") {
|
||||
|
||||
} else if (namespace !== undefined && namespace !== null && namespace !== "") {
|
||||
parsedurl = `${globalUrl}/api/v1/files/namespaces/${namespace}?ids=true`
|
||||
} else if (selectedNamespace !== undefined && selectedNamespace !== null && selectedNamespace !== "default" && selectedNamespace !== "") {
|
||||
parsedurl = `${globalUrl}/api/v1/files/namespaces/${selectedNamespace}?ids=true`
|
||||
} else if (selectedCategory !== undefined && selectedCategory !== null && selectedCategory !== "default" && selectedCategory !== "") {
|
||||
parsedurl = `${globalUrl}/api/v1/files/namespaces/${selectedCategory}?ids=true`
|
||||
}
|
||||
|
||||
fetch(parsedurl, {
|
||||
@@ -149,8 +151,10 @@ const Files = (props) => {
|
||||
setFiles([]);
|
||||
}
|
||||
|
||||
if (responseJson.namespaces !== undefined && responseJson.namespaces !== null && (fileNamespaces.length === 0 || responseJson.namespaces.length > fileNamespaces.length)) {
|
||||
setFileNamespaces(responseJson.namespaces);
|
||||
if (namespace === undefined || namespace === null || namespace === "default") {
|
||||
if (responseJson.namespaces !== undefined && responseJson.namespaces !== null && (fileCategories.length === 0 || responseJson.namespaces.length > fileCategories.length)) {
|
||||
setFileCategories(responseJson.namespaces)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -159,7 +163,21 @@ const Files = (props) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getFiles(selectedNamespace)
|
||||
getFiles("default")
|
||||
|
||||
setTimeout(() => {
|
||||
var category = selectedCategory
|
||||
if (window.location.search.includes("category=")) {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
category = urlParams.get("category")
|
||||
}
|
||||
|
||||
if (category !== undefined && category !== null && category.length > 0 && category !== "default") {
|
||||
setSelectedCategory(category)
|
||||
}
|
||||
|
||||
getFiles(category)
|
||||
}, 1000)
|
||||
}, []);
|
||||
|
||||
const importStandardsFromUrl = (url, folder) => {
|
||||
@@ -392,8 +410,9 @@ const Files = (props) => {
|
||||
) {
|
||||
toast("Failed to delete file: " + responseJson.reason);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
getFiles();
|
||||
getFiles(selectedCategory)
|
||||
}, 1500);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -402,6 +421,8 @@ const Files = (props) => {
|
||||
};
|
||||
|
||||
const readFileData = (file) => {
|
||||
setContentLoading(true)
|
||||
|
||||
fetch(globalUrl + "/api/v1/files/" + file.id + "/content", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -411,6 +432,7 @@ const Files = (props) => {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
setContentLoading(false)
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for file :O!");
|
||||
return "";
|
||||
@@ -428,12 +450,12 @@ const Files = (props) => {
|
||||
return respdata
|
||||
})
|
||||
.then((responseData) => {
|
||||
|
||||
setFileContent(responseData);
|
||||
//console.log("filecontent state ",fileContent);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
setContentLoading(false)
|
||||
toast(error.toString())
|
||||
});
|
||||
};
|
||||
|
||||
@@ -505,12 +527,12 @@ const Files = (props) => {
|
||||
};
|
||||
|
||||
if (
|
||||
selectedNamespace !== undefined &&
|
||||
selectedNamespace !== null &&
|
||||
selectedNamespace.length > 0 &&
|
||||
selectedNamespace !== "default"
|
||||
selectedCategory !== undefined &&
|
||||
selectedCategory !== null &&
|
||||
selectedCategory.length > 0 &&
|
||||
selectedCategory !== "default"
|
||||
) {
|
||||
data.namespace = selectedNamespace;
|
||||
data.namespace = selectedCategory;
|
||||
}
|
||||
|
||||
fetch(globalUrl + "/api/v1/files/create", {
|
||||
@@ -676,22 +698,23 @@ const Files = (props) => {
|
||||
//setFile(fileObject)
|
||||
//const files = event.target.files[0]
|
||||
uploadFiles(event.target.files);
|
||||
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{ marginLeft: 5, marginRight: 15, backgroundColor:isSelectedFiles?"#2F2F2F":null,borderRadius:isSelectedFiles?200:null, width:isSelectedFiles?81:null, height:isSelectedFiles?40:null, boxShadow: isSelectedFiles?'none':null, }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => getFiles()}
|
||||
onClick={() => getFiles(selectedCategory)}
|
||||
>
|
||||
<CachedIcon />
|
||||
</Button>
|
||||
|
||||
|
||||
|
||||
{fileNamespaces !== undefined &&
|
||||
fileNamespaces !== null &&
|
||||
fileNamespaces.length > 1 ? (
|
||||
{fileCategories !== undefined &&
|
||||
fileCategories !== null &&
|
||||
fileCategories.length > 1 ? (
|
||||
<FormControl style={{ minWidth: 150, maxWidth: 150 }}>
|
||||
<InputLabel id="input-namespace-label">File Category</InputLabel>
|
||||
<Select
|
||||
@@ -703,18 +726,26 @@ const Files = (props) => {
|
||||
maxWidth: 150,
|
||||
float: "right",
|
||||
}}
|
||||
value={selectedNamespace}
|
||||
value={selectedCategory}
|
||||
onChange={(event) => {
|
||||
setSelectedNamespace(event.target.value);
|
||||
setSelectedCategory(event.target.value)
|
||||
|
||||
if (event.target.value === "all" || event.target.value === "default") {
|
||||
getFiles()
|
||||
} else {
|
||||
getFiles(event.target.value)
|
||||
}
|
||||
|
||||
// Add it to the url as a query
|
||||
if (window.location.search.includes("category=")) {
|
||||
const newurl = window.location.href.replace(/category=[^&]+/, `category=${event.target.value}`)
|
||||
window.history.pushState({ path: newurl }, "", newurl)
|
||||
} else {
|
||||
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${event.target.value}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{fileNamespaces.map((data, index) => {
|
||||
{fileCategories.map((data, index) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
@@ -754,8 +785,8 @@ const Files = (props) => {
|
||||
>
|
||||
<AddIcon/>
|
||||
</Button>
|
||||
</Tooltip> }
|
||||
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
{renderTextBox && <TextField
|
||||
onKeyPress={(event)=>{
|
||||
@@ -783,6 +814,7 @@ const Files = (props) => {
|
||||
isFileEditor = {true}
|
||||
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
|
||||
runUpdateText = {runUpdateText}
|
||||
contentLoading = {contentLoading}
|
||||
/>
|
||||
{isSelectedFiles?null:
|
||||
<Divider
|
||||
@@ -795,15 +827,17 @@ const Files = (props) => {
|
||||
|
||||
<List style={{borderRadius: isSelectedFiles?8:null, border:isSelectedFiles?"1px solid #494949":null, marginTop:isSelectedFiles?24:null}}>
|
||||
<ListItem style={{width:isSelectedFiles?"100%":null, borderBottom:isSelectedFiles?"1px solid #494949":null}}>
|
||||
{/*
|
||||
<ListItemText
|
||||
primary="Updated"
|
||||
style={{ maxWidth: 185, minWidth: 185 }}
|
||||
/>
|
||||
*/}
|
||||
<ListItemText
|
||||
primary="Name"
|
||||
style={{
|
||||
maxWidth: 150,
|
||||
minWidth: 150,
|
||||
maxWidth: 250,
|
||||
minWidth: 250,
|
||||
overflow: "hidden",
|
||||
marginLeft: 10,
|
||||
}}
|
||||
@@ -814,7 +848,7 @@ const Files = (props) => {
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Md5"
|
||||
style={{ minWidth: isSelectedFiles?210:250, maxWidth: isSelectedFiles?210:250, overflow: "hidden" }}
|
||||
style={{ minWidth: 300, maxWidth: 300, overflow: "hidden" }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Status"
|
||||
@@ -832,7 +866,7 @@ const Files = (props) => {
|
||||
file.namespace = "default";
|
||||
}
|
||||
|
||||
if (file.namespace !== selectedNamespace) {
|
||||
if (file.namespace !== selectedCategory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -853,6 +887,7 @@ const Files = (props) => {
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/*
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: isSelectedFiles ? 170:225,
|
||||
@@ -861,10 +896,11 @@ const Files = (props) => {
|
||||
}}
|
||||
primary={new Date(file.updated_at * 1000).toISOString()}
|
||||
/>
|
||||
*/}
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 150,
|
||||
minWidth: 150,
|
||||
maxWidth: 250,
|
||||
minWidth: 250,
|
||||
overflow: "hidden",
|
||||
marginLeft: 10,
|
||||
}}
|
||||
@@ -928,8 +964,8 @@ const Files = (props) => {
|
||||
<ListItemText
|
||||
primary={file.md5_sum}
|
||||
style={{
|
||||
minWidth: isSelectedFiles?200:300,
|
||||
maxWidth: isSelectedFiles?200:300,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
marginLeft:isSelectedFiles? 15:null,
|
||||
overflow: isSelectedFiles?"auto":"hidden",
|
||||
}}
|
||||
@@ -1076,7 +1112,7 @@ const Files = (props) => {
|
||||
disabled={file.status !== "active"}
|
||||
style = {{padding: "6px"}}
|
||||
onClick={() => {
|
||||
deleteFile(file);
|
||||
deleteFile(file)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import theme from "../theme.jsx";
|
||||
import { BrowserView, MobileView } from "react-device-detect";
|
||||
@@ -84,6 +84,15 @@ const Header = (props) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const topbar = localStorage.getItem("topbar_closed")
|
||||
if (topbar === "true") {
|
||||
setShowTopbar(false)
|
||||
} else {
|
||||
setShowTopbar(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
setAnchorElAvatar(null);
|
||||
@@ -537,7 +546,7 @@ const Header = (props) => {
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Tooltip color="primary" title={"Join the Shuffle Automation Community"} placement="left">
|
||||
<Tooltip color="primary" title={"Shuffle is Open Source, and has a thriving Discord Automation Community"} placement="left">
|
||||
<IconButton
|
||||
color="primary"
|
||||
style={{}}
|
||||
@@ -545,11 +554,8 @@ const Header = (props) => {
|
||||
aria-haspopup="true"
|
||||
onClick={(event) => { }}
|
||||
>
|
||||
<img
|
||||
alt="Discord Community Join"
|
||||
src={"/images/social/discord.png"}
|
||||
style={{ height: 30, width: 30 }}
|
||||
/>
|
||||
<svg viewBox="0 0 256 199" width="256" height="199" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" style={{height: 30, width: 30, }}><path d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z" fill="#dadae1"/></svg>
|
||||
{/*#f865f2*/}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</a>
|
||||
@@ -1590,17 +1596,17 @@ const Header = (props) => {
|
||||
|
||||
navigate("/training")
|
||||
|
||||
//if (window.drift !== undefined) {
|
||||
// window.drift.api.startInteraction({ interactionId: 341911 })
|
||||
//} else {
|
||||
// console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift)
|
||||
//}
|
||||
}} style={{ cursor: "pointer", textDecoration: "none", color: "rgba(255,255,255,0.8)" }}>
|
||||
Public Training!
|
||||
</span>
|
||||
</u>
|
||||
</Typography>
|
||||
<IconButton color="secondary" style={{ position: "absolute", top: -3, right: 20, }} onClick={(event) => { setShowTopbar(false) }}>
|
||||
<IconButton color="secondary" style={{ position: "absolute", top: -3, right: 20, }} onClick={(event) => {
|
||||
setShowTopbar(false)
|
||||
|
||||
// Set storage that it's clicked
|
||||
localStorage.setItem("topbar_closed", "true")
|
||||
}}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -471,32 +471,26 @@ const AuthenticationOauth2 = (props) => {
|
||||
state += `%26refresh_uri%3d${authentication_url}`;
|
||||
}
|
||||
|
||||
// No prompt forcing
|
||||
//var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=login&scope=${resources}&state=${state}&access_type=offline`;
|
||||
// FIXME: Should this be =consent?
|
||||
var defaultPrompt = "login"
|
||||
if (prompt !== undefined && prompt !== null && prompt.length > 0) {
|
||||
defaultPrompt = prompt
|
||||
}
|
||||
|
||||
var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`;
|
||||
|
||||
if (admin_consent === true) {
|
||||
console.log("Running Oauth2 WITH admin consent")
|
||||
//url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=consent&scope=${resources}&state=${state}&access_type=offline`;
|
||||
url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=admin_consent&scope=${resources}&state=${state}&access_type=offline`;
|
||||
}
|
||||
|
||||
console.log("OAUTH2 URL: ", url)
|
||||
|
||||
// Force new consent
|
||||
//const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=consent&state=${state}&access_type=offline`;
|
||||
|
||||
// Admin consent
|
||||
//const url = `https://accounts.zoho.com/oauth/v2/auth?response_type=code&client_id=${client_id}&scope=AaaServer.profile.Read&redirect_uri=${redirectUri}&prompt=consent`
|
||||
|
||||
// &resource=https%3A%2F%2Fgraph.microsoft.com&
|
||||
|
||||
// FIXME: Awful, but works for prototyping
|
||||
// How can we get a callback properly realtime?
|
||||
// How can we properly try-catch without breaks on error?
|
||||
try {
|
||||
var newwin = window.open(url, "", "width=582,height=700");
|
||||
//console.log(newwin)
|
||||
@@ -997,7 +991,6 @@ const AuthenticationOauth2 = (props) => {
|
||||
color: "white",
|
||||
padding: 5,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
}}
|
||||
onChange={(e, value) => {
|
||||
//handleScopeChange(e)
|
||||
|
||||
@@ -55,6 +55,10 @@ const OrgHeaderexpanded = (props) => {
|
||||
globalUrl,
|
||||
isCloud,
|
||||
adminTab,
|
||||
|
||||
selectedStatus,
|
||||
setSelectedStatus,
|
||||
isEditOrgTab
|
||||
} = props;
|
||||
|
||||
const classes = useStyles();
|
||||
@@ -169,6 +173,11 @@ const OrgHeaderexpanded = (props) => {
|
||||
: selectedOrganization.sso_config.openid_token
|
||||
)
|
||||
|
||||
const [uploadRepo, setUploadRepo] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_repo === undefined || selectedOrganization.defaults.workflow_upload_repo.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_repo)
|
||||
const [uploadBranch, setUploadBranch] = React.useState(selectedOrganization.defaults === undefined ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch === undefined || selectedOrganization.defaults.workflow_upload_branch.length === 0 ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch)
|
||||
const [uploadUsername, setUploadUsername] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_username === undefined || selectedOrganization.defaults.workflow_upload_username.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_username)
|
||||
const [uploadToken, setUploadToken] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_token === undefined || selectedOrganization.defaults.workflow_upload_token.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_token)
|
||||
|
||||
const [workflows, setWorkflows] = React.useState([])
|
||||
const [workflow, setWorkflow] = React.useState({})
|
||||
|
||||
@@ -299,7 +308,6 @@ const OrgHeaderexpanded = (props) => {
|
||||
client_secret: openidClientSecret,
|
||||
openid_authorization: openidAuthorization,
|
||||
openid_token: openidToken,
|
||||
SSORequired: SSORequired
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -311,16 +319,7 @@ const OrgHeaderexpanded = (props) => {
|
||||
);
|
||||
|
||||
const toggleBetweenRequiredOrOptional = (event) => {
|
||||
if (ssoEntrypoint === "" && openidAuthorization === "" && openidToken === "") {
|
||||
if (!SSORequired) {
|
||||
toast.error("Please fill in fields for either OpenID connect or SSO before continuing. ")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
toast.info("Toggled SSO. Remember to save.")
|
||||
}
|
||||
|
||||
setSSORequired(event.target.checked)
|
||||
setSSORequired(event.target.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -512,16 +511,164 @@ const OrgHeaderexpanded = (props) => {
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
{/* {isCloud ? null : */}
|
||||
<Typography variant="h4" style={{ textAlign: "left", marginTop: 75, marginLeft: 20, }}>Single Signon - SAML/OpenID</Typography>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 20, marginLeft: 10, width: '100%', borderBottom: '1px solid #414347' }}>
|
||||
<Typography variant="body1" style={{ margin: '5px 0px 5px 10px' }}>Make SAML SSO or OpenID Authentication required/optional for your organization. Tip: Do not make it required until you have tested the URL directly.</Typography>
|
||||
|
||||
<Grid item xs={12} style={{ marginTop: 50, }}>
|
||||
<Typography variant="h4" style={{ textAlign: "left", }}>Workflow Backup Repository</Typography>
|
||||
<Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary">
|
||||
Decide where workflows are backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the root folder in the format 'status_workflowid.json'
|
||||
</Typography>
|
||||
<Grid container style={{ marginTop: 10, }} spacing={2}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Repository for workflow backup</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={1}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="Ex: github/com/shuffle/workflowbackup "
|
||||
value={uploadRepo}
|
||||
onChange={(e) => {
|
||||
setUploadRepo(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Branch</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={1}
|
||||
placeholder="The branch to use for backup of workflows"
|
||||
value={uploadBranch}
|
||||
onChange={(e) => {
|
||||
setUploadBranch(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container style={{ marginTop: 10, }} spacing={2}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Username</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={1}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The username to use for backup of workflows"
|
||||
value={uploadUsername}
|
||||
onChange={(e) => {
|
||||
setUploadUsername(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Git token/password</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={1}
|
||||
placeholder="The token to use for backup of workflows. PS: This will be stored in cleartext in the database for now."
|
||||
value={uploadToken}
|
||||
onChange={(e) => {
|
||||
setUploadToken(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
type="password"
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Typography variant="h4" style={{ marginLeft: 20, paddingTop: 100, borderTop: "1px solid rgba(255, 255, 255, 0.12)", width: "100%", marginTop: 50, }}>
|
||||
SSO Configuration
|
||||
</Typography>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginLeft: 10, width: '100%', }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{ margin: '5px 0px 5px 10px' }}>Make SAML SSO or OpenID Authentication Required or Optional for Your Organization.</Typography>
|
||||
<div>
|
||||
<Switch
|
||||
checked={SSORequired}
|
||||
onChange={(e) => {
|
||||
toggleBetweenRequiredOrOptional(e)
|
||||
}}
|
||||
onChange={toggleBetweenRequiredOrOptional}
|
||||
name="onOffSwitch"
|
||||
color="primary"
|
||||
title="Make SAML SSO or OpenID Authentication Required or Optional for Your Organization"
|
||||
@@ -529,10 +676,8 @@ const OrgHeaderexpanded = (props) => {
|
||||
{SSORequired ? 'Required' : 'Optional'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
<Grid item xs={12} style={{}}>
|
||||
<Typography variant="h6" style={{ textAlign: "left", }}>OpenID connect</Typography>
|
||||
<Typography variant="h6" style={{ textAlign: "center", }}>OpenID connect</Typography>
|
||||
<Grid container style={{ marginTop: 10, }}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
@@ -549,6 +694,11 @@ const OrgHeaderexpanded = (props) => {
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
@@ -583,6 +733,11 @@ const OrgHeaderexpanded = (props) => {
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
@@ -677,7 +832,7 @@ const OrgHeaderexpanded = (props) => {
|
||||
{/* } */}
|
||||
{/*isCloud ? null : */}
|
||||
<Grid item xs={12} style={{ marginTop: 50, }}>
|
||||
<Typography variant="h4" style={{ textAlign: "center", }}>SAML SSO (v1.1)</Typography>
|
||||
<Typography variant="h6" style={{ textAlign: "center", }}>SAML SSO (v1.1)</Typography>
|
||||
<Grid container style={{ marginTop: 20, }}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
@@ -694,6 +849,11 @@ const OrgHeaderexpanded = (props) => {
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
|
||||
+1509
-1449
File diff suppressed because it is too large
Load Diff
@@ -27,8 +27,9 @@ const Priorities = (props) => {
|
||||
|
||||
const [selectedWorkflow, setSelectedWorkflow] = React.useState("NO HIGHLIGHT");
|
||||
const [selectedExecutionId, setSelectedExecutionId] = React.useState("NO HIGHLIGHT");
|
||||
let navigate = useNavigate();
|
||||
const [highlightKMS, setHighlightKMS] = React.useState(false)
|
||||
|
||||
let navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
getFramework()
|
||||
|
||||
@@ -36,6 +37,12 @@ const Priorities = (props) => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const workflow = urlParams.get("workflow")
|
||||
const execution_id = urlParams.get("execution_id")
|
||||
const kms = urlParams.get("kms")
|
||||
|
||||
if (kms !== null && kms !== undefined && kms.length > 0 && kms === "true") {
|
||||
toast.info("KMS-related notifications are highlighted.")
|
||||
setHighlightKMS(true)
|
||||
}
|
||||
|
||||
if (execution_id !== null) {
|
||||
setSelectedExecutionId(execution_id)
|
||||
@@ -217,7 +224,16 @@ const Priorities = (props) => {
|
||||
var orgId = "";
|
||||
|
||||
|
||||
const highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow)
|
||||
var highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow)
|
||||
|
||||
if (!highlighted && highlightKMS) {
|
||||
if (data.title !== undefined && data.title !== null && data.title.toLowerCase().includes("kms")) {
|
||||
highlighted = true
|
||||
} else if (data.description !== undefined && data.description !== null && data.description.toLowerCase().includes("kms")) {
|
||||
highlighted = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (userdata.orgs !== undefined) {
|
||||
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
|
||||
|
||||
@@ -88,12 +88,12 @@ const SearchData = props => {
|
||||
|
||||
const textFieldRef = useRef(null);
|
||||
const keyPressHandler = (e) => {
|
||||
if (e.which === 13) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
// navigate(`/search?q=${currentRefinement}`, { state: value, replace: true });
|
||||
// setModalOpen(false);
|
||||
const trimmedValue = inputValue.trim();
|
||||
if (trimmedValue !== '') {
|
||||
e.preventDefault();
|
||||
navigate(`/search?q=${trimmedValue}`, { state: trimmedValue, replace: true });
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
@@ -107,10 +107,9 @@ const CodeEditor = (props) => {
|
||||
getParents,
|
||||
|
||||
fieldname,
|
||||
contentLoading,
|
||||
} = props
|
||||
|
||||
|
||||
|
||||
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
|
||||
|
||||
//const { setContainer } = useCodeMirror({
|
||||
@@ -627,8 +626,8 @@ const CodeEditor = (props) => {
|
||||
newMarkers.push({
|
||||
startRow: i,
|
||||
startCol: startCh,
|
||||
endRow: i+1,
|
||||
endCol: endCh+1,
|
||||
endRow: i,
|
||||
endCol: endCh,
|
||||
className: correctVariable ? "good-marker" : "bad-marker",
|
||||
type: "text",
|
||||
})
|
||||
@@ -989,6 +988,17 @@ const CodeEditor = (props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
{contentLoading === true ?
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={`The File content is loading. Please wait a moment.`}
|
||||
placement="top"
|
||||
>
|
||||
<CircularProgress style={{position: "absolute", right: 106, top: 6, }}/>
|
||||
</Tooltip>
|
||||
: null}
|
||||
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={`Move window`}
|
||||
@@ -1571,7 +1581,8 @@ const CodeEditor = (props) => {
|
||||
>
|
||||
<div>
|
||||
<span style={{color: "white"}}>
|
||||
Expected Output
|
||||
|
||||
{selectedAction === undefined ? "" : selectedAction.name === "execute_python" ? "Code to run" : "Expected Output"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1588,7 +1599,7 @@ const CodeEditor = (props) => {
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
position: "absolute",
|
||||
top: 24,
|
||||
right: 65,
|
||||
right: 100,
|
||||
maxHeight: 35,
|
||||
minWidth: 70,
|
||||
}}
|
||||
@@ -1599,7 +1610,7 @@ const CodeEditor = (props) => {
|
||||
{executing ?
|
||||
<CircularProgress style={{height: 18, width: 18, }} />
|
||||
:
|
||||
<span>Try it <PlayArrowIcon style={{height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} /> </span>
|
||||
<span>{selectedAction === undefined ? "" : selectedAction.name === "execute_python" ? "Run Python Code" : "Try it"}<PlayArrowIcon style={{height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} /> </span>
|
||||
}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -204,6 +204,9 @@ const AppGrid = props => {
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
@@ -221,6 +224,11 @@ const AppGrid = props => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
: null}
|
||||
@@ -233,6 +241,8 @@ const AppGrid = props => {
|
||||
flexWrap: "wrap",
|
||||
alignContent: "space-between",
|
||||
marginTop: 5,
|
||||
padding: "0px 180px",
|
||||
width:"auto"
|
||||
}
|
||||
|
||||
var workflowDelay = -50
|
||||
|
||||
@@ -133,6 +133,11 @@ const data = [
|
||||
"background-gradient-stop-colors": "data(fillGradient)",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `node[?parent_controlled]`,
|
||||
css: {
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `node[app_name="Testing"]`,
|
||||
css: {
|
||||
|
||||
@@ -18,11 +18,9 @@ code {
|
||||
margin-right: -13px;
|
||||
}
|
||||
|
||||
.toc:hover {
|
||||
background-color: gray;
|
||||
color: white;
|
||||
table th, table td {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
/* .cm-string{
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
+657
-300
File diff suppressed because it is too large
Load Diff
+2597
-1344
File diff suppressed because it is too large
Load Diff
@@ -955,7 +955,6 @@ const Apps = (props) => {
|
||||
</Link>
|
||||
: null
|
||||
|
||||
console.log("Sharing config: ", sharingConfiguration);
|
||||
const activateButton =
|
||||
selectedApp.generated && !selectedApp.activated ? (
|
||||
<div>
|
||||
@@ -1281,10 +1280,15 @@ const Apps = (props) => {
|
||||
{isCloud && !internalIds.includes(selectedApp.name.toLowerCase()) ?
|
||||
<Tooltip title="Deactivates this app for the current organisation. This means the app will not be usable again until you re-activate it." placement="top">
|
||||
<Button
|
||||
variant="contained"
|
||||
variant={selectedApp.reference_org === userdata.active_org.id ? "outlined" : "contained"}
|
||||
component="label"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (selectedApp.reference_org === userdata.active_org.id) {
|
||||
toast.info("Can't deactivate apps made in this org. Please contact support if you want to deactivate this app.")
|
||||
return
|
||||
}
|
||||
|
||||
const tmpurl = new URL(window.location.href);
|
||||
const searchParams = tmpurl.searchParams;
|
||||
const queryID = searchParams.get("queryID");
|
||||
@@ -1316,7 +1320,7 @@ const Apps = (props) => {
|
||||
console.log("No query to handle when activating");
|
||||
}
|
||||
|
||||
activateApp(selectedApp.id, true)
|
||||
activateApp(selectedApp.id, true, true)
|
||||
}}
|
||||
style={{ height: 35, marginTop: 0, marginLeft: 10, }}
|
||||
>
|
||||
@@ -1845,9 +1849,12 @@ const Apps = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const activateApp = (appid, refresh) => {
|
||||
const activateApp = (appid, refresh, deactivate) => {
|
||||
const appExists = userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appid)
|
||||
const url = appExists ? `${globalUrl}/api/v1/apps/${appid}/deactivate` : `${globalUrl}/api/v1/apps/${appid}/activate`
|
||||
const url = deactivate === true ?
|
||||
`${globalUrl}/api/v1/apps/${appid}/deactivate`
|
||||
:
|
||||
appExists ? `${globalUrl}/api/v1/apps/${appid}/deactivate` : `${globalUrl}/api/v1/apps/${appid}/activate`
|
||||
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -1876,7 +1883,7 @@ const Apps = (props) => {
|
||||
if (appExists) {
|
||||
toast("App deactivated for your organization! Existing workflows with the app will continue to work.")
|
||||
} else {
|
||||
toast("App activated for your organization!")
|
||||
toast("App activation changed for your organization!")
|
||||
}
|
||||
|
||||
if (refresh === true) {
|
||||
@@ -2021,8 +2028,6 @@ const Apps = (props) => {
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
} else {
|
||||
console.log("No query to handle when activating")
|
||||
}
|
||||
|
||||
activateApp(hit.objectID, true)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import ReactJson from "react-json-view";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { BrowserView, MobileView } from "react-device-detect";
|
||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import {
|
||||
Grid,
|
||||
TextField,
|
||||
@@ -26,7 +24,6 @@ import {
|
||||
ListItemButton,
|
||||
ListItemText
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
Link as LinkIcon,
|
||||
Edit as EditIcon,
|
||||
@@ -35,7 +32,6 @@ import {
|
||||
FileCopy as FileCopyIcon
|
||||
} from "@mui/icons-material";
|
||||
import { fontGrid } from "@mui/material/styles/cssUtils.js";
|
||||
import { active } from "d3";
|
||||
|
||||
const Body = {
|
||||
//maxWidth: 1000,
|
||||
@@ -46,6 +42,7 @@ const Body = {
|
||||
height: "100%",
|
||||
color: "white",
|
||||
position: "relative",
|
||||
paddingTop: 40,
|
||||
//textAlign: "center",
|
||||
};
|
||||
|
||||
@@ -155,9 +152,16 @@ export const OuterLink = (props) => {
|
||||
|
||||
|
||||
export const Img = (props) => {
|
||||
return <img style={{ borderRadius: theme.palette.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }} alt={props.alt} src={props.src} />;
|
||||
return(
|
||||
<img
|
||||
style={{border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }}
|
||||
alt={props.alt}
|
||||
src={props.src}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const CodeHandler = (props) => {
|
||||
const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : ""
|
||||
|
||||
@@ -369,8 +373,7 @@ const Docs = (defaultprops) => {
|
||||
hash = hash.split('?')[0]
|
||||
}
|
||||
if (hash) {
|
||||
console.log("HASH: ", hash)
|
||||
const element = document.getElementById(hash)
|
||||
const element = document.getElementById(hash.toLowerCase())
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: "instant",
|
||||
@@ -1022,8 +1025,10 @@ const Docs = (defaultprops) => {
|
||||
<Markdown
|
||||
components={markdownComponents}
|
||||
id="markdown_wrapper"
|
||||
className={"style.reactMarkdown"}
|
||||
escapeHtml={false}
|
||||
skipHtml={false}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
style={{
|
||||
maxWidth: "100%", minWidth: "100%",
|
||||
}}
|
||||
|
||||
@@ -166,7 +166,7 @@ const LoginDialog = (props) => {
|
||||
} else {
|
||||
if (responseJson["reason"] === "MFA_REDIRECT") {
|
||||
setLoginInfo(
|
||||
"MFA required. Please the 6-digit code from your authenticator"
|
||||
"MFA required. Please enter the 6-digit code from your authenticator"
|
||||
);
|
||||
setMFAField(true);
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import ReactDOM from "react-dom"
|
||||
|
||||
import { ToastContainer, toast } from "react-toastify"
|
||||
import { useInterval } from "react-powerhooks";
|
||||
import { makeStyles } from '@mui/material/styles';
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
@@ -44,6 +45,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
const { globalUrl, isLoaded, isLoggedIn, setIsLoggedIn, setCookie, register, serverside } = defaultprops;
|
||||
|
||||
let navigate = useNavigate();
|
||||
const [_, setUpdate] = useState(""); // Used to force rendring, don't remove
|
||||
const [message, setMessage] = useState("");
|
||||
const [workflow, setWorkflow] = React.useState({});
|
||||
const [executionRequest, setExecutionRequest] = React.useState({});
|
||||
@@ -55,6 +57,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
const [selectedOrganization, setSelectedOrganization] = React.useState(undefined);
|
||||
const [apps, setApps] = React.useState([]);
|
||||
const [buttonClicked, setButtonClicked] = React.useState("");
|
||||
const [foundSourcenode, setFoundSourcenode] = React.useState(undefined);
|
||||
|
||||
const boxStyle = {
|
||||
color: "white",
|
||||
@@ -340,6 +343,8 @@ const RunWorkflow = (defaultprops) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
console.log("ONSUBMIT: ", event, execution_id, authorization, answer)
|
||||
|
||||
stop()
|
||||
setMessage("")
|
||||
setExecutionLoading(true)
|
||||
@@ -389,12 +394,27 @@ const RunWorkflow = (defaultprops) => {
|
||||
url += `?reference_execution=${execution_id}&authorization=${authorization}&answer=${answer}`
|
||||
data = {}
|
||||
fetchBody.method = "GET"
|
||||
|
||||
if (executionArgument !== undefined && executionArgument !== null) {
|
||||
try {
|
||||
if (typeof executionArgument === "string") {
|
||||
url += "¬e=" + executionArgument
|
||||
} else {
|
||||
url += "¬e=" + JSON.stringify(executionArgument)
|
||||
}
|
||||
} catch (e) {
|
||||
url += "¬e=" + executionArgument
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
fetchBody.method = "POST"
|
||||
fetchBody.body = JSON.stringify(data)
|
||||
}
|
||||
|
||||
console.log("Pre request: ", url, fetchBody)
|
||||
// IF there is an execution argument, we should use it
|
||||
console.log("FULL URL: ", url)
|
||||
|
||||
fetch(url, fetchBody)
|
||||
.then((response) => {
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
@@ -408,16 +428,26 @@ const RunWorkflow = (defaultprops) => {
|
||||
})
|
||||
|
||||
start();
|
||||
return
|
||||
return response.json()
|
||||
}
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return response.json()
|
||||
})
|
||||
.then(responseJson => {
|
||||
setExecutionLoading(false)
|
||||
if (responseJson["success"] === false) {
|
||||
if (responseJson.success === false) {
|
||||
console.log("Failed sending execution request")
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
toast.warn(responseJson.reason)
|
||||
}
|
||||
|
||||
stop()
|
||||
setMessage("")
|
||||
setExecutionData({})
|
||||
setExecutionInfo("")
|
||||
setExecutionRunning(false)
|
||||
setExecutionRequest({})
|
||||
} else {
|
||||
console.log("Started execution")
|
||||
|
||||
@@ -426,17 +456,24 @@ const RunWorkflow = (defaultprops) => {
|
||||
} else {
|
||||
setExecutionRunning(true);
|
||||
setExecutionRequest(responseJson)
|
||||
start();
|
||||
start()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
//setExecutionInfo("Error in workflow startup: " + error)
|
||||
toast.warn("Error in workflow startup: " + error)
|
||||
|
||||
stop()
|
||||
setMessage("")
|
||||
setExecutionData({})
|
||||
setExecutionInfo("")
|
||||
|
||||
setExecutionLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const getWorkflow = (workflow_id) => {
|
||||
const getWorkflow = (workflow_id, selectedNode) => {
|
||||
fetch(globalUrl + "/api/v1/workflows/" + workflow_id, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -480,6 +517,48 @@ const RunWorkflow = (defaultprops) => {
|
||||
setExecutionArgument(newexec)
|
||||
}
|
||||
|
||||
if (selectedNode !== undefined && selectedNode !== null && selectedNode.length > 0) {
|
||||
var found = false
|
||||
for (var actionkey in responseJson.actions) {
|
||||
if (responseJson.actions[actionkey].id === selectedNode) {
|
||||
found = true
|
||||
setFoundSourcenode(responseJson.actions[actionkey])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
for (var triggerkey in responseJson.triggers) {
|
||||
if (responseJson.triggers[triggerkey].id !== selectedNode) {
|
||||
continue
|
||||
}
|
||||
|
||||
setFoundSourcenode(responseJson.triggers[triggerkey])
|
||||
|
||||
if (responseJson.input_questions !== undefined && responseJson.input_questions !== null && responseJson.input_questions.length > 0 && responseJson.triggers[triggerkey].trigger_type === "USERINPUT") {
|
||||
|
||||
// Look for input questions param
|
||||
for (var paramkey in responseJson.triggers[triggerkey].parameters) {
|
||||
if (responseJson.triggers[triggerkey].parameters[paramkey].name === "input_questions") {
|
||||
|
||||
var relevantquestions = []
|
||||
for (var questionkey in responseJson.input_questions) {
|
||||
if (responseJson.triggers[triggerkey].parameters[paramkey].value.includes(responseJson.input_questions[questionkey].name)) {
|
||||
relevantquestions.push(responseJson.input_questions[questionkey])
|
||||
}
|
||||
}
|
||||
|
||||
responseJson.input_questions = relevantquestions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleGetOrg(responseJson.org_id)
|
||||
setWorkflow(responseJson);
|
||||
})
|
||||
@@ -489,7 +568,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
};
|
||||
|
||||
const { start, stop } = useInterval({
|
||||
duration: 3000,
|
||||
duration: 1500,
|
||||
startImmediate: true,
|
||||
callback: () => {
|
||||
fetchUpdates(executionRequest.execution_id, executionRequest.authorization)
|
||||
@@ -526,15 +605,11 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
for (var key in responseJson.results) {
|
||||
if (responseJson.results[key].status === "WAITING") {
|
||||
console.log("Found: ", responseJson.results[key])
|
||||
|
||||
const validate = validateJson(responseJson.results[key].result)
|
||||
console.log("Validate: ", validate)
|
||||
if (validate.valid && typeof validate.result === "string") {
|
||||
validate.result = JSON.parse(validate.result)
|
||||
}
|
||||
|
||||
console.log("Newresult: ", validate.result)
|
||||
if (validate.result["information"] !== undefined && validate.result["information"] !== null) {
|
||||
setWorkflowQuestion(validate.result["information"])
|
||||
}
|
||||
@@ -651,11 +726,14 @@ const RunWorkflow = (defaultprops) => {
|
||||
});
|
||||
};
|
||||
|
||||
const answer = new URLSearchParams(window.location.search).get("answer")
|
||||
const execution_id = new URLSearchParams(window.location.search).get("reference_execution")
|
||||
const authorization = new URLSearchParams(window.location.search).get("authorization")
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const answer = searchParams.get("answer")
|
||||
const execution_id = searchParams.get("reference_execution")
|
||||
const authorization = searchParams.get("authorization")
|
||||
const sourceNode = searchParams.get("source_node")
|
||||
|
||||
useEffect(() => {
|
||||
getWorkflow(props.match.params.key)
|
||||
getWorkflow(props.match.params.key, sourceNode)
|
||||
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) {
|
||||
console.log("Get execution: ", execution_id)
|
||||
fetchUpdates(execution_id, authorization, true)
|
||||
@@ -667,13 +745,60 @@ const RunWorkflow = (defaultprops) => {
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (executionData === undefined || executionData === null || executionData === {}) {
|
||||
return
|
||||
}
|
||||
|
||||
if (foundSourcenode === undefined || foundSourcenode === null || foundSourcenode === {}) {
|
||||
return
|
||||
}
|
||||
|
||||
if (foundSourcenode.trigger_type !== "USERINPUT") {
|
||||
return
|
||||
}
|
||||
|
||||
if (executionData.results === undefined || executionData.results === null || executionData.results.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (var resultkey in executionData.results) {
|
||||
const result = executionData.results[resultkey]
|
||||
if (result.action.id !== foundSourcenode.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
var parsedresult = result.result
|
||||
try {
|
||||
parsedresult = JSON.parse(parsedresult)
|
||||
} catch (e) {
|
||||
console.log("Error parsing result: ", e)
|
||||
}
|
||||
|
||||
if (result.status !== "WAITING") {
|
||||
if (parsedresult.click_info !== undefined && parsedresult.click_info !== null) {
|
||||
if (parsedresult.click_info.user !== undefined && parsedresult.click_info.user !== null && parsedresult.click_info.user.length > 0) {
|
||||
setMessage("Already answered by " + parsedresult.click_info.user)
|
||||
}
|
||||
} else {
|
||||
setMessage("Answered.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
}, [executionData, foundSourcenode])
|
||||
|
||||
const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"
|
||||
const buttonStyle = {borderRadius: 25, height: 50, fontSize: 18, backgroundImage: handleValidateForm(executionArgument) || executionLoading ? buttonBackground : "grey", color: "white"}
|
||||
|
||||
//console.log("execdata: ", executionData)
|
||||
const disabledButtons = message.length > 0 || executionData.status === "FINISHED" || executionData.status === "ABORTED"
|
||||
|
||||
//{disabledButtons ? null :
|
||||
|
||||
|
||||
const organization = selectedOrganization !== undefined && selectedOrganization !== null ? selectedOrganization.name : "Unknown"
|
||||
const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.org !== undefined && selectedOrganization.org !== null? selectedOrganization.org : "support@shuffler.io"
|
||||
//const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.contact !== undefined && selectedOrganization.contact !== null? selectedOrganization.contact : "support@shuffler.io"
|
||||
@@ -724,49 +849,54 @@ const RunWorkflow = (defaultprops) => {
|
||||
<Typography variant="h6" style={{marginBottom: 10, marginTop: 50, textAlign: "center", }}>
|
||||
{organization}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="textSecondary" style={{marginBottom: 15, marginTop: 0, textAlign: "center",}}>
|
||||
{contact}
|
||||
</Typography>
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
||||
<Typography color="textSecondary">{message}</Typography>
|
||||
|
||||
{disabledButtons && message.length > 0 ? null :
|
||||
<Typography color="textSecondary" style={{textAlign: "center", }}>
|
||||
{message}
|
||||
</Typography>
|
||||
}
|
||||
|
||||
{answer !== undefined && answer !== null ? null :
|
||||
<Typography variant="h6" style={{marginBottom: 15, }}><b>{workflow.name}</b></Typography>
|
||||
<Typography variant="h6" style={{marginBottom: 15, textAlign: "center", }}><b>{workflow.name}</b></Typography>
|
||||
}
|
||||
|
||||
{workflowQuestion.length > 0 ?
|
||||
<Typography variant="body1" style={{ marginBottom: 35, marginTop: 30, marginRight: 15, textAlign: "center", whiteSpace: "pre-line", }}>
|
||||
<div style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
padding: 20,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
marginBottom: 35,
|
||||
marginTop: 30,
|
||||
}}>
|
||||
<Typography variant="body1" style={{ marginRight: 15, textAlign: "center", whiteSpace: "pre-line", }}>
|
||||
{workflowQuestion}
|
||||
</Typography>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ?
|
||||
<div style={{marginBottom: 5, }}>
|
||||
{workflow.input_questions.map((question, index) => {
|
||||
|
||||
return (
|
||||
<div style={{marginBottom: 5}}>
|
||||
{question.name}
|
||||
<TextField
|
||||
color="primary"
|
||||
style={{backgroundColor: theme.palette.inputColor, marginTop: 5, }}
|
||||
multiLine
|
||||
maxRows={2}
|
||||
InputProps={{
|
||||
style:{
|
||||
height: "50px",
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
label={question.value}
|
||||
required
|
||||
|
||||
disabled={disabledButtons}
|
||||
fullWidth={true}
|
||||
placeholder=""
|
||||
id="emailfield"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(e) => {
|
||||
onBlur={(e) => {
|
||||
//setExecutionArgument(e.target.value)
|
||||
executionArgument[question.value] = e.target.value
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -813,20 +943,27 @@ const RunWorkflow = (defaultprops) => {
|
||||
</Typography>
|
||||
: null}
|
||||
</span>
|
||||
|
||||
:
|
||||
answer !== undefined && answer !== null ?
|
||||
((answer !== undefined && answer !== null) || (foundSourcenode !== undefined && foundSourcenode !== null)) ?
|
||||
<span style={{marginTop: 20, }}>
|
||||
|
||||
{disabledButtons && message.length > 0 ?
|
||||
<Typography variant="body1" style={{textAlign: "center", marginTop: 30, marginBottom: 20, }}>
|
||||
{disabledButtons ? "Already answered. Nothing to do." : ""}
|
||||
{message}. You may close this window.
|
||||
</Typography>
|
||||
:
|
||||
<Typography variant="body1" style={{textAlign: "center", marginTop: 30, marginBottom: 20, }}>
|
||||
{disabledButtons ? "Answered. You may close this window." : ""}
|
||||
</Typography>
|
||||
}
|
||||
|
||||
{disabledButtons ? null :
|
||||
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}>
|
||||
What do you want to do?
|
||||
</Typography>
|
||||
}
|
||||
<div fullWidth style={{width: "100%", marginTop: 10, marginBottom: 10, display: "flex", }}>
|
||||
<Button fullWidth id="continue_execution" variant="contained" disabled={disabledButtons} color="primary" style={{border: answer === "true" ? "2px solid rgba(255,255,255,0.6)" : null, flex: 1,}} onClick={() => {
|
||||
<Button fullWidth id="continue_execution" variant="contained" disabled={disabledButtons} color="primary" style={{flex: 1,}} onClick={() => {
|
||||
onSubmit(null, execution_id, authorization, true)
|
||||
|
||||
setButtonClicked("FINISHED")
|
||||
@@ -837,7 +974,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
<Typography variant="body1" style={{marginLeft: 3, marginRight: 3, marginTop: 3, }}>
|
||||
or
|
||||
</Typography>
|
||||
<Button fullWidth id="abort_execution" variant="contained" color="primary" disabled={disabledButtons} style={{border: answer !== "true" ? "2px solid rgba(255,255,255,0.6)" : null, flex: 1, }} onClick={() => {
|
||||
<Button fullWidth id="abort_execution" variant="contained" color="primary" disabled={disabledButtons} style={{ flex: 1, }} onClick={() => {
|
||||
onSubmit(null, execution_id, authorization, false)
|
||||
|
||||
setButtonClicked("ABORTED")
|
||||
@@ -856,7 +993,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
</div>
|
||||
}
|
||||
|
||||
{buttonClicked !== undefined && buttonClicked !== null && buttonClicked !== "finished" && buttonClicked.length > 0 ?
|
||||
{/*buttonClicked !== undefined && buttonClicked !== null && buttonClicked !== "finished" && buttonClicked.length > 0 ?
|
||||
<img id="finalize_gif" src="/images/finalize.gif" alt="finalize workflow animation" style={{width: 150, marginLeft: 125, borderRadius: theme.palette.borderRadius, }}
|
||||
onLoad={() => {
|
||||
console.log("Img loaded.")
|
||||
@@ -868,7 +1005,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
: null*/}
|
||||
|
||||
<div style={{marginTop: "10px"}}>
|
||||
{executionInfo}
|
||||
|
||||
@@ -43,11 +43,10 @@ const Search = (props) => {
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundTab = params["tab"];
|
||||
if (foundTab !== null && foundTab !== undefined) {
|
||||
for (var key in Object.keys(views)) {
|
||||
for (let key in views) {
|
||||
const value = views[key];
|
||||
console.log(key, value);
|
||||
if (value === foundTab) {
|
||||
setConfig("", key);
|
||||
setConfig(null, key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -130,7 +129,7 @@ const Search = (props) => {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflowX: "hidden",
|
||||
minHeight: 400,
|
||||
minHeight: 400
|
||||
};
|
||||
|
||||
const views = {
|
||||
@@ -138,7 +137,9 @@ const Search = (props) => {
|
||||
1: "workflows",
|
||||
2: "docs",
|
||||
3: "creators",
|
||||
4: "discord"
|
||||
};
|
||||
|
||||
const setConfig = (event, inputValue) => {
|
||||
const newValue = parseInt(inputValue);
|
||||
|
||||
@@ -216,7 +217,7 @@ const Search = (props) => {
|
||||
<div style={boxStyle}>
|
||||
<Tabs
|
||||
style={{
|
||||
width: 741,
|
||||
width: 757,
|
||||
margin: isHeader ? null : "auto",
|
||||
marginTop: hidemargins === true ? 0 : isHeader ? null : 25,
|
||||
backgroundColor: "rgba(33, 33, 33, 1)",
|
||||
@@ -227,8 +228,8 @@ const Search = (props) => {
|
||||
textColor="secondary"
|
||||
onChange={setConfig}
|
||||
aria-label="disabled tabs example"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
// variant="scrollable"
|
||||
// scrollButtons="auto"
|
||||
classes={{ indicator: classes.hideIndicator, root: classes.customTab }}
|
||||
>
|
||||
<StyledTab
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Divider,
|
||||
TextField,
|
||||
Modal,
|
||||
Switch,
|
||||
} from "@mui/material";
|
||||
//import { useAlert
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
@@ -19,6 +20,9 @@ import { FileCopy, Visibility, VisibilityOff } from "@mui/icons-material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { Box } from "@mui/system";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
|
||||
const Settings = (props) => {
|
||||
const { globalUrl, isLoaded, userdata, setUserData } = props;
|
||||
@@ -32,6 +36,11 @@ const Settings = (props) => {
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newPassword2, setNewPassword2] = useState("");
|
||||
const [selectedOrganization, setSelectedOrganization] = useState({});
|
||||
const [MFARequired, setMFARequired] = React.useState(false);
|
||||
const [image2FA, setImage2FA] = React.useState("");
|
||||
const [value2FA, setValue2FA] = React.useState("");
|
||||
|
||||
// const [file, setFile] = React.useState("");
|
||||
// const [fileBase64, setFileBase64] = React.useState(
|
||||
// userdata.image === undefined || userdata.image === null
|
||||
@@ -87,6 +96,82 @@ const Settings = (props) => {
|
||||
flexDirection: "column",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userdata.active_org === undefined || userdata.active_org === null || userdata.active_org.id === undefined || userdata.active_org.id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${userdata.active_org.id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
console.log("Received response:", response);
|
||||
if (response.status !== 200) {
|
||||
console.error("Status not 200:", response.status);
|
||||
throw new Error(`Status not 200: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
setSelectedOrganization(responseJson);
|
||||
setMFARequired(responseJson.mfa_required);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching organization:", error);
|
||||
});
|
||||
}, [userdata]);
|
||||
|
||||
const UpdateMFAInUserOrg = (org_id) => {
|
||||
|
||||
if (MFARequired === false) {
|
||||
toast("Making MFA required for your organization. Please wait...");
|
||||
} else {
|
||||
toast("Making MFA optional for your organization. Please wait...");
|
||||
}
|
||||
|
||||
const data = {
|
||||
mfa_required: !selectedOrganization.mfa_required,
|
||||
org_id: selectedOrganization.id,
|
||||
}
|
||||
|
||||
const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`;
|
||||
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((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
console.log(responseJson)
|
||||
if (responseJson["success"] === false) {
|
||||
toast.error("Failed updating org: ", responseJson.reason);
|
||||
} else {
|
||||
if (MFARequired === false) {
|
||||
setMFARequired(true)
|
||||
toast.success("Successfully make MFA required for your organization!");
|
||||
} else {
|
||||
setMFARequired(false)
|
||||
toast.success("Successfully make MFA optional for your organization!")
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
toast("Err: " + error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
const checkOwner = (data, userdata) => {
|
||||
var currentOwner = false;
|
||||
if (data.owner.address === userdata.eth_info.account) {
|
||||
@@ -940,7 +1025,6 @@ const Settings = (props) => {
|
||||
</Button>
|
||||
<h3>{passwordFormMessage}</h3>
|
||||
|
||||
|
||||
{isCloud && (
|
||||
<>
|
||||
<Divider style={{ marginTop: "40px" }} />
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
ArrowLeft as ArrowLeftIcon,
|
||||
ArrowRight as ArrowRightIcon,
|
||||
QueryStats as QueryStatsIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import { DataGrid, GridToolbar } from "@mui/x-data-grid";
|
||||
@@ -129,7 +130,6 @@ export const GetIconInfo = (action) => {
|
||||
// Finds the icon based on the action. Should be verbs.
|
||||
const iconList = [
|
||||
{ key: "cases", values: ["cases"] },
|
||||
{ key: "communication", values: ["communication", "comms", "email",] },
|
||||
{ key: "cache_add", values: ["set_cache"] },
|
||||
{ key: "cache_get", values: ["get_cache"] },
|
||||
{ key: "filter", values: ["filter"] },
|
||||
@@ -199,6 +199,7 @@ export const GetIconInfo = (action) => {
|
||||
values: ["compare", "convert", "to", "filter", "translate", "parse"],
|
||||
},
|
||||
{ key: "close", values: ["close", "stop", "cancel", "block"] },
|
||||
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
|
||||
];
|
||||
|
||||
var selectedKey = ""
|
||||
@@ -614,6 +615,7 @@ const Workflows = (props) => {
|
||||
const [videoViewOpen, setVideoViewOpen] = React.useState(false)
|
||||
const [gettingStartedItems, setGettingStartedItems] = React.useState([])
|
||||
const [selectedWorkflowIndexes, setSelectedWorkflowIndexes] = React.useState([])
|
||||
const [highlightIds, setHighlightIds] = React.useState([])
|
||||
|
||||
const [apps, setApps] = React.useState([]);
|
||||
|
||||
@@ -974,15 +976,16 @@ const Workflows = (props) => {
|
||||
}, 1000);
|
||||
} else if (selectedWorkflowIndexes.length > 0) {
|
||||
// Do backwards so it doesn't change
|
||||
toast("Starting deletion of workflows. This might take a while.")
|
||||
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);
|
||||
deleteWorkflow(workflow.id, true)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
getAvailableWorkflows();
|
||||
getAvailableWorkflows()
|
||||
}, 1000);
|
||||
|
||||
setSelectedWorkflowIndexes([]);
|
||||
@@ -1113,7 +1116,7 @@ const Workflows = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getAvailableWorkflows = () => {
|
||||
const getAvailableWorkflows = (amount) => {
|
||||
var storageWorkflows = []
|
||||
try {
|
||||
const storagewf = localStorage.getItem("workflows")
|
||||
@@ -1130,7 +1133,12 @@ const Workflows = (props) => {
|
||||
//console.log("Failed to get workflows from localstorage: ", e)
|
||||
}
|
||||
|
||||
fetch(globalUrl + "/api/v1/workflows", {
|
||||
var url = `${globalUrl}/api/v1/workflows`
|
||||
if (amount !== undefined && amount !== null) {
|
||||
url += `?top=${amount}`
|
||||
}
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -1154,10 +1162,11 @@ const Workflows = (props) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson !== undefined) {
|
||||
|
||||
var newarray = []
|
||||
for (var key in responseJson) {
|
||||
const wf = responseJson[key]
|
||||
if (wf.public === true) {
|
||||
for (var wfkey in responseJson) {
|
||||
const wf = responseJson[wfkey]
|
||||
if (wf.public === true || wf.hidden === true) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1170,9 +1179,9 @@ const Workflows = (props) => {
|
||||
var parsedactionlist = [];
|
||||
for (var key in newarray) {
|
||||
const workflow = newarray[key]
|
||||
if (workflow.status === "production") {
|
||||
setProdFilter = true
|
||||
}
|
||||
//if (workflow.status === "production") {
|
||||
// setProdFilter = true
|
||||
//}
|
||||
|
||||
for (var actionkey in newarray[key].actions) {
|
||||
const action = newarray[key].actions[actionkey];
|
||||
@@ -1213,7 +1222,24 @@ const Workflows = (props) => {
|
||||
}
|
||||
|
||||
setFirstLoad(false)
|
||||
}, 100)
|
||||
}, 250)
|
||||
|
||||
/*
|
||||
setTimeout(() => {
|
||||
var timeout = 0
|
||||
for (var key in newarray) {
|
||||
const wf = newarray[key]
|
||||
if (wf.actions === undefined || wf.actions === null || wf.actions.length === 0) {
|
||||
setTimeout(() => {
|
||||
sideloadWorkflow(wf.id, false)
|
||||
}, timeout)
|
||||
|
||||
timeout += 1000
|
||||
}
|
||||
|
||||
}
|
||||
}, 1000)
|
||||
*/
|
||||
|
||||
} else {
|
||||
if (isLoggedIn) {
|
||||
@@ -1319,8 +1345,21 @@ const Workflows = (props) => {
|
||||
setView(tmpView);
|
||||
}
|
||||
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundTab = params["top"];
|
||||
if (foundTab !== null && foundTab !== undefined) {
|
||||
// Check if it's a number
|
||||
if (isNaN(foundTab)) {
|
||||
getAvailableWorkflows()
|
||||
} else {
|
||||
getAvailableWorkflows(foundTab)
|
||||
}
|
||||
} else {
|
||||
getAvailableWorkflows()
|
||||
}
|
||||
|
||||
getApps()
|
||||
getAvailableWorkflows();
|
||||
getFramework()
|
||||
}
|
||||
}, [])
|
||||
@@ -1637,20 +1676,24 @@ const Workflows = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const copyWorkflow = (data) => {
|
||||
data = JSON.parse(JSON.stringify(data));
|
||||
toast("Copying workflow " + data.name);
|
||||
data.id = "";
|
||||
data.name = data.name + "_copy";
|
||||
data = deduplicateIds(data, true);
|
||||
const duplicateWorkflow = (data) => {
|
||||
//data = JSON.parse(JSON.stringify(data));
|
||||
toast("Copying workflow '" + data.name + "'. The new workflow will load in and be highlighted.");
|
||||
//data.id = "";
|
||||
//data.name = data.name + "_copy";
|
||||
//data = deduplicateIds(data, true);
|
||||
|
||||
fetch(globalUrl + "/api/v1/workflows", {
|
||||
const duplicateData = {
|
||||
name: data.name + "_copy",
|
||||
}
|
||||
|
||||
fetch(`${globalUrl}/api/v1/workflows/${data.id}/duplicate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
body: JSON.stringify(duplicateData),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
@@ -1660,7 +1703,20 @@ const Workflows = (props) => {
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(() => {
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
if (responseJson.reason !== undefined) {
|
||||
toast("Failed copying workflow: " + responseJson.reason)
|
||||
} else {
|
||||
toast("Failed copying workflow")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (responseJson.id !== undefined) {
|
||||
setHighlightIds([responseJson.id])
|
||||
}
|
||||
setTimeout(() => {
|
||||
getAvailableWorkflows();
|
||||
}, 1000);
|
||||
@@ -1689,19 +1745,72 @@ const Workflows = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const sideloadWorkflow = (id, openEdit) => {
|
||||
const exportSingleWorkflow = (data, setOpen) => {
|
||||
setExportModalOpen(true)
|
||||
|
||||
if (data.triggers !== null && data.triggers !== undefined) {
|
||||
var newSubflows = [];
|
||||
for (var key in data.triggers) {
|
||||
const trigger = data.triggers[key];
|
||||
|
||||
if (
|
||||
trigger.parameters !== null &&
|
||||
trigger.parameters !== undefined
|
||||
) {
|
||||
for (var subkey in trigger.parameters) {
|
||||
const param = trigger.parameters[subkey];
|
||||
if (
|
||||
param.name === "workflow" &&
|
||||
param.value !== data.id &&
|
||||
!newSubflows.includes(param.value)
|
||||
) {
|
||||
newSubflows.push(param.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var parsedworkflows = [];
|
||||
for (var key in newSubflows) {
|
||||
const foundWorkflow = workflows.find(
|
||||
(workflow) => workflow.id === newSubflows[key]
|
||||
);
|
||||
if (foundWorkflow !== undefined && foundWorkflow !== null) {
|
||||
parsedworkflows.push(foundWorkflow);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedworkflows.length > 0) {
|
||||
console.log(
|
||||
"Appending subflows during export: ",
|
||||
parsedworkflows.length
|
||||
);
|
||||
data.subflows = parsedworkflows;
|
||||
}
|
||||
}
|
||||
|
||||
setExportData(data)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const sideloadWorkflow = (id, action, setOpen) => {
|
||||
|
||||
const storagewf = localStorage.getItem("workflows")
|
||||
const storageWorkflows = JSON.parse(storagewf)
|
||||
if (storageWorkflows === null || storageWorkflows === undefined || storageWorkflows.length === 0) {
|
||||
} else {
|
||||
for (var i = 0; i < storageWorkflows.length; i++) {
|
||||
if (storageWorkflows[i].id === id) {
|
||||
if (storageWorkflows[i].image !== "") {
|
||||
if (storageWorkflows[i].image !== "" && storageWorkflows[i].image !== undefined && storageWorkflows[i].image !== null) {
|
||||
|
||||
if (action === undefined || action === null || action === "") {
|
||||
console.log("RETURNING")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetch(globalUrl + "/api/v1/workflows/" + id, {
|
||||
method: "GET",
|
||||
@@ -1718,8 +1827,16 @@ const Workflows = (props) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (openEdit) {
|
||||
if (responseJson.success !== false && responseJson.id !== undefined) {
|
||||
if (action === "edit") {
|
||||
setEditing(responseJson)
|
||||
} else if (action === "publish") {
|
||||
setPublishModalOpen(true)
|
||||
setSelectedWorkflow(responseJson)
|
||||
} else if (action === "export") {
|
||||
exportSingleWorkflow(responseJson, setOpen)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (var i = 0; i < storageWorkflows.length; i++) {
|
||||
@@ -1730,15 +1847,16 @@ const Workflows = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
setWorkflows(storageWorkflows)
|
||||
//setFilteredWorkflows(storageWorkflows)
|
||||
//setWorkflows(storageWorkflows)
|
||||
setFilteredWorkflows(storageWorkflows)
|
||||
//setUpdate(Math.random())
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error.toString())
|
||||
})
|
||||
}
|
||||
|
||||
const deleteWorkflow = (id) => {
|
||||
const deleteWorkflow = (id, bulk) => {
|
||||
fetch(globalUrl + "/api/v1/workflows/" + id, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -1752,15 +1870,19 @@ const Workflows = (props) => {
|
||||
console.log("Status not 200 for setting workflows :O!");
|
||||
toast("Failed deleting workflow. Do you have access?");
|
||||
} else {
|
||||
if (bulk !== true) {
|
||||
toast("Deleted workflow " + id);
|
||||
}
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(() => {
|
||||
if (bulk !== true) {
|
||||
setTimeout(() => {
|
||||
getAvailableWorkflows();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
@@ -1878,7 +2000,9 @@ const Workflows = (props) => {
|
||||
const appGroup = getWorkflowAppgroup(data)
|
||||
const [triggers, subflows] = getWorkflowMeta(data)
|
||||
|
||||
const isDistributed = data.suborg_distribution !== undefined && data.suborg_distribution !== null && data.suborg_distribution.includes(userdata.active_org.id)
|
||||
const hasSuborgs = data.suborg_distribution !== undefined && data.suborg_distribution !== null && data.suborg_distribution.length > 0
|
||||
const isDistributed = (data.parentorg_workflow !== undefined && data.parentorg_workflow !== null && data.parentorg_workflow.length > 0) //|| (data.org_id !== userdata.active_org.id && data.org_id !== undefined && data.org_id !== null && data.org_id.length > 0)
|
||||
|
||||
const workflowMenuButtons = (
|
||||
<Menu
|
||||
id="long-menu"
|
||||
@@ -1890,8 +2014,20 @@ const Workflows = (props) => {
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
>
|
||||
{isDistributed ?
|
||||
<MenuItem
|
||||
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
|
||||
onClick={() => {
|
||||
navigate(`/workflows/${data.id}`)
|
||||
}}
|
||||
>
|
||||
<VisibilityIcon style={{ marginLeft: 0, marginRight: 8 }} />
|
||||
Explore Workflow
|
||||
</MenuItem>
|
||||
: null}
|
||||
<MenuItem
|
||||
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
|
||||
disabled={isDistributed}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
if (data.actions !== undefined && data.actions !== null && data.actions.length > 0 && data.image !== "") {
|
||||
@@ -1899,7 +2035,9 @@ const Workflows = (props) => {
|
||||
|
||||
} else {
|
||||
//toast("Need to side-load workflow to be edited properly")
|
||||
sideloadWorkflow(data.id, true)
|
||||
sideloadWorkflow(data.id, "edit")
|
||||
|
||||
toast.info("Loading full workflow for editing. Please wait...")
|
||||
}
|
||||
}}
|
||||
key={"change"}
|
||||
@@ -1909,9 +2047,11 @@ const Workflows = (props) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
|
||||
disabled={isDistributed}
|
||||
onClick={() => {
|
||||
setSelectedWorkflow(data);
|
||||
setPublishModalOpen(true);
|
||||
sideloadWorkflow(data.id, "publish")
|
||||
|
||||
toast.info("Loading full workflow for publishing. Please wait...")
|
||||
}}
|
||||
key={"publish"}
|
||||
>
|
||||
@@ -1920,9 +2060,10 @@ const Workflows = (props) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
|
||||
disabled={isDistributed}
|
||||
onClick={() => {
|
||||
copyWorkflow(data);
|
||||
setOpen(false);
|
||||
duplicateWorkflow(data)
|
||||
setOpen(false)
|
||||
}}
|
||||
key={"duplicate"}
|
||||
>
|
||||
@@ -1931,52 +2072,11 @@ const Workflows = (props) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
|
||||
disabled={isDistributed}
|
||||
onClick={() => {
|
||||
setExportModalOpen(true);
|
||||
sideloadWorkflow(data.id, "export", setOpen)
|
||||
|
||||
if (data.triggers !== null && data.triggers !== undefined) {
|
||||
var newSubflows = [];
|
||||
for (var key in data.triggers) {
|
||||
const trigger = data.triggers[key];
|
||||
|
||||
if (
|
||||
trigger.parameters !== null &&
|
||||
trigger.parameters !== undefined
|
||||
) {
|
||||
for (var subkey in trigger.parameters) {
|
||||
const param = trigger.parameters[subkey];
|
||||
if (
|
||||
param.name === "workflow" &&
|
||||
param.value !== data.id &&
|
||||
!newSubflows.includes(param.value)
|
||||
) {
|
||||
newSubflows.push(param.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var parsedworkflows = [];
|
||||
for (var key in newSubflows) {
|
||||
const foundWorkflow = workflows.find(
|
||||
(workflow) => workflow.id === newSubflows[key]
|
||||
);
|
||||
if (foundWorkflow !== undefined && foundWorkflow !== null) {
|
||||
parsedworkflows.push(foundWorkflow);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedworkflows.length > 0) {
|
||||
console.log(
|
||||
"Appending subflows during export: ",
|
||||
parsedworkflows.length
|
||||
);
|
||||
data.subflows = parsedworkflows;
|
||||
}
|
||||
}
|
||||
|
||||
setExportData(data);
|
||||
setOpen(false);
|
||||
toast.info("Loading full workflow to be exported. Please wait...")
|
||||
}}
|
||||
key={"export"}
|
||||
>
|
||||
@@ -1985,6 +2085,7 @@ const Workflows = (props) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
|
||||
disabled={isDistributed}
|
||||
onClick={() => {
|
||||
setDeleteModalOpen(true);
|
||||
setSelectedWorkflowId(data.id);
|
||||
@@ -2076,7 +2177,7 @@ const Workflows = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", minWidth: 321, position: "relative", border: isDistributed ? "2px solid #40E0D0" : "inherit", borderRadius: theme.palette.borderRadius, }}>
|
||||
<div style={{width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? "1px solid #40E0D0" : "inherit", borderRadius: theme.palette.borderRadius, }}>
|
||||
<Paper square style={paperAppStyle}>
|
||||
{selectedCategory !== "" ?
|
||||
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
|
||||
@@ -2129,8 +2230,17 @@ const Workflows = (props) => {
|
||||
<Typography>
|
||||
Edit '{data.name}'
|
||||
</Typography>
|
||||
|
||||
<br />
|
||||
|
||||
{isDistributed || hasSuborgs ?
|
||||
<Typography>
|
||||
This is a parentorg-controlled workflow.
|
||||
</Typography>
|
||||
: null}
|
||||
</div>
|
||||
} placement="right">
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{
|
||||
@@ -2462,6 +2572,8 @@ const Workflows = (props) => {
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
var workflowids = []
|
||||
|
||||
// Waits for the read
|
||||
reader.addEventListener("load", (event) => {
|
||||
var data = reader.result;
|
||||
@@ -2498,6 +2610,7 @@ const Workflows = (props) => {
|
||||
data.org = []
|
||||
data.execution_org = {}
|
||||
|
||||
workflowids.push(data.id)
|
||||
|
||||
// Actually create it
|
||||
setNewWorkflow(
|
||||
@@ -2528,6 +2641,10 @@ const Workflows = (props) => {
|
||||
}
|
||||
|
||||
setLoadWorkflowsModalOpen(false);
|
||||
|
||||
if (workflowids.length > 0) {
|
||||
setHighlightIds(workflowids)
|
||||
}
|
||||
};
|
||||
|
||||
const getWorkflowMeta = (data) => {
|
||||
@@ -2869,7 +2986,7 @@ const Workflows = (props) => {
|
||||
className={classes.datagrid}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
pageSize={25}
|
||||
pageSize={100}
|
||||
checkboxSelection
|
||||
autoHeight
|
||||
density="standard"
|
||||
@@ -3682,7 +3799,7 @@ const Workflows = (props) => {
|
||||
workflowDelay += 75
|
||||
} else {
|
||||
return (
|
||||
<Grid key={index} item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px" }}>
|
||||
<Grid key={index} item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px", }}>
|
||||
<WorkflowPaper key={index} data={data} />
|
||||
</Grid>
|
||||
)
|
||||
|
||||
@@ -4,16 +4,16 @@ go 1.22.0
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
replace github.com/shuffle/shuffle-shared => ../../../../shuffle-shared
|
||||
//replace github.com/shuffle/shuffle-shared => ../../../../shuffle-shared
|
||||
|
||||
require (
|
||||
github.com/docker/docker v26.1.0+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/shuffle/shuffle-shared v0.6.37
|
||||
k8s.io/api v0.30.0
|
||||
k8s.io/apimachinery v0.30.0
|
||||
k8s.io/client-go v0.30.0
|
||||
github.com/shuffle/shuffle-shared v0.6.50
|
||||
k8s.io/api v0.30.2
|
||||
k8s.io/apimachinery v0.30.2
|
||||
k8s.io/client-go v0.30.2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -31,7 +31,7 @@ require (
|
||||
github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
|
||||
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -41,7 +41,7 @@ require (
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/frikky/kin-openapi v0.41.0 // indirect
|
||||
github.com/frikky/schemaless v0.0.11 // indirect
|
||||
github.com/frikky/schemaless v0.0.13 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
@@ -82,6 +82,8 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sashabaranov/go-openai v1.19.2 // indirect
|
||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
|
||||
@@ -97,6 +97,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
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/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ=
|
||||
@@ -139,6 +141,8 @@ github.com/frikky/schemaless v0.0.9 h1:RzNLPkJq5c4nlm5iLiTndFcbeQxdMGJIj266wSGt2
|
||||
github.com/frikky/schemaless v0.0.9/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ=
|
||||
github.com/frikky/schemaless v0.0.11 h1:c4r6CJX30XI+SoJdT9RlUd9qYSQlx6hvwGRtsypu+uM=
|
||||
github.com/frikky/schemaless v0.0.11/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ=
|
||||
github.com/frikky/schemaless v0.0.13 h1:ARiN9V7wr2VZXAr9JK5wvTbyPgpGrgeiL1VhR5MlgaQ=
|
||||
github.com/frikky/schemaless v0.0.13/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
@@ -385,6 +389,10 @@ github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6
|
||||
github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
|
||||
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
|
||||
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.6.16 h1:dQBDRmb2Wgl3pEuewqjDvN6v6nUKr+1EvGSEja9zG6s=
|
||||
@@ -395,6 +403,8 @@ github.com/shuffle/shuffle-shared v0.6.27 h1:q4qZD6bGZFIvZ5Y10unGr3N3rZ7OryWyvva
|
||||
github.com/shuffle/shuffle-shared v0.6.27/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4=
|
||||
github.com/shuffle/shuffle-shared v0.6.37 h1:IB8tJqubJmJwwpLYbXNMVWck5jgVO9SKcKo/BBu+wTc=
|
||||
github.com/shuffle/shuffle-shared v0.6.37/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4=
|
||||
github.com/shuffle/shuffle-shared v0.6.50 h1:MBeGAiBNkw9Eg+3YTJIlBOuskWntGvT0uefFUYOBhbY=
|
||||
github.com/shuffle/shuffle-shared v0.6.50/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
@@ -908,10 +918,16 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA=
|
||||
k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE=
|
||||
k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI=
|
||||
k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI=
|
||||
k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA=
|
||||
k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
|
||||
k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg=
|
||||
k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
|
||||
k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ=
|
||||
k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY=
|
||||
k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50=
|
||||
k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs=
|
||||
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
|
||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
|
||||
@@ -1452,7 +1452,7 @@ func main() {
|
||||
if isKubernetes == "true" {
|
||||
clientset, _, err := shuffle.GetKubernetesClient()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Error getting kubernetes client:", err)
|
||||
log.Printf("[ERROR] Error getting kubernetes client: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user