From f4f9e9052be93d1a88c49832a287b9aa5a2bc84b Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Tue, 9 Apr 2024 09:51:52 +0530 Subject: [PATCH] Integrate core config section --- src/api/tenants/index.ts | 136 +++++ src/assets/question-mark.svg | 5 + src/images.ts | 1 + src/ui/components/checkbox/Checkbox.tsx | 44 ++ src/ui/components/checkbox/checkbox.scss | 33 ++ .../tenantDetail/CoreConfigSection.tsx | 464 ++++++++++++------ .../tenantDetail/LoginMethodsSection.tsx | 75 +-- .../tenants/tenantDetail/TenantDetail.tsx | 4 +- .../tenants/tenantDetail/tenantDetail.scss | 240 ++++----- .../UneditablePropertyDialog.tsx | 42 ++ src/ui/styles/variables.css | 4 + 11 files changed, 719 insertions(+), 329 deletions(-) create mode 100644 src/assets/question-mark.svg create mode 100644 src/ui/components/checkbox/Checkbox.tsx create mode 100644 src/ui/components/checkbox/checkbox.scss create mode 100644 src/ui/components/tenants/tenantDetail/uneditablePropertyDialog/UneditablePropertyDialog.tsx diff --git a/src/api/tenants/index.ts b/src/api/tenants/index.ts index df7e5f36..c342e7f2 100644 --- a/src/api/tenants/index.ts +++ b/src/api/tenants/index.ts @@ -34,6 +34,14 @@ export const useTenantCreateService = () => { } | undefined > => { + // TODO: Temporary mock data + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return { + status: "OK", + createdNew: true, + }; + const response = await fetchData({ url: getApiUrl("/api/tenant"), method: "PUT", @@ -70,6 +78,72 @@ export const useTenantGetService = () => { } | undefined > => { + // TODO: Temporary mock data + await new Promise((resolve) => setTimeout(resolve, 10)); + + return { + status: "OK", + tenant: { + tenantId, + thirdParty: { + providers: [], + }, + firstFactors: [], + requiredSecondaryFactors: [], + userCount: 12, + coreConfig: [ + { + key: "password_reset_token_lifetime", + valueType: "number", + value: 3600000, + description: "The time in milliseconds for which the password reset token is valid.", + isSaaSProtected: false, + isDifferentAcrossTenants: true, + isModifyableOnlyViaConfigYaml: false, + defaultValue: 3600000, + isNullable: false, + isPluginProperty: false, + }, + { + key: "access_token_blacklisting", + valueType: "boolean", + value: false, + description: "Whether to blacklist access tokens or not.", + isSaaSProtected: false, + isDifferentAcrossTenants: true, + isModifyableOnlyViaConfigYaml: false, + defaultValue: false, + isNullable: false, + isPluginProperty: false, + }, + { + key: "ip_allow_regex", + valueType: "string", + value: null, + description: "The regex to match the IP address of the user.", + isSaaSProtected: false, + isDifferentAcrossTenants: true, + isModifyableOnlyViaConfigYaml: false, + defaultValue: null, + isNullable: true, + isPluginProperty: false, + }, + { + key: "postgresql_emailpassword_users_table_name", + valueType: "string", + value: null, + description: "The name of the table where the emailpassword users are stored.", + isSaaSProtected: false, + isDifferentAcrossTenants: true, + isModifyableOnlyViaConfigYaml: false, + defaultValue: 3600000, + isNullable: true, + isPluginProperty: true, + }, + ], + }, + }; + const response = await fetchData({ url: getApiUrl("/api/tenant", tenantId), method: "GET", @@ -95,6 +169,13 @@ export const useTenantDeleteService = () => { method: "DELETE", }); + // TODO: Temporary mock data + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return { + status: "OK", + }; + if (response.ok) { return { status: "OK", @@ -119,6 +200,13 @@ export const useUpdateFirstFactorsService = () => { | { status: "RECIPE_NOT_CONFIGURED_ON_BACKEND_SDK"; message: string } | { status: "UNKNOWN_TENANT_ERROR" } > => { + // TODO: Temporary mock data + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return { + status: "OK", + }; + const response = await fetchData({ url: getApiUrl("/api/tenant/first-factor", tenantId), method: "PUT", @@ -156,6 +244,13 @@ export const useUpdateSecondaryFactorsService = () => { | { status: "MFA_REQUIREMENTS_FOR_AUTH_OVERRIDDEN" } | { status: "UNKNOWN_TENANT_ERROR" } > => { + // TODO: Temporary mock data + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return { + status: "OK", + }; + const response = await fetchData({ url: getApiUrl("/api/tenant/secondary-factor", tenantId), method: "PUT", @@ -179,6 +274,47 @@ export const useUpdateSecondaryFactorsService = () => { return updateSecondaryFactors; }; +export const useUpdateCoreConfigService = () => { + const fetchData = useFetchData(); + + const updateCoreConfig = async ( + tenantId: string, + name: string, + value: string | number | boolean | null + ): Promise< + { status: "OK" } | { status: "UNKNOWN_TENANT_ERROR" } | { status: "INVALID_CONFIG"; message: string } + > => { + // TODO: Temporary mock data + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return { + status: "INVALID_CONFIG", + message: "Invalid config", + }; + + const response = await fetchData({ + url: getApiUrl("/api/tenant/core-config", tenantId), + method: "PUT", + config: { + body: JSON.stringify({ + name, + value, + }), + }, + }); + + if (response.ok) { + return { + status: "OK", + }; + } + + throw new Error("Unknown error"); + }; + + return updateCoreConfig; +}; + export const useThirdPartyService = () => { const fetchData = useFetchData(); diff --git a/src/assets/question-mark.svg b/src/assets/question-mark.svg new file mode 100644 index 00000000..6b622b8e --- /dev/null +++ b/src/assets/question-mark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/images.ts b/src/images.ts index 3ef0c7c7..7638bdc6 100644 --- a/src/images.ts +++ b/src/images.ts @@ -73,6 +73,7 @@ import "./assets/provider-google.svg"; import "./assets/provider-linkedin.svg"; import "./assets/provider-okta.png"; import "./assets/provider-twitter.svg"; +import "./assets/question-mark.svg"; import "./assets/refresh.svg"; import "./assets/right_arrow_icon.svg"; import "./assets/roles-and-permissions.svg"; diff --git a/src/ui/components/checkbox/Checkbox.tsx b/src/ui/components/checkbox/Checkbox.tsx new file mode 100644 index 00000000..ddf55510 --- /dev/null +++ b/src/ui/components/checkbox/Checkbox.tsx @@ -0,0 +1,44 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { ChangeEvent } from "react"; +import "./checkbox.scss"; + +export const Checkbox = ({ + id, + label, + onChange, + checked, + disabled, +}: { + id: string; + label: string; + onChange: (e: ChangeEvent) => void; + checked: boolean; + disabled?: boolean; +}) => { + return ( +
+ + +
+ ); +}; diff --git a/src/ui/components/checkbox/checkbox.scss b/src/ui/components/checkbox/checkbox.scss new file mode 100644 index 00000000..a969bee3 --- /dev/null +++ b/src/ui/components/checkbox/checkbox.scss @@ -0,0 +1,33 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +.checkbox-container { + display: flex; + gap: 6px; + align-items: center; + + label { + font-family: inherit; + font-size: 12px; + font-weight: 500; + line-height: 14px; + color: var(--color-secondary); + } + + &--disabled { + opacity: 0.6; + cursor: not-allowed; + } +} diff --git a/src/ui/components/tenants/tenantDetail/CoreConfigSection.tsx b/src/ui/components/tenants/tenantDetail/CoreConfigSection.tsx index 85d78275..6f58c325 100644 --- a/src/ui/components/tenants/tenantDetail/CoreConfigSection.tsx +++ b/src/ui/components/tenants/tenantDetail/CoreConfigSection.tsx @@ -12,78 +12,26 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; +import { useUpdateCoreConfigService } from "../../../../api/tenants"; +import { ReactComponent as PencilIcon } from "../../../../assets/edit.svg"; import { ReactComponent as InfoIcon } from "../../../../assets/info-icon.svg"; -import { ReactComponent as RightArrow } from "../../../../assets/right_arrow_icon.svg"; -import { PopupContentContext } from "../../../contexts/PopupContentContext"; +import { ReactComponent as QuestionMarkIcon } from "../../../../assets/question-mark.svg"; +import { PUBLIC_TENANT_ID } from "../../../../constants"; +import Button from "../../button"; +import { Checkbox } from "../../checkbox/Checkbox"; import InputField from "../../inputField/InputField"; +import { NativeSelect } from "../../nativeSelect/NativeSelect"; import { Toggle } from "../../toggle/Toggle"; import TooltipContainer from "../../tooltip/tooltip"; import { useTenantDetailContext } from "./TenantDetailContext"; -import { - PanelHeader, - PanelHeaderAction, - PanelHeaderTitleWithTooltip, - PanelRoot, -} from "./tenantDetailPanel/TenantDetailPanel"; +import { PanelHeader, PanelHeaderTitleWithTooltip, PanelRoot } from "./tenantDetailPanel/TenantDetailPanel"; +import { UneditablePropertyDialog } from "./uneditablePropertyDialog/UneditablePropertyDialog"; export const CoreConfigSection = () => { - const [isEditing, setIsEditing] = useState(false); - const [isSavingProperties, setIsSavingProperties] = useState(false); - const { tenantInfo, refetchTenant } = useTenantDetailContext(); - const { showToast } = useContext(PopupContentContext); - const [currentConfig, setCurrentConfig] = useState(tenantInfo?.coreConfig ?? {}); - const [configErrors, setConfigErrors] = useState>({}); - const hasProperties = Object.keys(tenantInfo?.coreConfig ?? {}).length > 0; + const { tenantInfo } = useTenantDetailContext(); - // Ensure that the state reflects the latest core config - useEffect(() => { - setCurrentConfig(tenantInfo.coreConfig); - }, [tenantInfo.coreConfig]); - - const handleSave = async () => { - // const errors = Object.entries(currentConfig).reduce((acc: Record, [key, value]) => { - // const propertyObj = coreConfigOptions.find((property) => property.name === key); - // if (value === "" || value === undefined) { - // acc[key] = "Value cannot be empty"; - // return acc; - // } - // if (propertyObj?.type === "number" && isNaN(Number(value))) { - // acc[key] = "Value must be a number"; - // return acc; - // } - // return acc; - // }, {}); - // setConfigErrors(errors); - // if (Object.keys(errors).length > 0) { - // return; - // } - // try { - // const parsedConfig = Object.entries(currentConfig).reduce((acc: Record, [key, value]) => { - // const propertyObj = coreConfigOptions.find((property) => property.name === key); - // if (propertyObj?.type === "number") { - // acc[key] = Number(value); - // } else { - // acc[key] = value; - // } - // return acc; - // }, {}); - // setIsSavingProperties(true); - // await updateTenant(tenantInfo.tenantId, { - // coreConfig: parsedConfig, - // }); - // setIsEditing(false); - // await refetchTenant(); - // } catch (_) { - // showToast({ - // iconImage: getImageUrl("form-field-error-icon.svg"), - // toastType: "error", - // children: <>Something went wrong!, Failed to save config, - // }); - // } finally { - // setIsSavingProperties(false); - // } - }; + const hasPluginProperties = tenantInfo.coreConfig.some((config) => config.isPluginProperty); return ( @@ -91,56 +39,62 @@ export const CoreConfigSection = () => { Core Config - {hasProperties && ( - - )} - {!hasProperties ? ( -
-
No Property Added
-

- There are no core config properties added by you for this tenant. You can click below to add new - property or{" "} - - click here - {" "} - to see the list of all available core config property options. -

-
- ) : ( -
-
-
Property name
-
Value
-
-
- {Object.entries(currentConfig).map(([name, value]) => { - const propertyObj = undefined; - if (propertyObj === undefined) { - return null; - } - return <>; - })} -
-
- )} -
- {hasProperties && ( - - See all config properties - + +
+ {tenantInfo.coreConfig + .filter((config) => !config.isPluginProperty) + .map((config) => { + return ( + + ); + })} + {hasPluginProperties && ( + <> +
+

+ Plugin Properties +

+
+

+ These properties cannot be directly modified from the UI, instead you can make API + request to core to modify these properties. Click here to see an example. +

+
+ {tenantInfo.coreConfig + .filter((config) => config.isPluginProperty) + .map((config) => { + return ( + + ); + })} + )}
@@ -149,62 +103,254 @@ export const CoreConfigSection = () => { type CoreConfigTableRowProps = { name: string; - value: string | number | boolean; - type: "string" | "boolean" | "number" | "enum"; - handleChange: (name: string, newValue: string | number | boolean) => void; - isEditing: boolean; - tooltip?: string; - error?: string; + value: string | number | boolean | null; + type: "string" | "boolean" | "number"; + isNullable: boolean; + tooltip: string; + defaultValue: string | number | boolean | null; + possibleValues?: string[]; + isSaaSProtected: boolean; + isDifferentAcrossTenants: boolean; + isModifyableOnlyViaConfigYaml: boolean; + isPluginProperty: boolean; }; +const isUsingSaaS = false; +const isUsingNonPublicApp = false; + const CoreConfigTableRow = ({ name, value, tooltip, type, - isEditing, - handleChange, - error, + isNullable, + defaultValue, + possibleValues, + isSaaSProtected, + isDifferentAcrossTenants, + isModifyableOnlyViaConfigYaml, + isPluginProperty, }: CoreConfigTableRowProps) => { - return ( -
-
- {name} - {tooltip && ( - - - - )} + const [isEditing, setIsEditing] = useState(false); + const [currentValue, setCurrentValue] = useState(value); + const [error, setError] = useState(null); + const { tenantInfo, refetchTenant } = useTenantDetailContext(); + const updateCoreConfig = useUpdateCoreConfigService(); + const [isLoading, setIsLoading] = useState(false); + const [isUneditablePropertyDialogVisible, setIsUneditablePropertyDialogVisible] = useState(false); + const isMultiValue = Array.isArray(possibleValues) && possibleValues.length > 0; + const isPublicTenant = tenantInfo.tenantId === PUBLIC_TENANT_ID; + + const isUneditable = + isPublicTenant || + isPluginProperty || + isModifyableOnlyViaConfigYaml || + (isUsingSaaS && isSaaSProtected) || + (!isPublicTenant && !isDifferentAcrossTenants); + + // Keep the state in sync with the prop value + useEffect(() => { + setCurrentValue(value); + }, [value]); + + const toggleNull = () => { + if (currentValue === null) { + setCurrentValue(type === "number" ? 0 : ""); + } else { + setCurrentValue(null); + } + }; + + const handleCancelEdit = () => { + setIsEditing(false); + setCurrentValue(value); + setError(null); + }; + + const handleSaveProperty = async () => { + try { + setIsLoading(true); + setError(null); + const res = await updateCoreConfig(tenantInfo.tenantId, name, currentValue); + if (res.status !== "OK") { + if (res.status === "UNKNOWN_TENANT_ERROR") { + setError("Tenant not found."); + } else { + setError(res.message); + } + return; + } + await refetchTenant(); + setIsEditing(false); + } catch (e) { + setError("Something went wrong. Please try again."); + } finally { + setIsLoading(false); + } + }; + + const renderConfigAction = () => { + if (isUneditable) { + return null; + } + + if (!isEditing) { + return ( + + ); + } + + return ( +
+ +
-
- {(type === "string" || type === "number") && - (isEditing ? ( - { - if (e.type === "change") { - handleChange(name, e.currentTarget.value); - } - }} - value={`${value}`} - /> - ) : ( -
{value}
- ))} - {typeof value === "boolean" && type === "boolean" && ( - handleChange(name, !value)} - /> + ); + }; + + const renderUneditablePropertyReason = () => { + if (isPluginProperty) { + return "This property is a plugin property and cannot be modified from the UI. Checkout the description for this section for more details."; + } + + if (isModifyableOnlyViaConfigYaml && isUsingSaaS) { + return "This property cannot be modified since you are using the managed service."; + } + + if (isSaaSProtected && isUsingSaaS) { + return "This property cannot be edited or viewed since you are using a managed service and we hide it for security reasons."; + } + + if ((isPublicTenant && !isUsingNonPublicApp) || isModifyableOnlyViaConfigYaml) { + return isUsingSaaS + ? "You can modify this property via the SaaS dashboard." + : "This property is modifyable only via the config.yaml file."; + } + + if (isUsingNonPublicApp && isPublicTenant) { + return ( + <> + You would need to use{" "} + + this core API + {" "} + to update this property. + + ); + } + + return isUsingSaaS + ? "You can modify this property via the SaaS dashboard." + : "This property is modifyable only via the config.yaml file."; + }; + + return ( + <> +
+
+
+
+ {tooltip && ( + + + + )} + {name} +
+ {renderConfigAction()} +
+
+ +
+ {isMultiValue && ( + { + setCurrentValue(e.target.value); + }} + /> + )} + + {(type === "string" || type === "number") && !isMultiValue && ( + { + setCurrentValue(e.target.value); + }} + error={error ?? undefined} + forceShowError + value={currentValue === null ? "[null]" : `${currentValue}`} + /> + )} + + {typeof currentValue === "boolean" && type === "boolean" && ( + { + setCurrentValue(!currentValue); + }} + /> + )} + {isNullable && ( + + )} +
+
+
+ {isUneditable && ( + )}
-
+ {isUneditablePropertyDialogVisible && ( + setIsUneditablePropertyDialogVisible(false)}> + {renderUneditablePropertyReason()} + + )} + ); }; diff --git a/src/ui/components/tenants/tenantDetail/LoginMethodsSection.tsx b/src/ui/components/tenants/tenantDetail/LoginMethodsSection.tsx index 7757fe4a..5606b1c6 100644 --- a/src/ui/components/tenants/tenantDetail/LoginMethodsSection.tsx +++ b/src/ui/components/tenants/tenantDetail/LoginMethodsSection.tsx @@ -29,6 +29,13 @@ export const LoginMethodsSection = () => { const { tenantInfo, setTenantInfo } = useTenantDetailContext(); const updateFirstFactors = useUpdateFirstFactorsService(); const updateSecondaryFactors = useUpdateSecondaryFactorsService(); + const [selectedFactors, setSelectedFactors] = useState<{ + firstFactors: Array; + requiredSecondaryFactors: Array; + }>({ + firstFactors: tenantInfo.firstFactors ?? [], + requiredSecondaryFactors: tenantInfo.requiredSecondaryFactors ?? [], + }); const [isFirstFactorsLoading, setIsFirstFactorsLoading] = useState(false); const [isSecondaryFactorsLoading, setIsSecondaryFactorsLoading] = useState(false); @@ -51,38 +58,22 @@ export const LoginMethodsSection = () => { !tenantInfo.firstFactors?.includes(FactorIds.THIRDPARTY); const handleFactorChange = async (factorKey: "firstFactors" | "requiredSecondaryFactors", id: string) => { - const prevFactors = tenantInfo[factorKey] ?? []; - let newFactors = [...prevFactors]; - const doesFactorExist = prevFactors.includes(id); + const prevFactors = selectedFactors; + const newFactors = { ...prevFactors }; + const doesFactorExist = newFactors[factorKey].includes(id); if (doesFactorExist) { - newFactors = newFactors.filter((factor) => factor !== id); + newFactors[factorKey] = newFactors[factorKey].filter((factor) => factor !== id); } else { - newFactors = [...newFactors, id]; + newFactors[factorKey] = [...newFactors[factorKey], id]; } // Optimistically update the state for better UX - setTenantInfo((prev) => - prev - ? { - ...prev, - [factorKey]: newFactors, - } - : undefined - ); + setSelectedFactors(newFactors); try { if (factorKey === "firstFactors") { setIsFirstFactorsLoading(true); const res = await updateFirstFactors(tenantInfo.tenantId, id, !doesFactorExist); if (res.status !== "OK") { - // We revert the state in case of a non-OK response - setTenantInfo((prev) => - prev - ? { - ...prev, - firstFactors: prevFactors, - } - : undefined - ); - + setSelectedFactors(prevFactors); if (res.status === "RECIPE_NOT_CONFIGURED_ON_BACKEND_SDK") { setFactorErrors((prev) => ({ ...prev, @@ -91,6 +82,16 @@ export const LoginMethodsSection = () => { } else { throw new Error(res.status); } + } else { + // Update the tenantInfo state + setTenantInfo((prev) => + prev + ? { + ...prev, + firstFactors: newFactors.firstFactors, + } + : undefined + ); } } else { setIsSecondaryFactorsLoading(true); @@ -111,19 +112,23 @@ export const LoginMethodsSection = () => { } // We allow users to update secondary factors even if - // getMFARequirementsForAuth is overridden + // getMFARequirementsForAuth is overridden, for rest of + // cases we revert the state if (res.status !== "MFA_REQUIREMENTS_FOR_AUTH_OVERRIDDEN") { - // We revert the state in case of a non-OK response - setTenantInfo((prev) => - prev - ? { - ...prev, - requiredSecondaryFactors: prevFactors, - } - : undefined - ); + setSelectedFactors(prevFactors); } } + + if (res.status === "OK" || res.status === "MFA_REQUIREMENTS_FOR_AUTH_OVERRIDDEN") { + setTenantInfo((prev) => + prev + ? { + ...prev, + requiredSecondaryFactors: newFactors.requiredSecondaryFactors, + } + : undefined + ); + } } } catch (error) { showToast({ @@ -174,7 +179,7 @@ export const LoginMethodsSection = () => { disabled={isFirstFactorsLoading} description={method.description} error={factorErrors.firstFactors[method.id]} - checked={tenantInfo.firstFactors.includes(method.id)} + checked={selectedFactors.firstFactors.includes(method.id)} onChange={() => handleFactorChange("firstFactors", method.id)} /> ))} @@ -218,7 +223,7 @@ export const LoginMethodsSection = () => { fixedGap description={method.description} error={factorErrors.requiredSecondaryFactors[method.id]} - checked={tenantInfo.requiredSecondaryFactors?.includes(method.id) ?? false} + checked={selectedFactors.requiredSecondaryFactors.includes(method.id)} onChange={() => handleFactorChange("requiredSecondaryFactors", method.id)} /> ))} diff --git a/src/ui/components/tenants/tenantDetail/TenantDetail.tsx b/src/ui/components/tenants/tenantDetail/TenantDetail.tsx index 4f717842..8a3c8b0d 100644 --- a/src/ui/components/tenants/tenantDetail/TenantDetail.tsx +++ b/src/ui/components/tenants/tenantDetail/TenantDetail.tsx @@ -77,7 +77,7 @@ export const TenantDetail = ({ ) { setIsNoProviderAddedDialogVisible(true); } - }, [tenant, tenantHasThirdPartyEnabled]); + }, [tenantHasThirdPartyEnabled, tenant?.tenantId, tenant?.thirdParty.providers.length]); const refetchTenant = async () => { setShowLoadingOverlay(true); @@ -137,7 +137,7 @@ export const TenantDetail = ({ handleEditProvider={handleEditProvider} /> )} - {tenant?.tenantId !== PUBLIC_TENANT_ID && } +
{tenant?.tenantId !== PUBLIC_TENANT_ID && (
diff --git a/src/ui/components/tenants/tenantDetail/tenantDetail.scss b/src/ui/components/tenants/tenantDetail/tenantDetail.scss index e92ec0e7..d93b128b 100644 --- a/src/ui/components/tenants/tenantDetail/tenantDetail.scss +++ b/src/ui/components/tenants/tenantDetail/tenantDetail.scss @@ -54,180 +54,154 @@ margin-top: 20px; } - &__no-config-info-block { - a { - font-weight: 500; + &__core-config-table { + margin-top: 24px; + width: 100%; + display: flex; + flex-direction: column; + gap: 12px; + + &__plugin-properties-container { + margin-top: 8px; } - &__no-property-pill { - background: var(--color-info-pill-bg); - border-radius: 8px; - padding: 1px 8px; - color: white; - font-size: 12px; + &__plugin-propertier-header { + font-size: 16px; font-weight: 500; - line-height: 15px; - text-transform: uppercase; - width: fit-content; - margin-bottom: 12px; + line-height: 30px; + font-family: inherit; + color: var(--color-black); } - } - - &__core-config-table { - margin-top: 32px; - padding: 24px; - border: solid 1px var(--color-border-command); - border-radius: 6px; - width: 100%; - @media screen and (max-width: 480px) { - padding: 0px; + &__plugin-properties-divider { + display: block; + height: 1px; + background-color: var(--color-border); + margin: 10px 0px; border: none; - margin-top: 20px; } - &__header { - display: flex; - margin-bottom: 16px; - border-bottom: solid 1px var(--color-border-command); - padding-bottom: 16px; - padding-left: 24px; + &__plugin-properties-description { + font-size: 14px; + line-height: 23px; + font-family: inherit; + color: var(--color-secondary-text); + } - &__item { - font-size: 14px; - color: var(--color-secondary-text); - font-weight: 500; - line-height: normal; - text-transform: uppercase; - flex: 1; + &__row { + position: relative; + } + + &__row-container { + background-color: var(--color-config-bg); + border-radius: 6px; + padding: 10px 24px; + border: 1px solid var(--color-secondary); + width: 100%; + + &--editing { + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.15); } - @media screen and (max-width: 480px) { - display: none; + &--uneditable { + opacity: 0.6; } } - &__body { + &__row-info { display: flex; - flex-direction: column; - gap: 16px; - margin-top: 20px; - - @media screen and (max-width: 480px) { - margin-top: 0px; - } + align-items: center; + justify-content: space-between; + width: 100%; } - &__row { + &__row-name { display: flex; + gap: 10px; align-items: center; - padding: 10px 24px; - height: 40px; - border-radius: 6px; - background: var(--color-input-unfocused); + padding: 4px 10px; + border-radius: 3px; + background-color: var(--color-config-name-bg); font-size: 14px; - color: black; + line-height: 19px; font-weight: 500; - line-height: normal; + color: var(--color-config-property-label); - @media screen and (max-width: 480px) { - flex-direction: column; - align-items: flex-start; - justify-content: space-between; - height: 75px; + path { + fill: var(--color-config-property-label); } + } - &__label { - display: flex; - align-items: center; - flex: 1; - gap: 9px; - } - - &__value { - flex: 1; - padding-left: 18px; - display: flex; - justify-content: space-between; - align-items: center; - - @media screen and (max-width: 480px) { - padding-left: 0px; - width: 100%; - } + &__row-edit-button-container { + background-color: white; + border-radius: 4px; + padding: 8px; + border: 1px solid var(--color-secondary); - &__text { - max-width: 195px; - padding: 3px 8px; - background: var(--color-copy-box-bg); - color: var(--color-copy-box); - font-size: 13px; - font-family: Menlo, "Source Code Pro", Monaco, Consolas, "Courier New", monospace; - border-radius: 3px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + svg { + width: 11px; + height: 12px; } - &__delete-property { - padding: 6px; - border-radius: 3px; - border: 1px solid var(--color-border-command); - background: white; - - & > svg > path { - fill: var(--color-trash-button) !important; - } - - &:hover { - border: 1px solid var(--color-trash-button-hover); - background: var(--color-trash-button-hover-bg); - - & > svg > path { - fill: var(--color-trash-button-hover) !important; - } - } + &:hover { + cursor: pointer; + background-color: var(--color-config-action-hover-bg); } } - } - &__core-config-footer { - display: flex; - width: 100%; - justify-content: space-between; - margin-top: 16px; - align-items: center; + &__row-uneditable-button-container { + position: absolute; + right: 24px; + top: 10px; + background-color: white; + border-radius: 4px; + padding: 7px; + border: 1px solid rgba(128, 188, 255, 1); - @media screen and (max-width: 480px) { - flex-direction: column; - gap: 16px; - align-items: stretch; - button { - justify-content: center; + svg { + width: 13px; + height: 13px; + } + + &:hover { + cursor: pointer; + background-color: var(--color-config-action-hover-bg); } } - &__footer-link { - font-size: 14px; - font-weight: 500; - color: var(--color-link); + &__row-buttons { display: flex; + gap: 10px; align-items: center; - cursor: pointer; - gap: 6px; - border-bottom: 1px solid transparent !important; - text-decoration: none; + } - &:hover { - border-bottom: 1px solid var(--color-link) !important; - background-color: transparent; + &__row-value-container { + margin-top: 14px; + display: flex; + gap: 9px; + + > label { + font-family: inherit; + font-weight: 500; + font-size: 14px; + line-height: 19px; + color: var(--color-black); + transform: translateY(4px); } - & > svg > path { - fill: var(--color-link) !important; + &--toggle { + align-items: center; + > label { + transform: translateY(0); + } } } + + &__row-field-container { + display: flex; + flex-direction: column; + gap: 8px; + } } &__header.panel { diff --git a/src/ui/components/tenants/tenantDetail/uneditablePropertyDialog/UneditablePropertyDialog.tsx b/src/ui/components/tenants/tenantDetail/uneditablePropertyDialog/UneditablePropertyDialog.tsx new file mode 100644 index 00000000..688caaf3 --- /dev/null +++ b/src/ui/components/tenants/tenantDetail/uneditablePropertyDialog/UneditablePropertyDialog.tsx @@ -0,0 +1,42 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import Button from "../../../button"; +import { Dialog, DialogConfirmText, DialogContent, DialogFooter } from "../../../dialog"; + +export const UneditablePropertyDialog = ({ + onCloseDialog, + children, +}: { + onCloseDialog: () => void; + children: React.ReactNode; +}) => { + return ( + + + {children} + + + + + + + ); +}; diff --git a/src/ui/styles/variables.css b/src/ui/styles/variables.css index c63dcd69..16c1c308 100644 --- a/src/ui/styles/variables.css +++ b/src/ui/styles/variables.css @@ -54,6 +54,9 @@ body { --color-input-field-prefix-bg: rgb(250, 250, 250); --color-error-block-bg: rgba(255, 213, 213); --color-info-box-bg: rgba(246, 246, 246); + --color-config-bg: rgba(240, 244, 255); + --color-config-name-bg: rgba(147, 176, 255, 0.33); + --color-config-action-hover-bg: rgba(128, 188, 255, 0.4); /* Border Colors */ --color-border: rgb(229, 229, 229); @@ -76,6 +79,7 @@ body { --color-disabled: rgb(125, 125, 125); --color-transparent-button: rgb(136, 136, 136); --color-info-box-header: rgb(83, 83, 83); + --color-config-property-label: rgba(74, 91, 135, 1); /* Recipe Pill Colors */ --color-emailpassword-bg: rgb(221, 252, 247);