diff --git a/frontend/src/components/ApiExplorer.jsx b/frontend/src/components/ApiExplorer.jsx index 45e6eab5..d1321220 100644 --- a/frontend/src/components/ApiExplorer.jsx +++ b/frontend/src/components/ApiExplorer.jsx @@ -1463,7 +1463,7 @@ const ActionsList = memo(({
0 ? openapi?.info["x-logo"] : theme?.palette?.defaultImage} width={48} height={48} alt="app logo" diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx index 8908590f..8c65366e 100644 --- a/frontend/src/components/AppAuthTab.jsx +++ b/frontend/src/components/AppAuthTab.jsx @@ -83,18 +83,17 @@ const AppAuthTab = memo((props) => { const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState(""); const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]); const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState(""); + const [selectedSubOrg, setSelectedSubOrg] = useState([]); const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState(""); const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]); const [searchQuery, setSearchQuery] = React.useState(""); const [showAppModal, setShowAppModal] = useState(false) + const [selectedAuthId, setSelectedAuthId] = useState(""); + const [showDistributionPopup, setShowDistributionPopup] = useState(false); const [showAuthenticationLoader, setShowAuthenticationLoader] = useState(true) const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true) const { themeMode, supportEmail, brandColor } = useContext(Context) const theme = getTheme(themeMode, brandColor) - const changeDistribution = (data) => { - //changeDistributed(data, !isDistributed) - editAuthenticationConfig(data.id, "suborg_distribute") - } useEffect(() => { getAppAuthentication(); @@ -214,11 +213,39 @@ const AppAuthTab = memo((props) => { }); }; - const editAuthenticationConfig = (id, parentAction) => { + const handleSelectSubOrg = (id, action) => { + if (action === "all") { + const childOrgs = userdata.orgs.filter( + (data) => data.creator_org === userdata.active_org.id + ); + setSelectedSubOrg((prev) => { + if (prev.length === childOrgs.length) { + // If all child orgs are already selected, clear the selection + return []; + } else { + // Otherwise, select all child org IDs + return childOrgs.map((data) => data.id); + } + }); + } else if (action === "none") { + setSelectedSubOrg([]); + } else { + setSelectedSubOrg((prev) => { + if (prev.includes(id)) { + return prev.filter((data) => data !== id); + } else { + return [...prev, id]; + } + }); + } + }; + + const editAuthenticationConfig = (id, parentAction, selectedSuborgs) => { const data = { id: id, action: parentAction !== undefined && parentAction !== null ? parentAction : "assign_everywhere", - } + selected_suborgs: selectedSuborgs !== undefined && selectedSuborgs !== null ? selectedSuborgs : [], + } const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; @@ -240,6 +267,7 @@ const AppAuthTab = memo((props) => { } else { toast("Successfully updated auth!"); setSelectedUserModalOpen(false); + setShowDistributionPopup(false); setTimeout(() => { getAppAuthentication(); }, 1000); @@ -251,6 +279,106 @@ const AppAuthTab = memo((props) => { }); }; + + const changeDistribution = (id, selectedSubOrg) => { + + editAuthenticationConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)]) + } + + + const cacheDistributionModal = showDistributionPopup ? ( + {setShowDistributionPopup(false);setSelectedAuthId("")}} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: "600px", + minHeight: "320px", + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + + Select sub-org to distribute Datastore key + + + + {handleSelectSubOrg(null, "none")}}>None + {handleSelectSubOrg(null, "all")}}>All + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + + const imagesize = 22; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: data.id === userdata.active_org.id ? 0 : 20, + }; + + const image = data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); + + return ( + handleSelectSubOrg(data.id)} + style={{ display: "flex", alignItems: "center" }} + > + + {image} + {data.name} + + ); + })} + +
+ + +
+
+
+ ) : null; + const editAuthenticationModal = selectedAuthenticationModalOpen ? ( { return (
{appModal} + {cacheDistributionModal}
@@ -1004,7 +1133,7 @@ const AppAuthTab = memo((props) => { ]; } - const isDistributed = data.suborg_distributed === true ? true : false; + const isDistributed = data?.suborg_distribution?.length > 0 || data?.suborg_distributed ? true : false; var validIcon = if (data.validation !== null && data.validation !== undefined && data.validation.valid === false) { @@ -1260,7 +1389,23 @@ const AppAuthTab = memo((props) => { style={{ }} color="secondary" onClick={() => { - changeDistribution(data, !isDistributed) + setShowDistributionPopup(true) + if(data?.suborg_distribution?.length > 0){ + setSelectedSubOrg(data.suborg_distribution) + }else{ + setSelectedSubOrg([]) + } + setSelectedAuthId(data.id) + if (data?.suborg_distributed) { + const allSuborg = userdata?.orgs?.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + return data.id; + }) + setSelectedSubOrg(allSuborg.filter((data) => data !== null)) + } + }} /> diff --git a/frontend/src/components/Branding.jsx b/frontend/src/components/Branding.jsx index 40a76846..f913a58a 100644 --- a/frontend/src/components/Branding.jsx +++ b/frontend/src/components/Branding.jsx @@ -33,7 +33,7 @@ const Branding = (props) => { //const alert = useAlert(); const [publishingInfo, setPublishingInfo] = useState(""); const [publishRequirements, setPublishRequirements] = useState([]) - const [currentSelectedTheme, setCurrentSelectedTheme] = useState("dark"); + const [currentSelectedTheme, setCurrentSelectedTheme] = useState(themeMode); const [integrationPartner, setIntegrationPartner] = useState(false); const [changingTheme, setChangingTheme] = useState(false); const theme = getTheme(themeMode, brandColor) @@ -190,6 +190,13 @@ const Branding = (props) => { }) }; + useEffect(() => { + if (userdata && userdata?.active_org && userdata?.active_org?.branding?.theme?.length > 0) { + console.log("Setting current selected theme from userdata", userdata?.active_org?.branding?.theme); + setCurrentSelectedTheme(userdata?.active_org?.branding?.theme); + } + },[userdata]); + // Should enable / disable org branding const handleChangePublishing = () => { console.log("Handle change publishing"); @@ -247,10 +254,6 @@ const Branding = (props) => { setCurrentSelectedTheme(userdata?.active_org?.branding.theme); } - if (userdata.support) { - setCurrentSelectedTheme(localStorage.getItem("theme") || "dark"); - } - if ( selectedOrganization && selectedOrganization?.branding?.brand_color && diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index 9c45df62..8502c81f 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -213,9 +213,16 @@ const CacheView = memo((props) => { const cache = { key: dataValue.key, value: value, - category: selectedCategory, + category: selectedCategory, } + if (listCache.length > 0) { + const selectedCache = listCache.find((data) => data.key === dataValue.key); + if (selectedCache?.suborg_distribution?.length > 0) { + cache.suborg_distribution = selectedCache.suborg_distribution; + } + } + setCacheInput([cache]); fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, { @@ -805,7 +812,7 @@ const CacheView = memo((props) => { }} > - Category (beta) + Category } diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index a156e4d6..dbd8d3ea 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -212,13 +212,7 @@ const EditWorkflow = (props) => { //minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, //maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, borderRadius: theme.palette.DialogStyle.borderRadius, - backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, - '& .MuiDialogContent-root': { - backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, - }, - '& .MuiDialogTitle-root': { - backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, - }, + backgroundColor: themeMode === "dark" ? "black" : theme?.palette?.DialogStyle?.backgroundColor, }, }} > @@ -299,10 +293,10 @@ const EditWorkflow = (props) => { right: 20, bottom: 0, zIndex: 1002, - backgroundColor: theme.palette.DialogStyle.backgroundColor, height: 75, paddingTop: 20, paddingLeft: 30, + backgroundColor: themeMode === "dark" ? "#262626" : theme.palette.DialogStyle.backgroundColor, }}>
+ {userdata && userdata?.active_org?.creator_org?.length > 0 ? null : +
+ + Skip SSO for Admins. When enabled, parent org admins will be able switch to the sub-organization without SSO. + +
+ +
+
+ }
{ defaults, sso_config, lead_info, - { mfa_required } = {} + { mfa_required } = {}, + editing, ) => { const data = { name: name, @@ -248,6 +249,7 @@ const Admin2 = (props) => { sso_config: sso_config, lead_info: lead_info, mfa_required: mfa_required !== undefined ? mfa_required : selectedOrganization?.mfa_required, + editing: editing?.length > 0 ? editing : "", }; const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index ad7aaf48..ee6d8831 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1973,7 +1973,7 @@ const AppCreator = (defaultprops) => { // Saving the app that's been configured. // Save SAVE app const submitApp = () => { - toast("Uploading and building app " + name); + toast.info("Uploading and building app " + name + ". This may take a minute or two."); setAppBuilding(true); setErrorCode(""); @@ -2600,15 +2600,23 @@ const AppCreator = (defaultprops) => { } if (setExtraAuth.length > 0) { + const invalidfieldnames = ["apikey", "username", "password", "username_basic", "password_basic", "url", "access_token"] for (let authkey in extraAuth) { const curauth = extraAuth[authkey]; - if (curauth.name.length === 0 || curauth.name.toLowerCase() == "url") { toast("Can't add extra auth with empty name or Name URL"); setAppBuilding(false); return; } + // Additional comparisonchecks + if (authenticationOption !== "No authentication" && authenticationOption !== "") { + if (invalidfieldnames.includes(curauth.name.toLowerCase())) { + toast.warn("Skipping extra auth field: " + curauth.name + ". This is not valid.") + continue + } + } + data.components.securitySchemes[curauth.name] = { type: "apiKey", in: curauth.type, @@ -2662,9 +2670,15 @@ const AppCreator = (defaultprops) => { if (responseJson.reason !== undefined) { setErrorCode(responseJson.reason); - toast.error("Failed to build: " + responseJson.reason, { - autoClose: 10000 - }) + if (responseJson.details !== undefined && responseJson.details !== null) { + toast.error("Failed to build - contact support@shuffler.io: " + responseJson.details, { + autoClose: 60000 + }) + } else { + toast.error("Failed to build: " + responseJson.reason, { + autoClose: 10000 + }) + } } } else { toast.success("Successfully built openapi app! Added job to rebuild it in your hybrid runtime locations (Orborus)."); @@ -2678,7 +2692,7 @@ const AppCreator = (defaultprops) => { .catch((error) => { setAppBuilding(false); setErrorCode(error.toString()); - toast(error.toString()); + toast.error(error.toString()); }); }; @@ -2810,10 +2824,21 @@ const AppCreator = (defaultprops) => { margin="normal" variant="outlined" defaultValue={extraAuth[index].name} + helperText={ + + {extraAuth[index]?.name?.toLowerCase() === "url" + || extraAuth[index]?.name?.toLowerCase() === "apikey" + ? `ERROR: Invalid key: ${extraAuth[index].name}. ` : ""} + + } onChange={(e) => { extraAuth[index].name = e.target.value; setExtraAuth(extraAuth); }} + onBlur={(e) => { + // Forcerender + setUpdate(Math.random()); + }} InputProps={{ classes: { notchedOutline: classes.notchedOutline, @@ -5865,7 +5890,9 @@ const AppCreator = (defaultprops) => {

- {name}{" "} + 0 ? `/apps/${props?.match?.params?.appid}` : "/apps" } style={{ textDecoration: "none", color: theme.palette.text.primary }}> + {name}{" "} + {actions === null || actions === undefined || actions.length === 0 ? null : ( diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx index 94bd3ed3..63c81d63 100644 --- a/frontend/src/views/AppExplorer.jsx +++ b/frontend/src/views/AppExplorer.jsx @@ -3121,12 +3121,12 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; style={{ marginBottom: 0, marginLeft: 0, marginRight: 0, minWidth: 800, maxWidth: 800, margin: "auto", }} aria-label="disabled tabs example" > - } label="Docs" /> - : } label={appType === 0 || appType === 2 ? "Explore the API" : "Try it out"} /> + } label="Docs" /> + : } label={appType === 0 || appType === 2 ? "Try the API" : "Try it out"} /> - } style={{color: theme.palette.text.primary}} label="Stats" /> - } disabled style={{color: theme.palette.text.secondary}} label="Integrations" /> - } disabled={userdata.support !== true} style={{color: userdata.support !== true ? theme.palette.text.secondary: theme.palette.text.primary}} label="Creator" value={4} /> + } style={{color: theme.palette.text.primary, textTransform: "none", }} label="Stats & Downloads" /> + } style={{color: theme.palette.text.primary, textTransform: "none", }} disabled style={{color: theme.palette.text.secondary}} label="Integrations" /> + } disabled={userdata.support !== true} style={{color: userdata.support !== true ? theme.palette.text.secondary: theme.palette.text.primary, textTransform: "none", }} label="Creator" value={4} />
{selectedTab === 1 && app.skipped_build == false ? ( @@ -3135,8 +3135,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
- {currentAction.description !== undefined && currentAction.description !== null && currentAction.description !== "" ? -
- Due to using docker containers with privately uploaded containers, we had to use a custom registry. Use the command below to download the image to the server if it fails to run. + Custom uses a custom Docker registry for private containers. Use the command below to download the app image to a server if it fails to do so automatically. - It will authenticate and authorize you, before redirecting to a Signed URL on https://storage.googleapis.com + The downloadable image may not be ready until 5 minutes after the app was built. -  Now also works for ARM containers! +  Now also works for ARM containers (?arch=arm)!
) : null} - {selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.id !== undefined && userdata.support === true ? + {selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.id !== undefined ? // Iconbutton for authentication with just an icon. Link to /apps/authentication?app_id=app.id - Explore API + Try the API