Fixed relevant stats & license trackers to make it easier to use
This commit is contained in:
@@ -969,6 +969,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
|||||||
tutorialsFinished = append(tutorialsFinished, tutorial)
|
tutorialsFinished = append(tutorialsFinished, tutorial)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
licensed := shuffle.IsLicensed(ctx, *currentOrg)
|
||||||
|
|
||||||
returnValue := shuffle.HandleInfo{
|
returnValue := shuffle.HandleInfo{
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -989,6 +990,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
|||||||
Tutorials: tutorialsFinished,
|
Tutorials: tutorialsFinished,
|
||||||
|
|
||||||
Priorities: orgPriorities,
|
Priorities: orgPriorities,
|
||||||
|
Licensed: licensed,
|
||||||
}
|
}
|
||||||
|
|
||||||
returnData, err := json.Marshal(returnValue)
|
returnData, err := json.Marshal(returnValue)
|
||||||
|
|||||||
@@ -293,12 +293,35 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
|||||||
env, err := shuffle.GetEnvironment(ctx, orgId, "")
|
env, err := shuffle.GetEnvironment(ctx, orgId, "")
|
||||||
timeNow := time.Now().Unix()
|
timeNow := time.Now().Unix()
|
||||||
if err == nil && len(env.Id) > 0 && len(env.Name) > 0 {
|
if err == nil && len(env.Id) > 0 && len(env.Name) > 0 {
|
||||||
|
// Updates every 60 seconds~
|
||||||
if time.Now().Unix() > env.Edited+60 {
|
if time.Now().Unix() > env.Edited+60 {
|
||||||
env.RunningIp = request.RemoteAddr
|
env.RunningIp = shuffle.GetRequestIp(request)
|
||||||
|
if len(orborusLabel) > 0 {
|
||||||
|
env.RunningIp = orborusLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Method == "POST" {
|
||||||
|
body, err := ioutil.ReadAll(request.Body)
|
||||||
|
if err == nil {
|
||||||
|
var envData shuffle.OrborusStats
|
||||||
|
err = json.Unmarshal(body, &envData)
|
||||||
|
if err == nil {
|
||||||
|
if envData.Swarm {
|
||||||
|
env.Licensed = true
|
||||||
|
env.RunType = "docker"
|
||||||
|
}
|
||||||
|
|
||||||
|
if envData.Kubernetes {
|
||||||
|
env.RunType = "k8s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
env.Checkin = timeNow
|
env.Checkin = timeNow
|
||||||
err = shuffle.SetEnvironment(ctx, env)
|
err = shuffle.SetEnvironment(ctx, &env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARNING] Failed updating environment: %s", err)
|
log.Printf("[ERROR] Failed updating environment: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,10 +120,11 @@ const Billing = (props) => {
|
|||||||
minHeight: 280,
|
minHeight: 280,
|
||||||
maxWidth: 400,
|
maxWidth: 400,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
backgroundColor: theme.palette.surfaceColor,
|
backgroundColor: theme.palette.platformColor,
|
||||||
borderRadius: theme.palette.borderRadius,
|
borderRadius: theme.palette.borderRadius*2,
|
||||||
border: "1px solid rgba(255,255,255,0.3)",
|
border: "1px solid rgba(255,255,255,0.3)",
|
||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
|
marginTop: 15,
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCloud =
|
const isCloud =
|
||||||
@@ -273,6 +274,7 @@ const Billing = (props) => {
|
|||||||
|
|
||||||
const [signatureOpen, setSignatureOpen] = React.useState(false);
|
const [signatureOpen, setSignatureOpen] = React.useState(false);
|
||||||
const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed)
|
const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed)
|
||||||
|
const [hovered, setHovered] = React.useState(false)
|
||||||
|
|
||||||
var top_text = "Base Cloud Access"
|
var top_text = "Base Cloud Access"
|
||||||
if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) {
|
if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) {
|
||||||
@@ -321,8 +323,16 @@ const Billing = (props) => {
|
|||||||
newPaperstyle.border = "1px solid #f85a3e"
|
newPaperstyle.border = "1px solid #f85a3e"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
newPaperstyle.backgroundColor = theme.palette.surfaceColor
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper style={newPaperstyle}>
|
<Paper
|
||||||
|
style={newPaperstyle}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={signatureOpen}
|
open={signatureOpen}
|
||||||
PaperProps={{
|
PaperProps={{
|
||||||
@@ -960,16 +970,16 @@ const Billing = (props) => {
|
|||||||
|
|
||||||
const isChildOrg = userdata.active_org.creator_org !== "" && userdata.active_org.creator_org !== undefined && userdata.active_org.creator_org !== null
|
const isChildOrg = userdata.active_org.creator_org !== "" && userdata.active_org.creator_org !== undefined && userdata.active_org.creator_org !== null
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{}}>
|
||||||
{addDealModal}
|
{addDealModal}
|
||||||
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
|
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
|
||||||
Billing
|
Billing & Licensing
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10}}>
|
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10}}>
|
||||||
{isCloud ?
|
{isCloud ?
|
||||||
"We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below."
|
"Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below."
|
||||||
:
|
:
|
||||||
"Shuffle is an Open Source automation platform, and no license is required to use it. You may however activate Cloud Sync, get our Scale license, get help with Kubernetes, or talk to Shuffle's Support team to get automation help."
|
"Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update."
|
||||||
}
|
}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
@@ -1315,7 +1325,7 @@ const Billing = (props) => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
) : null*/}
|
) : null*/}
|
||||||
<div style={{ marginTop: 30, }}>
|
<div style={{ marginTop: 100, }}>
|
||||||
<Typography
|
<Typography
|
||||||
style={{ marginTop: 40, marginLeft: 10, marginBottom: 5 }}
|
style={{ marginTop: 40, marginLeft: 10, marginBottom: 5 }}
|
||||||
variant="h4"
|
variant="h4"
|
||||||
|
|||||||
@@ -636,13 +636,13 @@ const AppStats = (defaultprops) => {
|
|||||||
|
|
||||||
const data = (
|
const data = (
|
||||||
<div className="content" style={{width: "100%", margin: "auto", }}>
|
<div className="content" style={{width: "100%", margin: "auto", }}>
|
||||||
<Typography variant="body1" style={{margin: "auto", marginLeft: 10, marginBottom: 20, }}>
|
<Typography variant="body1" style={{margin: "auto", marginLeft: 10, marginBottom: 20, }} color="textSecondary">
|
||||||
All shown statistics are gathered from <a
|
All shown statistics are gathered from <a
|
||||||
href={`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`}
|
href={`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style={{ textDecoration: "none", color: "#f85a3e",}}
|
style={{ textDecoration: "none", color: "#f85a3e",}}
|
||||||
>Your Organization Statistics </a>
|
>Your Organisation Statistics. </a>
|
||||||
This is a feature to help give you more insight into Shuffle, and to understand your utilization of the Shuffle platform. <b>The billing tracker is in Beta, and is always calculated manually before being invoiced.</b>
|
It exists to give you more insight into your workflows, and to understand your utilization of the Shuffle platform. <b>The billing tracker is in Beta, and is always calculated manually before being invoiced.</b>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div style={{display: "flex", textAlign: "center",}}>
|
<div style={{display: "flex", textAlign: "center",}}>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
Publish as PublishIcon,
|
Publish as PublishIcon,
|
||||||
OpenInNew as OpenInNewIcon,
|
OpenInNew as OpenInNewIcon,
|
||||||
Add as AddIcon,
|
Add as AddIcon,
|
||||||
|
Remove as RemoveIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
|
||||||
const EditWorkflow = (props) => {
|
const EditWorkflow = (props) => {
|
||||||
@@ -76,7 +77,8 @@ const EditWorkflow = (props) => {
|
|||||||
const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "")
|
const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "")
|
||||||
const [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date*1000) : dayjs().subtract(1, 'day'))
|
const [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date*1000) : dayjs().subtract(1, 'day'))
|
||||||
|
|
||||||
const [inputFields, setInputFields] = React.useState([])
|
console.log("WORKFLOW: ", workflow)
|
||||||
|
const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : [])
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
@@ -232,7 +234,91 @@ const EditWorkflow = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<DialogContent style={{paddingTop: 10, display: "flex", minHeight: 300, zIndex: 1001, }}>
|
<div style={{width: 600, position: "fixed", left: 0, bottom: 0, zIndex: 1002, backgroundColor: "rgba(53,53,53,1)", height: 75, paddingTop: 20, paddingLeft: 75, }}>
|
||||||
|
{/*
|
||||||
|
<Button
|
||||||
|
style={{}}
|
||||||
|
onClick={() => {
|
||||||
|
if (setNewWorkflow !== undefined) {
|
||||||
|
setWorkflow({})
|
||||||
|
}
|
||||||
|
|
||||||
|
setModalOpen(false)
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
*/}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
style={{}}
|
||||||
|
disabled={name.length === 0 || submitLoading === true}
|
||||||
|
onClick={() => {
|
||||||
|
setSubmitLoading(true)
|
||||||
|
|
||||||
|
// Loop inputfields
|
||||||
|
var validfields = []
|
||||||
|
for (var i = 0; i < inputQuestions.length; i++) {
|
||||||
|
if (inputQuestions[i].deleted === true) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputQuestions[i].value.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
validfields.push(inputQuestions[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
innerWorkflow.input_questions = validfields
|
||||||
|
|
||||||
|
innerWorkflow.name = name
|
||||||
|
innerWorkflow.description = description
|
||||||
|
if (newWorkflowTags.length > 0) {
|
||||||
|
innerWorkflow.tags = newWorkflowTags
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedUsecases.length > 0) {
|
||||||
|
innerWorkflow.usecase_ids = selectedUsecases
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dueDate > 0) {
|
||||||
|
innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"]+1}-${dueDate["$D"]}`).getTime()/1000
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setNewWorkflow !== undefined) {
|
||||||
|
setNewWorkflow(
|
||||||
|
innerWorkflow.name,
|
||||||
|
innerWorkflow.description,
|
||||||
|
innerWorkflow.tags,
|
||||||
|
innerWorkflow.default_return_value,
|
||||||
|
innerWorkflow,
|
||||||
|
newWorkflow,
|
||||||
|
innerWorkflow.usecase_ids,
|
||||||
|
innerWorkflow.blogpost,
|
||||||
|
innerWorkflow.status,
|
||||||
|
)
|
||||||
|
setWorkflow({})
|
||||||
|
} else {
|
||||||
|
setWorkflow(innerWorkflow)
|
||||||
|
console.log("editing workflow: ", innerWorkflow)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitLoading(true)
|
||||||
|
|
||||||
|
// If new workflow, don't close it
|
||||||
|
if (isEditing) {
|
||||||
|
setModalOpen(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{submitLoading ? <CircularProgress color="secondary" /> : "Done"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogContent style={{paddingTop: 10, display: "flex", minHeight: 300, zIndex: 1001, paddingBottom: 200, }}>
|
||||||
<div style={{minWidth: newWorkflow ? 500 : 550, maxWidth: newWorkflow ? 450 : 500, }}>
|
<div style={{minWidth: newWorkflow ? 500 : 550, maxWidth: newWorkflow ? 450 : 500, }}>
|
||||||
<TextField
|
<TextField
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -477,41 +563,21 @@ const EditWorkflow = (props) => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/*
|
<Typography variant="h6" style={{marginTop: 50, }}>
|
||||||
<Typography variant="h6">
|
|
||||||
Input fields
|
Input fields
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1">
|
<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>.
|
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>
|
</Typography>
|
||||||
|
|
||||||
{inputFields.length === 0 ?
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
style={{ maxWidth: 50, marginLeft: 15 }}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => {
|
|
||||||
inputFields.push({
|
|
||||||
"name": "",
|
|
||||||
"required": false
|
|
||||||
})
|
|
||||||
setInputFields(inputFields)
|
|
||||||
setUpdate(Math.random());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AddIcon style={{}} />
|
|
||||||
</Button>
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{inputFields.map((data, index) => {
|
{inputQuestions.map((data, index) => {
|
||||||
console.log("Inputfield: ", data)
|
console.log("Inputfield: ", data)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: "flex", }}>
|
<div style={{display: "flex", }}>
|
||||||
<TextField
|
<TextField
|
||||||
required={data.required}
|
disabled={data.deleted === true}
|
||||||
style={{
|
style={{
|
||||||
height: 50,
|
height: 50,
|
||||||
flex: 2,
|
flex: 2,
|
||||||
@@ -521,14 +587,45 @@ const EditWorkflow = (props) => {
|
|||||||
marginRight: 5,
|
marginRight: 5,
|
||||||
}}
|
}}
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
placeholder="Name"
|
placeholder="Question"
|
||||||
|
id="standard-required"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
defaultValue={data.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
inputQuestions[index].name = e.target.value
|
||||||
|
setInputQuestions(inputQuestions)
|
||||||
|
setUpdate(Math.random());
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
classes: {
|
||||||
|
notchedOutline: classes.notchedOutline,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
color: "white",
|
||||||
|
minHeight: 50,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={data.deleted === true}
|
||||||
|
style={{
|
||||||
|
height: 50,
|
||||||
|
flex: 2,
|
||||||
|
marginTop: 0,
|
||||||
|
marginBottom: 0,
|
||||||
|
backgroundColor: theme.palette.inputColor,
|
||||||
|
marginRight: 5,
|
||||||
|
}}
|
||||||
|
fullWidth={true}
|
||||||
|
placeholder="JSON key"
|
||||||
id="standard-required"
|
id="standard-required"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
defaultValue={data.value}
|
defaultValue={data.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
inputFields[index].name = e.target.value
|
inputQuestions[index].value = e.target.value
|
||||||
setInputFields(inputFields)
|
setInputQuestions(inputQuestions)
|
||||||
setUpdate(Math.random());
|
setUpdate(Math.random());
|
||||||
}}
|
}}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@@ -544,28 +641,44 @@ const EditWorkflow = (props) => {
|
|||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
style={{ maxWidth: 50, marginLeft: 15 }}
|
style={{ maxWidth: 50, marginLeft: 15 }}
|
||||||
|
disabled={data.deleted === true}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
inputFields.push({
|
// Remove current index
|
||||||
"name": "",
|
console.log("Removing index: ", index)
|
||||||
"required": false
|
inputQuestions[index].deleted = true
|
||||||
})
|
setUpdate(Math.random());
|
||||||
setInputFields(inputFields)
|
|
||||||
setUpdate(Math.random());
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AddIcon style={{}} />
|
<RemoveIcon style={{}} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
*/}
|
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
style={{ maxWidth: 50, marginLeft: 15, marginTop: 20, }}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => {
|
||||||
|
inputQuestions.push({
|
||||||
|
"name": "",
|
||||||
|
"value": "",
|
||||||
|
"deleted": false,
|
||||||
|
"required": false
|
||||||
|
})
|
||||||
|
setInputQuestions(inputQuestions)
|
||||||
|
setUpdate(Math.random());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddIcon style={{}} />
|
||||||
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
<Tooltip color="primary" title={"Add more details"} placement="top">
|
<Tooltip color="primary" title={"Add more details"} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
style={{ color: "white", margin: "auto", marginTop: 10, textAlign: "center", width: 50,}}
|
style={{ color: "white", margin: "auto", textAlign: "center", width: 50, marginTop: 50, }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowMoreClicked(!showMoreClicked);
|
setShowMoreClicked(!showMoreClicked);
|
||||||
}}
|
}}
|
||||||
@@ -577,71 +690,7 @@ const EditWorkflow = (props) => {
|
|||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions style={{paddingRight: 100, marginBottom: 100, }}>
|
|
||||||
<Button
|
|
||||||
style={{}}
|
|
||||||
onClick={() => {
|
|
||||||
if (setNewWorkflow !== undefined) {
|
|
||||||
setWorkflow({})
|
|
||||||
}
|
|
||||||
|
|
||||||
setModalOpen(false)
|
|
||||||
}}
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
style={{}}
|
|
||||||
disabled={name.length === 0 || submitLoading === true}
|
|
||||||
onClick={() => {
|
|
||||||
setSubmitLoading(true)
|
|
||||||
|
|
||||||
innerWorkflow.name = name
|
|
||||||
innerWorkflow.description = description
|
|
||||||
if (newWorkflowTags.length > 0) {
|
|
||||||
innerWorkflow.tags = newWorkflowTags
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedUsecases.length > 0) {
|
|
||||||
innerWorkflow.usecase_ids = selectedUsecases
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dueDate > 0) {
|
|
||||||
innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"]+1}-${dueDate["$D"]}`).getTime()/1000
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setNewWorkflow !== undefined) {
|
|
||||||
setNewWorkflow(
|
|
||||||
innerWorkflow.name,
|
|
||||||
innerWorkflow.description,
|
|
||||||
innerWorkflow.tags,
|
|
||||||
innerWorkflow.default_return_value,
|
|
||||||
innerWorkflow,
|
|
||||||
newWorkflow,
|
|
||||||
innerWorkflow.usecase_ids,
|
|
||||||
innerWorkflow.blogpost,
|
|
||||||
innerWorkflow.status,
|
|
||||||
)
|
|
||||||
setWorkflow({})
|
|
||||||
} else {
|
|
||||||
setWorkflow(innerWorkflow)
|
|
||||||
console.log("editing workflow: ", innerWorkflow)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitLoading(true)
|
|
||||||
|
|
||||||
// If new workflow, don't close it
|
|
||||||
if (isEditing) {
|
|
||||||
setModalOpen(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
{submitLoading ? <CircularProgress color="secondary" /> : "Done"}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
{newWorkflow === true ?
|
{newWorkflow === true ?
|
||||||
<span style={{marginTop: 30, }}>
|
<span style={{marginTop: 30, }}>
|
||||||
<Typography variant="h6" style={{marginLeft: 30, paddingBottom: 0, }}>
|
<Typography variant="h6" style={{marginLeft: 30, paddingBottom: 0, }}>
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const Header = (props) => {
|
|||||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
const [anchorElAvatar, setAnchorElAvatar] = React.useState(null);
|
const [anchorElAvatar, setAnchorElAvatar] = React.useState(null);
|
||||||
const [subAnchorEl, setSubAnchorEl] = React.useState(null);
|
const [subAnchorEl, setSubAnchorEl] = React.useState(null);
|
||||||
|
const [upgradeHovered, setUpgradeHovered] = React.useState(false);
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const handleClick = (event) => {
|
const handleClick = (event) => {
|
||||||
@@ -1097,10 +1098,10 @@ const Header = (props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Show on cloud, if not suborg and if not customer/pov/internal */}
|
{/* Show on cloud, if not suborg and if not customer/pov/internal */}
|
||||||
{isCloud &&
|
{
|
||||||
(userdata.org_status === undefined ||
|
userdata.licensed !== undefined &&
|
||||||
userdata.org_status === null ||
|
userdata.licensed !== null &&
|
||||||
userdata.org_status.length === 0) ? (
|
userdata.licensed === false ?
|
||||||
<ListItem
|
<ListItem
|
||||||
style={{
|
style={{
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
@@ -1109,11 +1110,13 @@ const Header = (props) => {
|
|||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link to="/pricing?tab=cloud&highlight=true" style={hrefStyle}>
|
<Link to="/admin?tab=billing" style={hrefStyle}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
style={{ textTransform: "none" }}
|
style={{ textTransform: "none" }}
|
||||||
|
onMouseOver={() => {setUpgradeHovered(true)}}
|
||||||
|
onMouseOut={() => {setUpgradeHovered(false)}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
ReactGA.event({
|
ReactGA.event({
|
||||||
@@ -1122,13 +1125,30 @@ const Header = (props) => {
|
|||||||
label: "",
|
label: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.drift !== undefined) {
|
||||||
|
if (isCloud) {
|
||||||
|
window.drift.api.startInteraction({
|
||||||
|
interactionId: 386404,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
window.drift.api.startInteraction({
|
||||||
|
interactionId: 386403,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Upgrade
|
{upgradeHovered ?
|
||||||
|
isCloud ? "Upgrade License" : "Upgrade License"
|
||||||
|
:
|
||||||
|
isCloud ? "Upgrade" : "Upgrade"
|
||||||
|
}
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
: null}
|
||||||
|
|
||||||
{userdata === undefined ||
|
{userdata === undefined ||
|
||||||
userdata.app_execution_limit === undefined ||
|
userdata.app_execution_limit === undefined ||
|
||||||
@@ -1145,7 +1165,6 @@ const Header = (props) => {
|
|||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
borderRadius: theme.palette.borderRadius,
|
borderRadius: theme.palette.borderRadius,
|
||||||
marginRight: 10,
|
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
backgroundColor: theme.palette.surfaceColor,
|
backgroundColor: theme.palette.surfaceColor,
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
@@ -1154,7 +1173,7 @@ const Header = (props) => {
|
|||||||
userdata.app_execution_usage /
|
userdata.app_execution_usage /
|
||||||
userdata.app_execution_limit >=
|
userdata.app_execution_limit >=
|
||||||
0.9
|
0.9
|
||||||
? "#f86a3e"
|
? "2px solid #f86a3e"
|
||||||
: null,
|
: null,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -3258,7 +3258,6 @@ const ParsedAction = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("DID NAME REPLACE ACTUALLY WORK? - may be missing it in certain triggers");
|
|
||||||
setWorkflow(workflow);
|
setWorkflow(workflow);
|
||||||
setUpdate(Math.random());
|
setUpdate(Math.random());
|
||||||
baselabel = name
|
baselabel = name
|
||||||
|
|||||||
+159
-96
@@ -70,6 +70,9 @@ import {
|
|||||||
Business as BusinessIcon,
|
Business as BusinessIcon,
|
||||||
Visibility as VisibilityIcon,
|
Visibility as VisibilityIcon,
|
||||||
VisibilityOff as VisibilityOffIcon,
|
VisibilityOff as VisibilityOffIcon,
|
||||||
|
Cancel as CancelIcon,
|
||||||
|
Dns as DnsIcon,
|
||||||
|
Help as HelpIcon,
|
||||||
|
|
||||||
Flag as FlagIcon,
|
Flag as FlagIcon,
|
||||||
FmdGood as FmdGoodIcon,
|
FmdGood as FmdGoodIcon,
|
||||||
@@ -193,9 +196,26 @@ const Admin = (props) => {
|
|||||||
const [, forceUpdate] = React.useState();
|
const [, forceUpdate] = React.useState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUsers()
|
getUsers()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (adminTab === 3) {
|
||||||
|
window.scroll({
|
||||||
|
top: 450,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 1500)
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.scroll({
|
||||||
|
top: 450,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}, [adminTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDropzone) {
|
if (isDropzone) {
|
||||||
@@ -970,6 +990,8 @@ If you're interested, please let me know a time that works for you, or set up a
|
|||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
if (!responseJson.success && responseJson.reason !== undefined) {
|
if (!responseJson.success && responseJson.reason !== undefined) {
|
||||||
toast("Failed to deactivate user: " + responseJson.reason);
|
toast("Failed to deactivate user: " + responseJson.reason);
|
||||||
|
} else if (responseJson.success === false) {
|
||||||
|
toast("Failed to deactivate user. Please contact support@shuffler.io if this persists.")
|
||||||
} else {
|
} else {
|
||||||
toast("Changed activation for user " + data.id);
|
toast("Changed activation for user " + data.id);
|
||||||
}
|
}
|
||||||
@@ -1146,55 +1168,54 @@ If you're interested, please let me know a time that works for you, or set up a
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickChangeOrg = (orgId) => {
|
const handleClickChangeOrg = (orgId) => {
|
||||||
// Don't really care about the logout
|
// Don't really care about the logout
|
||||||
//name: org.name,
|
//name: org.name,
|
||||||
//orgId = "asd"
|
//orgId = "asd"
|
||||||
const data = {
|
const data = {
|
||||||
org_id: orgId,
|
org_id: orgId,
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem("globalUrl", "")
|
localStorage.setItem("globalUrl", "")
|
||||||
localStorage.setItem("getting_started_sidebar", "open");
|
localStorage.setItem("getting_started_sidebar", "open");
|
||||||
|
|
||||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
|
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
crossDomain: true,
|
crossDomain: true,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
console.log("Error in response")
|
console.log("Error in response")
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}).then(function(responseJson) {
|
}).then(function(responseJson) {
|
||||||
if (responseJson.success === true) {
|
if (responseJson.success === true) {
|
||||||
if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) {
|
if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) {
|
||||||
console.log("Region Change: ", responseJson.region_url)
|
localStorage.setItem("globalUrl", responseJson.region_url)
|
||||||
localStorage.setItem("globalUrl", responseJson.region_url)
|
//globalUrl = responseJson.region_url
|
||||||
//globalUrl = responseJson.region_url
|
}
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}, 2000)
|
}, 2000)
|
||||||
toast("Successfully changed active organization - refreshing!")
|
toast("Successfully changed active organization - refreshing!")
|
||||||
} else {
|
} else {
|
||||||
toast("Failed changing org: ", responseJson.reason)
|
toast("Failed changing org: "+responseJson.reason)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log("error changing: ", error)
|
console.log("error changing: ", error)
|
||||||
//removeCookie("session_token", {path: "/"})
|
//removeCookie("session_token", {path: "/"})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const inviteUser = (data) => {
|
const inviteUser = (data) => {
|
||||||
@@ -1850,6 +1871,8 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
if (!responseJson.success && responseJson.reason !== undefined) {
|
if (!responseJson.success && responseJson.reason !== undefined) {
|
||||||
toast("Failed setting user: " + responseJson.reason);
|
toast("Failed setting user: " + responseJson.reason);
|
||||||
|
} else if (responseJson.success === false) {
|
||||||
|
toast("Failed to update user")
|
||||||
} else {
|
} else {
|
||||||
//toast("Set the user field " + field + " to " + value);
|
//toast("Set the user field " + field + " to " + value);
|
||||||
toast("Successfully updated user field " + field)
|
toast("Successfully updated user field " + field)
|
||||||
@@ -2912,6 +2935,7 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
|
id="admin_tabs"
|
||||||
value={adminTab}
|
value={adminTab}
|
||||||
indicatorColor="primary"
|
indicatorColor="primary"
|
||||||
textColor="secondary"
|
textColor="secondary"
|
||||||
@@ -2961,18 +2985,18 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{adminTab === 0 ? (
|
{adminTab === 0 ? (
|
||||||
<OrgHeaderexpanded
|
<OrgHeaderexpanded
|
||||||
isCloud={isCloud}
|
isCloud={isCloud}
|
||||||
userdata={userdata}
|
userdata={userdata}
|
||||||
setSelectedOrganization={setSelectedOrganization}
|
setSelectedOrganization={setSelectedOrganization}
|
||||||
globalUrl={globalUrl}
|
globalUrl={globalUrl}
|
||||||
selectedOrganization={selectedOrganization}
|
selectedOrganization={selectedOrganization}
|
||||||
adminTab={adminTab}
|
adminTab={adminTab}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: adminTab === 1 ? (
|
: adminTab === 1 ? (
|
||||||
<div>
|
<div>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
style={{ marginBottom: "10px", color: "white" }}
|
style={{ marginBottom: "10px", color: "white" }}
|
||||||
@@ -4448,8 +4472,7 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
<div style={{ marginTop: 20, marginBottom: 20 }}>
|
<div style={{ marginTop: 20, marginBottom: 20 }}>
|
||||||
<h2 style={{ display: "inline" }}>Environments</h2>
|
<h2 style={{ display: "inline" }}>Environments</h2>
|
||||||
<span style={{ marginLeft: 25 }}>
|
<span style={{ marginLeft: 25 }}>
|
||||||
Decides what Orborus environment to execute an action in a workflow
|
Decides what Orborus environment to run your workflow actions. If you have scale problems, talk to our team: support@shuffler.io.
|
||||||
in.{" "}
|
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -4491,25 +4514,33 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem style={{paddingLeft: 10, }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="Type"
|
||||||
|
style={{ minWidth: 50, maxWidth: 50, }}
|
||||||
|
/>
|
||||||
|
<ListItemText
|
||||||
|
primary="License"
|
||||||
|
style={{ minWidth: 85, maxWidth: 85, }}
|
||||||
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Name"
|
primary="Name"
|
||||||
style={{ minWidth: 150, maxWidth: 150 }}
|
style={{ minWidth: 150, maxWidth: 150 }}
|
||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Orborus running"
|
primary="Orborus label"
|
||||||
style={{ minWidth: 200, maxWidth: 200 }}
|
style={{ minWidth: 150, maxWidth: 150 }}
|
||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Command"
|
primary="Command"
|
||||||
style={{ minWidth: 100, maxWidth: 100 }}
|
style={{ minWidth: 100, maxWidth: 100 }}
|
||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Type"
|
primary="Location"
|
||||||
style={{ minWidth: 100, maxWidth: 100, }}
|
style={{ minWidth: 100, maxWidth: 100, }}
|
||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={"In Queue"}
|
primary={"Queue"}
|
||||||
style={{ minWidth: 100, maxWidth: 100 }}
|
style={{ minWidth: 100, maxWidth: 100 }}
|
||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
@@ -4557,8 +4588,54 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
const queueSize = environment.queue !== undefined && environment.queue !== null ? environment.queue < 0 ? 0 : environment.queue > 1000 ? ">1000" : environment.queue : 0
|
const queueSize = environment.queue !== undefined && environment.queue !== null ? environment.queue < 0 ? 0 : environment.queue > 1000 ? ">1000" : environment.queue : 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span key={index}>
|
<span key={index}>
|
||||||
<ListItem key={index} style={{ backgroundColor: bgColor }}>
|
<ListItem key={index} style={{ backgroundColor: bgColor, marginLeft: 0, }}>
|
||||||
|
<ListItemText
|
||||||
|
primary={environment.run_type === "cloud" ?
|
||||||
|
<Tooltip title="Cloud" placement="top">
|
||||||
|
<CloudIcon style={{ color: "rgba(255,255,255,0.8)" }} />
|
||||||
|
</Tooltip>
|
||||||
|
: environment.run_type === "docker" ?
|
||||||
|
<Tooltip title="Docker" placement="top">
|
||||||
|
<img src="/icons/docker.svg" style={{ width: 30, height: 30, }} />
|
||||||
|
</Tooltip>
|
||||||
|
: environment.run_type === "k8s" ?
|
||||||
|
<Tooltip title="Kubernetes" placement="top">
|
||||||
|
<img src="/icons/k8s.svg" style={{ width: 30, height: 30, }} />
|
||||||
|
</Tooltip>
|
||||||
|
:
|
||||||
|
<Tooltip title="Unknown" placement="top">
|
||||||
|
<HelpIcon style={{ color: "rgba(255,255,255,0.8)" }} />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
minWidth: 50,
|
||||||
|
maxWidth: 50,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ListItemText
|
||||||
|
primary={environment.licensed ?
|
||||||
|
<Tooltip title="Licensed" placement="top">
|
||||||
|
<CheckCircleIcon style={{ color: "#4caf50", }} />
|
||||||
|
</Tooltip>
|
||||||
|
:
|
||||||
|
<Tooltip title="Not licensed, and can't scale.. This may cause service disruption." placement="top">
|
||||||
|
<a
|
||||||
|
href="/admin?tab=billing"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<CancelIcon style={{ color: "#f85a3e", }} />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
minWidth: 85,
|
||||||
|
maxWidth: 85,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={environment.Name}
|
primary={environment.Name}
|
||||||
style={{
|
style={{
|
||||||
@@ -4574,15 +4651,15 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
environment.running_ip === null ||
|
environment.running_ip === null ||
|
||||||
environment.running_ip.length === 0
|
environment.running_ip.length === 0
|
||||||
?
|
?
|
||||||
<div>
|
<div>
|
||||||
Not running
|
Not running
|
||||||
</div>
|
</div>
|
||||||
: environment.running_ip.split(":")[0]
|
: environment.running_ip.split(":")[0]
|
||||||
: "N/A"
|
: "N/A"
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
minWidth: 200,
|
minWidth: 150,
|
||||||
maxWidth: 200,
|
maxWidth: 150,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -4849,11 +4926,7 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Name"
|
primary="Name"
|
||||||
style={{ minWidth: 250, maxWidth: 250 }}
|
style={{ minWidth: 350, maxWidth: 350}}
|
||||||
/>
|
|
||||||
<ListItemText
|
|
||||||
primary="Your role"
|
|
||||||
style={{ minWidth: 150, maxWidth: 150 }}
|
|
||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="id"
|
primary="id"
|
||||||
@@ -4867,11 +4940,7 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={parentOrg.name}
|
primary={parentOrg.name}
|
||||||
style={{ minWidth: 250, maxWidth: 250 }}
|
style={{ minWidth: 350, maxWidth: 350}}
|
||||||
/>
|
|
||||||
<ListItemText
|
|
||||||
primary={parentOrg.role}
|
|
||||||
style={{ minWidth: 150, maxWidth: 150 }}
|
|
||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={parentOrg.id}
|
primary={parentOrg.id}
|
||||||
@@ -4881,11 +4950,12 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={parentOrg.id === selectedOrganization.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleClickChangeOrg(parentOrg.id);
|
handleClickChangeOrg(parentOrg.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Switch To Parent
|
Switch to Parent
|
||||||
</Button>
|
</Button>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
@@ -4920,7 +4990,7 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
letterSpacing: "1px",
|
letterSpacing: "1px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Sub Organizations of the Current Organization
|
Sub Organizations of the Current Organization ({subOrgs.length})
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -4940,11 +5010,7 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Name"
|
primary="Name"
|
||||||
style={{ minWidth: 250, maxWidth: 250 }}
|
style={{ minWidth: 350, maxWidth: 350 }}
|
||||||
/>
|
|
||||||
<ListItemText
|
|
||||||
primary="Your role"
|
|
||||||
style={{ minWidth: 150, maxWidth: 150 }}
|
|
||||||
/>
|
/>
|
||||||
<ListItemText primary="id" style={{ minWidth: 400, maxWidth: 400 }} />
|
<ListItemText primary="id" style={{ minWidth: 400, maxWidth: 400 }} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -4974,11 +5040,7 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={data.name}
|
primary={data.name}
|
||||||
style={{ minWidth: 250, maxWidth: 250 }}
|
style={{ minWidth: 350, maxWidth: 350 }}
|
||||||
/>
|
|
||||||
<ListItemText
|
|
||||||
primary={data.role}
|
|
||||||
style={{ minWidth: 150, maxWidth: 150 }}
|
|
||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={data.id}
|
primary={data.id}
|
||||||
@@ -4986,8 +5048,9 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={data.id === selectedOrganization.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleClickChangeOrg(data.id);
|
handleClickChangeOrg(data.id);
|
||||||
}}
|
}}
|
||||||
@@ -5247,7 +5310,7 @@ const handleClickChangeOrg = (orgId) => {
|
|||||||
disabled={userdata.admin !== "true"}
|
disabled={userdata.admin !== "true"}
|
||||||
label=<span>
|
label=<span>
|
||||||
<ScheduleIcon style={iconStyle} />
|
<ScheduleIcon style={iconStyle} />
|
||||||
Schedules
|
Triggers
|
||||||
</span>
|
</span>
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
|
|||||||
@@ -16255,6 +16255,16 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
executionData.execution_source === "questions" || executionData.execution_source === "web" ?
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={`/workflows/${executionData.workflow.id}/run`}
|
||||||
|
target="_blank"
|
||||||
|
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||||
|
>
|
||||||
|
Questions
|
||||||
|
</a>
|
||||||
|
:
|
||||||
executionData.execution_source
|
executionData.execution_source
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -141,11 +141,30 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{marginTop: executionMargin, }}>
|
<div style={{marginTop: executionMargin, }}>
|
||||||
<Typography variant="h6" style={{color: theme.palette.primaryColor}}>
|
{/*executionData !== undefined && executionData !== null && executionData !== {} && executionData.status !== undefined && (answer === undefined || answer === null) ?
|
||||||
Results
|
<div style={{ marginBottom: 5, display: "flex" }}>
|
||||||
</Typography>
|
<Typography variant="body1">
|
||||||
|
<b>Status </b>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="textSecondary" style={{ marginRight: 15, }}>
|
||||||
|
{executionData.status}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
: null*/}
|
||||||
|
|
||||||
{executionData.results.map((data, index) => {
|
{/*
|
||||||
|
<Typography variant="h6" style={{color: theme.palette.primaryColor}}>
|
||||||
|
Results: {executionData.results.length}/{executionData.workflow.actions.length}
|
||||||
|
</Typography>
|
||||||
|
*/}
|
||||||
|
|
||||||
|
{executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ?
|
||||||
|
<Typography variant="h6">
|
||||||
|
{executionData.result}
|
||||||
|
</Typography>
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{/*executionData.results.map((data, index) => {
|
||||||
if (executionData.results.length !== 1 && (data.status === "SKIPPED")) {
|
if (executionData.results.length !== 1 && (data.status === "SKIPPED")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -311,7 +330,7 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})*/}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -321,8 +340,6 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("In submit!")
|
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
setMessage("")
|
setMessage("")
|
||||||
setExecutionLoading(true)
|
setExecutionLoading(true)
|
||||||
@@ -330,12 +347,22 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
setExecutionInfo("")
|
setExecutionInfo("")
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
"execution_argument": executionArgument
|
"execution_argument": executionArgument,
|
||||||
|
"execution_source": "questions",
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) {
|
||||||
|
try {
|
||||||
|
data["execution_argument"] = JSON.stringify(executionArgument)
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error parsing execution argument: ", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflow.start !== undefined && workflow.start !== null && workflow.start.length > 0) {
|
if (workflow.start !== undefined && workflow.start !== null && workflow.start.length > 0) {
|
||||||
data.start = workflow.start
|
//data.start = workflow.start
|
||||||
} else {
|
} else {
|
||||||
|
/*
|
||||||
if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) {
|
if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) {
|
||||||
for (let actionkey in workflow.actions) {
|
for (let actionkey in workflow.actions) {
|
||||||
if (workflow.actions[actionkey].isStartNode) {
|
if (workflow.actions[actionkey].isStartNode) {
|
||||||
@@ -344,6 +371,7 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = `${globalUrl}/api/v1/workflows/${props.match.params.key}/execute`
|
var url = `${globalUrl}/api/v1/workflows/${props.match.params.key}/execute`
|
||||||
@@ -369,7 +397,6 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
console.log("Pre request: ", url, fetchBody)
|
console.log("Pre request: ", url, fetchBody)
|
||||||
fetch(url, fetchBody)
|
fetch(url, fetchBody)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log("Got answer 1")
|
|
||||||
if (response.status !== 200 && response.status !== 201) {
|
if (response.status !== 200 && response.status !== 201) {
|
||||||
|
|
||||||
if (answer !== undefined && execution_id !== undefined && authorization !== undefined) {
|
if (answer !== undefined && execution_id !== undefined && authorization !== undefined) {
|
||||||
@@ -385,11 +412,9 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Got answer 2")
|
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(responseJson => {
|
.then(responseJson => {
|
||||||
console.log("Got answer 3")
|
|
||||||
setExecutionLoading(false)
|
setExecutionLoading(false)
|
||||||
if (responseJson["success"] === false) {
|
if (responseJson["success"] === false) {
|
||||||
console.log("Failed sending execution request")
|
console.log("Failed sending execution request")
|
||||||
@@ -404,7 +429,6 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("Got answer 4")
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
//setExecutionInfo("Error in workflow startup: " + error)
|
//setExecutionInfo("Error in workflow startup: " + error)
|
||||||
@@ -446,8 +470,18 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
responseJson.triggers = [];
|
responseJson.triggers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGetOrg(responseJson.org_id)
|
if (responseJson.input_questions !== undefined && responseJson.input_questions !== null && responseJson.input_questions.length > 0) {
|
||||||
setWorkflow(responseJson);
|
var newexec = {}
|
||||||
|
for (let questionkey in responseJson.input_questions) {
|
||||||
|
const question = responseJson.input_questions[questionkey]
|
||||||
|
newexec[question.value] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
setExecutionArgument(newexec)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleGetOrg(responseJson.org_id)
|
||||||
|
setWorkflow(responseJson);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log("Get workflow error: ", error.toString());
|
console.log("Get workflow error: ", error.toString());
|
||||||
@@ -475,6 +509,18 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
// Doesn't work because this is some async garbage
|
// Doesn't work because this is some async garbage
|
||||||
if (executionData.execution_id === undefined || (responseJson.execution_id === executionData.execution_id && responseJson.results !== undefined && responseJson.results !== null)) {
|
if (executionData.execution_id === undefined || (responseJson.execution_id === executionData.execution_id && responseJson.results !== undefined && responseJson.results !== null)) {
|
||||||
if (executionData.status !== responseJson.status || executionData.result !== responseJson.result || (executionData.results !== undefined && responseJson.results !== null && executionData.results.length !== responseJson.results.length)) {
|
if (executionData.status !== responseJson.status || executionData.result !== responseJson.result || (executionData.results !== undefined && responseJson.results !== null && executionData.results.length !== responseJson.results.length)) {
|
||||||
|
|
||||||
|
if (responseJson.result !== undefined && responseJson.result !== null && responseJson.result.length > 0) {
|
||||||
|
if (responseJson.result.startsWith("[") && responseJson.result.endsWith("]")) {
|
||||||
|
try {
|
||||||
|
responseJson.result = JSON.parse(responseJson.result).length
|
||||||
|
console.log("Set length to: ", responseJson.result)
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error parsing length: ", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//console.log("Updating data!")
|
//console.log("Updating data!")
|
||||||
setExecutionData(responseJson)
|
setExecutionData(responseJson)
|
||||||
|
|
||||||
@@ -685,27 +731,50 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
<Typography color="textSecondary">{message}</Typography>
|
<Typography color="textSecondary">{message}</Typography>
|
||||||
|
|
||||||
{answer !== undefined && answer !== null ? null :
|
{answer !== undefined && answer !== null ? null :
|
||||||
<Typography variant="h6" style={{marginBottom: 15, }}><b>Workflow: </b>{workflow.name}</Typography>
|
<Typography variant="h6" style={{marginBottom: 15, }}><b>{workflow.name}</b></Typography>
|
||||||
}
|
}
|
||||||
|
|
||||||
{executionData !== undefined && executionData !== null && executionData !== {} && executionData.status !== undefined && (answer === undefined || answer === null) ?
|
|
||||||
<div style={{ marginBottom: 5, display: "flex" }}>
|
|
||||||
<Typography variant="body1">
|
|
||||||
<b>Status </b>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" color="textSecondary" style={{ marginRight: 15, }}>
|
|
||||||
{executionData.status}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{workflowQuestion.length > 0 ?
|
{workflowQuestion.length > 0 ?
|
||||||
<Typography variant="body1" style={{ marginBottom: 35, marginTop: 30, marginRight: 15, textAlign: "center", whiteSpace: "pre-line", }}>
|
<Typography variant="body1" style={{ marginBottom: 35, marginTop: 30, marginRight: 15, textAlign: "center", whiteSpace: "pre-line", }}>
|
||||||
{workflowQuestion}
|
{workflowQuestion}
|
||||||
</Typography>
|
</Typography>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
{answer !== undefined && answer !== null ? 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",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
fullWidth={true}
|
||||||
|
placeholder=""
|
||||||
|
id="emailfield"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
onChange={(e) => {
|
||||||
|
//setExecutionArgument(e.target.value)
|
||||||
|
executionArgument[question.value] = e.target.value
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
answer !== undefined && answer !== null ? null :
|
||||||
<span>
|
<span>
|
||||||
Runtime Argument
|
Runtime Argument
|
||||||
<div style={{marginBottom: 5}}>
|
<div style={{marginBottom: 5}}>
|
||||||
@@ -733,6 +802,7 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
{executionRunning ?
|
{executionRunning ?
|
||||||
<span style={{width: 50, height: 50, margin: "auto", alignItems: "center", justifyContent: "center", textAlign: "center", }}>
|
<span style={{width: 50, height: 50, margin: "auto", alignItems: "center", justifyContent: "center", textAlign: "center", }}>
|
||||||
<CircularProgress style={{marginTop: 20, marginBottom: 20, marginLeft: 185, }}/>
|
<CircularProgress style={{marginTop: 20, marginBottom: 20, marginLeft: 185, }}/>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
github.com/docker/docker v23.0.3+incompatible
|
github.com/docker/docker v23.0.3+incompatible
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
github.com/shuffle/shuffle-shared v0.5.68
|
github.com/shuffle/shuffle-shared v0.5.93
|
||||||
k8s.io/api v0.28.1
|
k8s.io/api v0.28.1
|
||||||
k8s.io/apimachinery v0.28.1
|
k8s.io/apimachinery v0.28.1
|
||||||
k8s.io/client-go v0.28.1
|
k8s.io/client-go v0.28.1
|
||||||
|
|||||||
@@ -361,6 +361,8 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
|||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/shuffle/shuffle-shared v0.5.68 h1:2ok4SsLojHvFIQMfe2upm3CLEvSbUPMJPIBaBZO1bL8=
|
github.com/shuffle/shuffle-shared v0.5.68 h1:2ok4SsLojHvFIQMfe2upm3CLEvSbUPMJPIBaBZO1bL8=
|
||||||
github.com/shuffle/shuffle-shared v0.5.68/go.mod h1:oIZkx93Z7EvtiTXty7xO+ax63Fjz8MQvjXMxDN0Qws0=
|
github.com/shuffle/shuffle-shared v0.5.68/go.mod h1:oIZkx93Z7EvtiTXty7xO+ax63Fjz8MQvjXMxDN0Qws0=
|
||||||
|
github.com/shuffle/shuffle-shared v0.5.93 h1:fZf9s2cEgDoyYXXvPYVeNh8UysuSlywQkc54IXkNL0k=
|
||||||
|
github.com/shuffle/shuffle-shared v0.5.93/go.mod h1:Lg6/+qjQlWzNKwj4/4ATpvScyP2JQGLkTPlNlRM6RJk=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
|
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
|
||||||
|
|||||||
@@ -1065,26 +1065,10 @@ func getOrborusStats(ctx context.Context) shuffle.OrborusStats {
|
|||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Returning for now due to this causing network congestion
|
if (swarmConfig == "run" || swarmConfig == "swarm") && strings.Contains(newWorkerImage, "scale") {
|
||||||
// and database fillup. The backend api also has it disabled.
|
|
||||||
return newStats
|
|
||||||
|
|
||||||
// Disable orborus stats
|
|
||||||
if os.Getenv("SHUFFLE_STATS_DISABLED") == "true" {
|
|
||||||
return newStats
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if swarmConfig == "run" || swarmConfig == "swarm" {
|
|
||||||
newStats.Swarm = true
|
newStats.Swarm = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Run this 1/10 times
|
|
||||||
//if rand.Intn(10) != 1 {
|
|
||||||
// return newStats
|
|
||||||
//}
|
|
||||||
|
|
||||||
newStats.PollTime = sleepTime
|
newStats.PollTime = sleepTime
|
||||||
newStats.MaxQueue = maxConcurrency
|
newStats.MaxQueue = maxConcurrency
|
||||||
newStats.Queue = executionCount
|
newStats.Queue = executionCount
|
||||||
@@ -1094,6 +1078,15 @@ func getOrborusStats(ctx context.Context) shuffle.OrborusStats {
|
|||||||
return newStats
|
return newStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable orborus stats
|
||||||
|
if os.Getenv("SHUFFLE_STATS_DISABLED") == "true" {
|
||||||
|
return newStats
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Returning for now due to this causing network congestion
|
||||||
|
// and database fillup. The backend api also has it disabled.
|
||||||
|
return newStats
|
||||||
|
|
||||||
// Use the docker API to get the CPU usage of the docker engine machine
|
// Use the docker API to get the CPU usage of the docker engine machine
|
||||||
pers, err := dockercli.Info(ctx)
|
pers, err := dockercli.Info(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user