Added authenticationSSO file
This commit is contained in:
@@ -2,7 +2,7 @@ module shuffle
|
||||
|
||||
go 1.13
|
||||
|
||||
replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared
|
||||
//replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared
|
||||
|
||||
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
|
||||
|
||||
@@ -23,7 +23,7 @@ require (
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/elastic/go-elasticsearch/v7 v7.13.1 // indirect
|
||||
github.com/frikky/kin-openapi v0.39.0
|
||||
github.com/frikky/shuffle-shared v0.0.98
|
||||
github.com/frikky/shuffle-shared v0.0.99
|
||||
github.com/fsouza/go-dockerclient v1.7.2
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/go-git/go-billy/v5 v5.0.0
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
version: '3'
|
||||
services:
|
||||
frontend:
|
||||
build: ./frontend
|
||||
#build: ./frontend
|
||||
image: ghcr.io/frikky/shuffle-frontend:nightly
|
||||
container_name: shuffle-frontend
|
||||
hostname: shuffle-frontend
|
||||
@@ -16,7 +16,7 @@ services:
|
||||
depends_on:
|
||||
- backend
|
||||
backend:
|
||||
build: ./backend
|
||||
#build: ./backend
|
||||
image: ghcr.io/frikky/shuffle-backend:nightly
|
||||
container_name: shuffle-backend
|
||||
hostname: ${BACKEND_HOSTNAME}
|
||||
@@ -62,7 +62,7 @@ services:
|
||||
- SHUFFLE_PASS_WORKER_PROXY=${SHUFFLE_PASS_WORKER_PROXY}
|
||||
- SHUFFLE_PASS_APP_PROXY=${SHUFFLE_PASS_APP_PROXY}
|
||||
- SHUFFLE_ORBORUS_EXECUTION_TIMEOUT=600
|
||||
- SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY=15
|
||||
- SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY=5
|
||||
- CLEANUP=${SHUFFLE_CONTAINER_AUTO_CLEANUP}
|
||||
restart: unless-stopped
|
||||
opensearch:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -52,6 +52,7 @@ const Admin = (props) => {
|
||||
const [selectedNamespace, setSelectedNamespace] = React.useState("default")
|
||||
const [fileNamespaces, setFileNamespaces] = React.useState([]);
|
||||
const [selectedUser, setSelectedUser] = React.useState({})
|
||||
const [newUsername, setNewUsername] = React.useState("");
|
||||
const [newPassword, setNewPassword] = React.useState("");
|
||||
const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false)
|
||||
const [selectedAuthentication, setSelectedAuthentication] = React.useState({})
|
||||
@@ -1201,6 +1202,7 @@ const Admin = (props) => {
|
||||
alert.error("Failed setting user: " + responseJson.reason)
|
||||
} else {
|
||||
alert.success("Set the user field " + field + " to " + value)
|
||||
setSelectedUserModalOpen(false)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -1340,6 +1342,44 @@ const Admin = (props) => {
|
||||
>
|
||||
<DialogTitle><span style={{ color: "white" }}><EditIcon style={{marginTop: 5}}/> Editing {selectedUser.username}</span></DialogTitle>
|
||||
<DialogContent>
|
||||
{isCloud ?
|
||||
null
|
||||
:
|
||||
<div style={{ display: "flex" }}>
|
||||
<TextField
|
||||
style={{ marginTop: 0, backgroundColor: theme.palette.inputColor, flex: 3 , marginRight: 10,}}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: 50,
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
required
|
||||
fullWidth={true}
|
||||
placeholder="New username"
|
||||
type="text"
|
||||
id="standard-required"
|
||||
autoComplete="username"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
defaultValue={selectedUser.username}
|
||||
onChange={e => {
|
||||
setNewUsername(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{ maxHeight: 50, flex: 1 }}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setUser(selectedUser.id, "username", newUsername)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
{isCloud ?
|
||||
null
|
||||
:
|
||||
@@ -1626,7 +1666,7 @@ const Admin = (props) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{selectedOrganization.name.length > 0 ?
|
||||
<OrgHeader setSelectedOrganization={setSelectedOrganization} globalUrl={globalUrl} selectedOrganization={selectedOrganization}/>
|
||||
<OrgHeader userdata={userdata} setSelectedOrganization={setSelectedOrganization} globalUrl={globalUrl} selectedOrganization={selectedOrganization}/>
|
||||
:
|
||||
<div style={{paddingTop: 250, width: 250, margin: "auto", textAlign: "center"}}>
|
||||
<CircularProgress />
|
||||
@@ -2070,7 +2110,6 @@ const Admin = (props) => {
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
console.log("VALUE: ", e.target.value)
|
||||
|
||||
setUser(data.id, "role", e.target.value)
|
||||
}}
|
||||
style={{ backgroundColor: theme.palette.surfaceColor, color: "white", height: "50px" }}
|
||||
|
||||
@@ -8186,7 +8186,7 @@ const AngularWorkflow = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.action.app_name === "Shuffle Tools") {
|
||||
if (data.action.app_name === "Shuffle Tools" && data.action.id !== undefined && cy !== undefined) {
|
||||
//console.log("APP (TOOLS): ", data.action)
|
||||
|
||||
const nodedata = cy.getElementById(data.action.id).data()
|
||||
|
||||
@@ -1146,8 +1146,37 @@ const AppCreator = (props) => {
|
||||
|
||||
if (queryitem.name.toLowerCase() == "url") {
|
||||
console.log(item.name+" uses a bad query: url")
|
||||
skipped = true
|
||||
break
|
||||
continue
|
||||
//skipped = true
|
||||
//break
|
||||
}
|
||||
|
||||
if (queryitem.name.toLowerCase() == "url"
|
||||
|| queryitem.name.toLowerCase() == "body"
|
||||
|| queryitem.name.toLowerCase() == "queries"
|
||||
|| queryitem.name.toLowerCase() == "headers"
|
||||
|| queryitem.name.includes("[")
|
||||
|| queryitem.name.includes("]")
|
||||
|| queryitem.name.includes("{")
|
||||
|| queryitem.name.includes("}")
|
||||
|| queryitem.name.includes("(")
|
||||
|| queryitem.name.includes(")")
|
||||
|| queryitem.name.includes("!")
|
||||
|| queryitem.name.includes("@")
|
||||
|| queryitem.name.includes("#")
|
||||
|| queryitem.name.includes("$")
|
||||
|| queryitem.name.includes("%")
|
||||
|| queryitem.name.includes("^")
|
||||
|| queryitem.name.includes("&")
|
||||
|| queryitem.name.includes(":")
|
||||
|| queryitem.name.includes(";")
|
||||
|| queryitem.name.includes("<")
|
||||
|| queryitem.name.includes(">")
|
||||
|| queryitem.name.includes("\"")
|
||||
|| queryitem.name.includes("\'")
|
||||
) {
|
||||
console.log(item.name+" error: uses a bad query - not adding: ", queryitem.name)
|
||||
continue
|
||||
}
|
||||
|
||||
var newitem = {
|
||||
@@ -1344,10 +1373,13 @@ const AppCreator = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
var newparamName = parameterName.replaceAll("\"", "")
|
||||
newparamName = newparamName.replaceAll("\'", "")
|
||||
|
||||
data.components.securitySchemes["ApiKeyAuth"] = {
|
||||
"type": "apiKey",
|
||||
"in": parameterLocation.toLowerCase(),
|
||||
"name": parameterName,
|
||||
"name": newparamName,
|
||||
}
|
||||
} else if (authenticationOption === "Bearer auth") {
|
||||
data.components.securitySchemes["BearerAuth"] = {
|
||||
@@ -1367,13 +1399,16 @@ const AppCreator = (props) => {
|
||||
"scheme": "basic",
|
||||
}
|
||||
} else if (authenticationOption === "Oauth2") {
|
||||
var newparamName = parameterName.replaceAll("\"", "")
|
||||
newparamName = newparamName.replaceAll("\'", "")
|
||||
|
||||
//parameterName, parameterValue, revocationUrl
|
||||
data.components.securitySchemes["Oauth2"] = {
|
||||
"type": "oauth2",
|
||||
"description": "Oauth2.0 authorizationCode authentication",
|
||||
"flow": {
|
||||
"authorizationCode": {
|
||||
"authorizationUrl": parameterName,
|
||||
"authorizationUrl": newparamName,
|
||||
"tokenUrl": parameterLocation,
|
||||
"refreshUrl": refreshUrl,
|
||||
"scopes": [],
|
||||
@@ -1769,8 +1804,10 @@ const AppCreator = (props) => {
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
value={parameterName}
|
||||
helperText={<span style={{color:"white", marginBottom: "2px",}}>Can't be empty. Can't contain any of the following characters: !#$%&'^+-._~|]+$</span>}
|
||||
onChange={e => setParameterName(e.target.value)}
|
||||
helperText={<span style={{color:"white", marginBottom: "2px",}}>Can't be empty. Can't contain any of the following characters: !#$%&'^"+-._~|]+$</span>}
|
||||
onChange={e => {
|
||||
setParameterName(e.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
@@ -1954,11 +1991,60 @@ const AppCreator = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const HandleIndividualChip = (props) => {
|
||||
const { chipData, index } = props;
|
||||
const [chipRequired, setChipRequired] = useState(false)
|
||||
|
||||
return (
|
||||
<Tooltip title={chipRequired ? "Make not required" : "Make required"}>
|
||||
<Chip
|
||||
style={{
|
||||
backgroundColor: chipRequired ? "#f86a3e" : "#3d3f43",
|
||||
height: 30,
|
||||
margin: 3,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
height: 28,
|
||||
cursor: "pointer",
|
||||
borderColor: "#3d3f43",
|
||||
color: "white",
|
||||
}}
|
||||
label={chipData}
|
||||
onClick={() => {
|
||||
console.log("CLICK: ", chipData)
|
||||
setChipRequired(!chipRequired)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const SetExtraBodyField = (props) => {
|
||||
const { extraBodyFields } = props;
|
||||
|
||||
if (extraBodyFields === undefined || extraBodyFields === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
//const parsedlist = extraBodyFields.join(", ")
|
||||
//console.log("LIST: ", parsedlist)
|
||||
|
||||
return (
|
||||
<span>
|
||||
{extraBodyFields.map((data, index) => {
|
||||
return (
|
||||
<HandleIndividualChip key={index} chipData={data} />
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const bodyInfo = actionBodyRequest.includes(currentActionMethod) ?
|
||||
<div style={{marginTop: 10}}>
|
||||
<b>Request Body</b>: {extraBodyFields.length > 0 ?
|
||||
<Typography style={{display: "inline-block"}}>
|
||||
Variables: {extraBodyFields.join(", ")}
|
||||
<SetExtraBodyField extraBodyFields={extraBodyFields}/>
|
||||
</Typography>
|
||||
:
|
||||
<Typography style={{display: "inline-block"}}>
|
||||
@@ -2988,10 +3074,13 @@ const AppCreator = (props) => {
|
||||
</Button>
|
||||
: null}
|
||||
</div>
|
||||
<h2>Actions {actionAmount > 0 ? <span>({actionAmount} / {filteredActions.length})</span> : null}</h2>
|
||||
<Typography variant="h6">Actions {actionAmount > 0 ? <span>({actionAmount} / {filteredActions.length})</span> : null}</Typography>
|
||||
<Typography variant="body2" style={{marginTop: 10, marginBottom: 10}}>
|
||||
Actions are the tasks performed by an app - usually single URL paths for REST API's.
|
||||
</Typography>
|
||||
|
||||
{projectCategories !== undefined && projectCategories !== null && projectCategories.length > 1 ?
|
||||
<div style={{marginTop: 5, marginBottom: 5}}>
|
||||
<div style={{marginTop: 10, marginBottom: 10}}>
|
||||
{projectCategories.map((tag, index) => {
|
||||
const newname = tag.charAt(0).toUpperCase() + tag.slice(1)
|
||||
return (
|
||||
@@ -3000,7 +3089,7 @@ const AppCreator = (props) => {
|
||||
style={{
|
||||
backgroundColor: tag === selectedCategory ? "#f86a3e" : "#3d3f43",
|
||||
height: 30,
|
||||
marginRight: 5,
|
||||
margin: 3,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
height: 28,
|
||||
@@ -3037,7 +3126,6 @@ const AppCreator = (props) => {
|
||||
})}
|
||||
</div>
|
||||
: null}
|
||||
Actions are the tasks performed by an app - usually single URL paths for REST API's.
|
||||
<div>
|
||||
{loopActions}
|
||||
<div style={{display: "flex"}}>
|
||||
|
||||
@@ -243,9 +243,9 @@ const Docs = (props) => {
|
||||
const path = "/docs/"+item
|
||||
const newname = item.charAt(0).toUpperCase()+item.substring(1).split("_").join(" ").split("-").join(" ")
|
||||
return (
|
||||
<li key={index} style={{marginTop: 15,}}>
|
||||
<li key={index} style={{marginTop: 10,}}>
|
||||
<Link key={index} style={hrefStyle} to={path} onClick={() => {fetchDocs(item)}}>
|
||||
<Typography variant="h6"><b>{newname}</b></Typography>
|
||||
<Typography style={{color: props.match.params.key.toLowerCase() === item.toLowerCase() ? "#f86a3e" : "inherit"}} variant="body1"><b>> {newname}</b></Typography>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import React, {useRef, useState, useEffect, useLayoutEffect} from 'react';
|
||||
|
||||
import { Typography, CircularProgress } from '@material-ui/core';
|
||||
|
||||
const SetAuthentication = (props) => {
|
||||
const { globalUrl, isLoggedIn, isLoaded, userdata } = props;
|
||||
|
||||
const [firstRequest, setFirstRequest] = useState(true)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [response, setResponse] = useState("")
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
if (firstRequest) {
|
||||
setFirstRequest(false)
|
||||
|
||||
//code
|
||||
//session_state
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
|
||||
const authenticationStore = []
|
||||
var appAuthData = {
|
||||
"label": "",
|
||||
"app": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"app_version": "",
|
||||
},
|
||||
"fields": [],
|
||||
"type": "oauth2",
|
||||
}
|
||||
|
||||
if (window !== undefined && window !== null) {
|
||||
console.log(window.location)
|
||||
appAuthData.fields.push({"key": "redirect_uri", "value": window.location.origin+window.location.pathname})
|
||||
}
|
||||
|
||||
if (params.code !== undefined && params.code !== null) {
|
||||
appAuthData.fields.push({"key": "code", "value": params.code})
|
||||
}
|
||||
|
||||
if (params.session_state !== undefined && params.session_state !== null) {
|
||||
appAuthData.fields.push({"key": "session_state", "value": params.session_state})
|
||||
}
|
||||
|
||||
if (params.state !== undefined && params.state !== null) {
|
||||
const paramsplit = params.state.split("&")
|
||||
console.log(paramsplit)
|
||||
for (var key in paramsplit) {
|
||||
const query = paramsplit[key].split("=")
|
||||
console.log(query)
|
||||
|
||||
if (query.length !== 2) {
|
||||
console.log("INVALID QUERY: ", query)
|
||||
continue
|
||||
}
|
||||
|
||||
if (query[0] === "workflow_id") {
|
||||
appAuthData.reference_workflow = query[1]
|
||||
}
|
||||
|
||||
if (query[0] === "reference_action_id") {
|
||||
//appAuthData.ReferenceWorkflow = query[1]
|
||||
}
|
||||
|
||||
if (query[0] === "app_name") {
|
||||
appAuthData.app.name = query[1]
|
||||
appAuthData.label = "Oauth2 for "+query[1]
|
||||
}
|
||||
|
||||
if (query[0] === "app_id") {
|
||||
appAuthData.app.id = query[1]
|
||||
}
|
||||
|
||||
if (query[0] === "app_version") {
|
||||
appAuthData.app.app_version = query[1]
|
||||
}
|
||||
|
||||
if (query[0] === "authentication_url") {
|
||||
appAuthData.fields.push({"key": "authentication_url", "value": query[1]})
|
||||
}
|
||||
|
||||
if (query[0] === "scope") {
|
||||
appAuthData.fields.push({"key": "scope", "value": query[1]})
|
||||
}
|
||||
|
||||
if (query[0] === "client_id") {
|
||||
appAuthData.fields.push({"key": "client_id", "value": query[1]})
|
||||
}
|
||||
|
||||
if (query[0] === "client_secret") {
|
||||
appAuthData.fields.push({"key": "client_secret", "value": query[1]})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(appAuthData)
|
||||
|
||||
fetch(globalUrl+"/api/v1/apps/authentication", {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(appAuthData),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for oauth2 authentication")
|
||||
setFailed(true)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
//setUserSettings(responseJson)
|
||||
console.log("Resp: ", responseJson)
|
||||
setFinished(true)
|
||||
setResponse(responseJson.reason)
|
||||
|
||||
setTimeout(() => {
|
||||
window.close()
|
||||
}, 1000)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: 1000, margin: "auto", itemAlign: "center",}}>
|
||||
<Typography variant="h6" style={{marginLeft: "auto", marginRight: "auto", marginTop: 200, }}>
|
||||
{!finished ? <CircularProgress /> : "DONE WITH AUTH - this will close soon!!"}
|
||||
<div />
|
||||
{failed ? "Failed setup. Error: " : ""} {response}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SetAuthentication;
|
||||
@@ -354,6 +354,8 @@ const Workflows = (props) => {
|
||||
const [downloadUrl, setDownloadUrl] = React.useState("https://github.com/frikky/shuffle-workflows")
|
||||
const [downloadBranch, setDownloadBranch] = React.useState("master")
|
||||
const [loadWorkflowsModalOpen, setLoadWorkflowsModalOpen] = React.useState(false)
|
||||
const [exportModalOpen, setExportModalOpen] = React.useState(false)
|
||||
const [exportData, setExportData] = React.useState("");
|
||||
|
||||
const [modalOpen, setModalOpen] = React.useState(false);
|
||||
const [newWorkflowName, setNewWorkflowName] = React.useState("");
|
||||
@@ -473,6 +475,50 @@ const Workflows = (props) => {
|
||||
findWorkflow(newfilters)
|
||||
}
|
||||
|
||||
const exportVerifyModal = exportModalOpen ?
|
||||
<Dialog
|
||||
open={exportModalOpen}
|
||||
onClose={() => {
|
||||
setExportModalOpen(false)
|
||||
setSelectedWorkflow({})
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: surfaceColor,
|
||||
color: "white",
|
||||
minWidth: 500,
|
||||
padding: 30,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle style={{marginBottom: 0, }}>
|
||||
<div style={{textAlign: "center", color: "rgba(255,255,255,0.9)"}}>
|
||||
Want to auto-sanitize this workflow before exporting?
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent style={{color: "rgba(255,255,255,0.65)", textAlign: "center"}}>
|
||||
<Typography variant="body1" style={{}}>
|
||||
This will make potentially sensitive fields such as username, password, url etc. empty
|
||||
</Typography>
|
||||
<Button variant="contained" style={{marginTop: 20, }} onClick={() => {
|
||||
setExportModalOpen(false)
|
||||
exportWorkflow(exportData, true)
|
||||
setExportData("")
|
||||
}} color="primary">
|
||||
Yes
|
||||
</Button>
|
||||
<Button style={{marginTop: 20, }} onClick={() => {
|
||||
setExportModalOpen(false)
|
||||
exportWorkflow(exportData, false)
|
||||
setExportData("")
|
||||
}} color="primary">
|
||||
No
|
||||
</Button>
|
||||
</DialogContent>
|
||||
|
||||
</Dialog>
|
||||
: null
|
||||
|
||||
const publishModal = publishModalOpen ?
|
||||
<Dialog
|
||||
open={publishModalOpen}
|
||||
@@ -771,7 +817,7 @@ const Workflows = (props) => {
|
||||
|
||||
const exportAllWorkflows = () => {
|
||||
for (var key in workflows) {
|
||||
exportWorkflow(workflows[key])
|
||||
exportWorkflow(workflows[key], false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,10 +936,13 @@ const Workflows = (props) => {
|
||||
return data
|
||||
}
|
||||
|
||||
const exportWorkflow = (data) => {
|
||||
const exportWorkflow = (data, sanitize) => {
|
||||
data = JSON.parse(JSON.stringify(data))
|
||||
let exportFileDefaultName = data.name+'.json';
|
||||
|
||||
if (sanitize === true) {
|
||||
data = sanitizeWorkflow(data)
|
||||
}
|
||||
//return
|
||||
|
||||
// Add correct ID's for triggers
|
||||
@@ -1269,7 +1318,8 @@ const Workflows = (props) => {
|
||||
{"Duplicate Workflow"}
|
||||
</MenuItem>
|
||||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||||
exportWorkflow(data)
|
||||
setExportModalOpen(true)
|
||||
setExportData(data)
|
||||
setOpen(false)
|
||||
}} key={"export"}>
|
||||
<GetAppIcon style={{marginLeft: 0, marginRight: 8}}/>
|
||||
@@ -2323,6 +2373,7 @@ const Workflows = (props) => {
|
||||
</Dropzone>
|
||||
{modalView}
|
||||
{deleteModal}
|
||||
{exportVerifyModal}
|
||||
{publishModal}
|
||||
{workflowDownloadModalOpen}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user