From d18422397e72c748eee703f2f2302004fbf36d8b Mon Sep 17 00:00:00 2001 From: simeng-li Date: Thu, 5 Dec 2024 15:35:52 +0800 Subject: [PATCH] feat(console): add SAML IdP settings page (#6853) * feat(console): add SAML IdP settings page add SAML IdP application settings page * feat(console): add download link add download link --- .../console/src/assets/icons/download.svg | 3 + .../ApplicationDetailsContent/index.tsx | 5 +- .../CertificateActionMenu/index.module.scss | 3 + .../CertificateActionMenu/index.tsx | 39 ++++ .../Settings.tsx | 206 ++++++++++++++++++ .../index.module.scss | 32 +++ .../SamlApplicationDetailsContent/index.tsx | 171 +++++++++++++++ .../use-secret-table-columns.tsx | 85 ++++++++ .../SamlApplicationDetailsContent/utils.ts | 52 +++++ .../Branding/NonThirdPartyBrandingForm.tsx | 0 .../Branding/index.module.scss | 0 .../Branding/index.tsx | 0 .../use-application-sign-in-experience-swr.ts | 0 .../Branding/use-sign-in-experience-swr.ts | 0 .../Branding/utils.ts | 0 .../constants.ts | 0 .../index.tsx | 0 .../ApplicationScopesAssignmentModal/type.ts | 0 .../use-application-scopes-assignment.ts | 0 .../use-organization-scopes-assignment.ts | 0 .../use-resource-scopes-assignment.ts | 0 .../use-user-scopes-assignment.ts | 0 .../index.tsx | 0 .../PermissionsCard/index.module.scss | 0 .../Permissions/PermissionsCard/index.tsx | 0 .../PermissionsCard/use-scopes-table.tsx | 0 .../Permissions/index.module.scss | 0 .../Permissions/index.tsx | 0 .../src/pages/ApplicationDetails/index.tsx | 22 +- packages/console/src/utils/downloader.ts | 15 ++ .../admin-console/application-details.ts | 16 ++ .../en/translation/admin-console/general.ts | 1 + 32 files changed, 640 insertions(+), 10 deletions(-) create mode 100644 packages/console/src/assets/icons/download.svg create mode 100644 packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/CertificateActionMenu/index.module.scss create mode 100644 packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/CertificateActionMenu/index.tsx create mode 100644 packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/Settings.tsx create mode 100644 packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/index.module.scss create mode 100644 packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/index.tsx create mode 100644 packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/use-secret-table-columns.tsx create mode 100644 packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/utils.ts rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Branding/NonThirdPartyBrandingForm.tsx (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Branding/index.module.scss (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Branding/index.tsx (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Branding/use-application-sign-in-experience-swr.ts (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Branding/use-sign-in-experience-swr.ts (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Branding/utils.ts (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/constants.ts (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/index.tsx (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/type.ts (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-application-scopes-assignment.ts (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-organization-scopes-assignment.ts (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-resource-scopes-assignment.ts (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-user-scopes-assignment.ts (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/ApplicationScopesManagementModal/index.tsx (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/index.module.scss (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/index.tsx (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/PermissionsCard/use-scopes-table.tsx (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/index.module.scss (100%) rename packages/console/src/pages/ApplicationDetails/{ApplicationDetailsContent => components}/Permissions/index.tsx (100%) create mode 100644 packages/console/src/utils/downloader.ts diff --git a/packages/console/src/assets/icons/download.svg b/packages/console/src/assets/icons/download.svg new file mode 100644 index 000000000..96df110d5 --- /dev/null +++ b/packages/console/src/assets/icons/download.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx index 4c3485f65..45a62eabe 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx @@ -30,13 +30,14 @@ import useTenantPathname from '@/hooks/use-tenant-pathname'; import { applicationTypeI18nKey } from '@/types/applications'; import { trySubmitSafe } from '@/utils/form'; +import Branding from '../components/Branding'; +import Permissions from '../components/Permissions'; + import BackchannelLogout from './BackchannelLogout'; -import Branding from './Branding'; import EndpointsAndCredentials, { type ApplicationSecretRow } from './EndpointsAndCredentials'; import GuideDrawer from './GuideDrawer'; import MachineLogs from './MachineLogs'; import MachineToMachineApplicationRoles from './MachineToMachineApplicationRoles'; -import Permissions from './Permissions'; import RefreshTokenSettings from './RefreshTokenSettings'; import Settings from './Settings'; import styles from './index.module.scss'; diff --git a/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/CertificateActionMenu/index.module.scss b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/CertificateActionMenu/index.module.scss new file mode 100644 index 000000000..ffa4d6683 --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/CertificateActionMenu/index.module.scss @@ -0,0 +1,3 @@ +.icon { + color: var(--color-text-secondary); +} diff --git a/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/CertificateActionMenu/index.tsx b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/CertificateActionMenu/index.tsx new file mode 100644 index 000000000..4368c51fb --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/CertificateActionMenu/index.tsx @@ -0,0 +1,39 @@ +import { type SamlApplicationSecretResponse } from '@logto/schemas'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import Download from '@/assets/icons/download.svg?react'; +import More from '@/assets/icons/more.svg?react'; +import ActionMenu, { ActionMenuItem } from '@/ds-components/ActionMenu'; +import { downloadText } from '@/utils/downloader'; + +import { buildSamlSigningCertificateFilename } from '../utils'; + +import styles from './index.module.scss'; + +type Props = { + readonly appId: string; + readonly secret: SamlApplicationSecretResponse; +}; + +function CertificateActionMenu({ secret: { id, certificate }, appId }: Props) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const onDownload = useCallback(() => { + downloadText( + certificate, + buildSamlSigningCertificateFilename(appId, id), + 'application/x-x509-ca-cert' + ); + }, [appId, certificate, id]); + + return ( + } title={t('general.more_options')}> + } onClick={onDownload}> + {t('general.download')} + + + ); +} + +export default CertificateActionMenu; diff --git a/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/Settings.tsx b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/Settings.tsx new file mode 100644 index 000000000..4b16ca351 --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/Settings.tsx @@ -0,0 +1,206 @@ +import { type SamlApplicationSecretResponse, type SamlApplicationResponse } from '@logto/schemas'; +import { appendPath, type Nullable } from '@silverhand/essentials'; +import { useContext } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import useSWR, { type KeyedMutator } from 'swr'; + +import DetailsForm from '@/components/DetailsForm'; +import FormCard from '@/components/FormCard'; +import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; +import { AppDataContext } from '@/contexts/AppDataProvider'; +import CopyToClipboard from '@/ds-components/CopyToClipboard'; +import FormField from '@/ds-components/FormField'; +import Table from '@/ds-components/Table'; +import TextInput from '@/ds-components/TextInput'; +import useApi, { type RequestError } from '@/hooks/use-api'; +import useCustomDomain from '@/hooks/use-custom-domain'; +import { trySubmitSafe } from '@/utils/form'; +import { uriValidator } from '@/utils/validator'; + +import { useSecretTableColumns } from './use-secret-table-columns'; +import { + parseFormDataToSamlApplicationRequest, + parseSamlApplicationResponseToFormData, + samlApplicationEndpointPrefix, + samlApplicationManagementApiPrefix, + samlApplicationMetadataEndpointSuffix, + samlApplicationSingleSignOnEndpointSuffix, +} from './utils'; + +export type SamlApplicationFormData = Pick< + SamlApplicationResponse, + 'id' | 'description' | 'name' | 'entityId' +> & { + // Currently we only support HTTP-POST binding + // Keep the acsUrl as a string in the form data instead of the object + acsUrl: Nullable; +}; + +type Props = { + readonly data: SamlApplicationResponse; + readonly mutateApplication: KeyedMutator; + readonly isDeleted: boolean; +}; + +function Settings({ data, mutateApplication, isDeleted }: Props) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const { tenantEndpoint } = useContext(AppDataContext); + const { applyDomain: applyCustomDomain } = useCustomDomain(); + + const secrets = useSWR( + `api/saml-applications/${data.id}/secrets` + ); + + const { + register, + handleSubmit, + reset, + formState: { isDirty, isSubmitting, errors }, + } = useForm({ + defaultValues: parseSamlApplicationResponseToFormData(data), + mode: 'onBlur', + }); + + const api = useApi(); + + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + if (isSubmitting) { + return; + } + + const { id, payload } = parseFormDataToSamlApplicationRequest(formData); + + const updated = await api + .patch(`api/saml-applications/${id}`, { json: payload }) + .json(); + + reset(parseSamlApplicationResponseToFormData(updated)); + void mutateApplication(updated); + + toast.success(t('general.saved')); + }) + ); + + const secretTableColumns = useSecretTableColumns({ appId: data.id }); + + return ( + <> + + + + + + + + + + + !value || uriValidator(value) || t('errors.invalid_uri_format'), + })} + error={Boolean(errors.acsUrl)} + placeholder={t('enterprise_sso.basic_info.saml.acs_url_field_name')} + /> + + + + + + + {tenantEndpoint && ( + <> + + + + + + + + + + + )} + + + + + + + + ); +} + +export default Settings; diff --git a/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/index.module.scss b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/index.module.scss new file mode 100644 index 000000000..7cc93b7ec --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/index.module.scss @@ -0,0 +1,32 @@ +@use '@/scss/underscore' as _; + +.deleteConfirm { + > :not(:first-child) { + margin-top: _.unit(6); + } + + .description { + font: var(--font-body-2); + } + + .highlight { + color: var(--color-primary-50); + } +} + +.tabContainer { + flex-direction: column; + flex-grow: 1; + + &[data-active='true'] { + display: flex; + } +} + +.expired { + color: var(--color-placeholder); +} + +.fingerPrint { + word-break: break-all; +} diff --git a/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/index.tsx b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/index.tsx new file mode 100644 index 000000000..356cd4dac --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/index.tsx @@ -0,0 +1,171 @@ +import { + type ApplicationResponse, + ApplicationType, + type SamlApplicationResponse, +} from '@logto/schemas'; +import { useCallback, useState } from 'react'; +import { toast } from 'react-hot-toast'; +import { Trans, useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import useSWR from 'swr'; + +import Delete from '@/assets/icons/delete.svg?react'; +import ApplicationIcon from '@/components/ApplicationIcon'; +import DetailsPageHeader from '@/components/DetailsPage/DetailsPageHeader'; +import Skeleton from '@/components/FormCard/Skeleton'; +import RequestDataError from '@/components/RequestDataError'; +import { ApplicationDetailsTabs } from '@/consts'; +import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal'; +import TabNav, { TabNavItem } from '@/ds-components/TabNav'; +import TabWrapper from '@/ds-components/TabWrapper'; +import useApi, { type RequestError } from '@/hooks/use-api'; +import useTenantPathname from '@/hooks/use-tenant-pathname'; +import { applicationTypeI18nKey } from '@/types/applications'; + +import Branding from '../components/Branding'; +import Permissions from '../components/Permissions'; + +import Settings from './Settings'; +import styles from './index.module.scss'; + +type SamlApplication = Omit & { + type: ApplicationType.SAML; +}; + +export const isSamlApplication = (data: ApplicationResponse): data is SamlApplication => + data.type === ApplicationType.SAML; + +type Props = { + readonly data: SamlApplication; +}; + +function SamlApplicationDetailsContent({ data }: Props) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const { tab } = useParams(); + const { navigate } = useTenantPathname(); + + const { + data: samlApplicationData, + error: samlApplicationError, + mutate: mutateSamlApplication, + } = useSWR(`api/saml-applications/${data.id}`); + + const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [isDeleted, setIsDeleted] = useState(false); + + const api = useApi(); + + const isLoading = !samlApplicationData && !samlApplicationError; + + const onDelete = useCallback(async () => { + setIsDeleting(true); + try { + await api.delete(`api/saml-applications/${data.id}`); + setIsDeleted(true); + setIsDeleteFormOpen(false); + toast.success( + t('application_details.application_deleted', { name: samlApplicationData?.name }) + ); + navigate( + samlApplicationData?.isThirdParty + ? '/applications/third-party-applications' + : '/applications' + ); + } finally { + setIsDeleting(false); + } + }, [api, data.id, navigate, samlApplicationData?.isThirdParty, samlApplicationData?.name, t]); + + if (isLoading) { + return ; + } + + if (samlApplicationError) { + return ( + { + void mutateSamlApplication(); + }} + /> + ); + } + + return ( + <> + } + title={data.name} + primaryTag={t(`${applicationTypeI18nKey.thirdParty}.title`)} + identifier={{ name: 'App ID', value: data.id }} + actionMenuItems={[ + { + type: 'danger', + title: 'general.delete', + icon: , + onClick: () => { + setIsDeleteFormOpen(true); + }, + }, + ]} + /> + { + setIsDeleteFormOpen(false); + }} + onConfirm={onDelete} + > +
+ }}> + {t('application_details.delete_description', { name: data.name })} + +
+
+ + + {t('application_details.settings')} + + + {t('application_details.permissions.name')} + + {/** TODO: Attribute mapping tab */} + + {t('application_details.branding.name')} + + + + {samlApplicationData && ( + + )} + + + + + + {/* isActive is needed to support conditional render UnsavedChangesAlertModal */} + + + + ); +} + +export default SamlApplicationDetailsContent; diff --git a/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/use-secret-table-columns.tsx b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/use-secret-table-columns.tsx new file mode 100644 index 000000000..5252d8830 --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/use-secret-table-columns.tsx @@ -0,0 +1,85 @@ +import { type SamlApplicationSecretResponse } from '@logto/schemas'; +import { compareDesc } from 'date-fns'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { LocaleDateTime } from '@/components/DateTime'; +import { type Column } from '@/ds-components/Table/types'; +import Tag from '@/ds-components/Tag'; +import { Tooltip } from '@/ds-components/Tip'; + +import CertificateActionMenu from './CertificateActionMenu'; +import styles from './index.module.scss'; + +const isExpired = (expiresAt: Date | number) => compareDesc(expiresAt, new Date()) === 1; + +function Expired({ expiresAt }: { readonly expiresAt: Date }) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + return ( + + {t('application_details.secrets.expired')} + + ); +} + +type UseSecretTableColumns = { + appId: string; +}; + +export const useSecretTableColumns = ({ appId }: UseSecretTableColumns) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const tableColumns: Array> = useMemo( + () => [ + { + title: t('application_details.saml_idp_certificates.expires_at'), + dataIndex: 'expiresAt', + colSpan: 3, + render: ({ expiresAt }) => ( + + {isExpired(expiresAt) ? ( + + ) : ( + {expiresAt} + )} + + ), + }, + { + title: t('application_details.saml_idp_certificates.finger_print'), + dataIndex: 'fingerPrint', + colSpan: 5, + render: ({ fingerprints }) => ( + {fingerprints.sha256.unformatted} + ), + }, + { + title: t('application_details.saml_idp_certificates.status'), + dataIndex: 'status', + render: ({ active }) => ( + + {t( + active + ? 'application_details.saml_idp_certificates.active' + : 'application_details.saml_idp_certificates.inactive' + )} + + ), + }, + { + title: '', + dataIndex: 'actions', + render: (secret) => { + return ; + }, + }, + ], + [appId, t] + ); + + return tableColumns; +}; diff --git a/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/utils.ts b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/utils.ts new file mode 100644 index 000000000..ad0f7a3fd --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/SamlApplicationDetailsContent/utils.ts @@ -0,0 +1,52 @@ +import { + BindingType, + type PatchSamlApplication, + type SamlApplicationResponse, +} from '@logto/schemas'; +import { removeUndefinedKeys } from '@silverhand/essentials'; + +import { type SamlApplicationFormData } from './Settings'; + +export const parseSamlApplicationResponseToFormData = ( + data: SamlApplicationResponse +): SamlApplicationFormData => { + const { id, description, name, entityId, acsUrl } = data; + + return { + id, + description, + name, + entityId, + acsUrl: acsUrl?.url ?? null, + }; +}; + +export const parseFormDataToSamlApplicationRequest = ( + data: SamlApplicationFormData +): { + id: string; + payload: PatchSamlApplication; +} => { + const { id, description, name, entityId, acsUrl } = data; + + // If acsUrl value is empty string, it should be removed. Convert it to null. + const acsUrlData = acsUrl ? { url: acsUrl, binding: BindingType.Post } : null; + + return { + id, + payload: removeUndefinedKeys({ + description, + name, + entityId, + acsUrl: acsUrlData, + }), + }; +}; + +export const buildSamlSigningCertificateFilename = (appId: string, id: string) => + `${appId}-saml-certificate-${id}`; + +export const samlApplicationManagementApiPrefix = '/api/saml-applications'; +export const samlApplicationEndpointPrefix = '/saml'; +export const samlApplicationMetadataEndpointSuffix = 'metadata'; +export const samlApplicationSingleSignOnEndpointSuffix = 'authn'; diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/NonThirdPartyBrandingForm.tsx b/packages/console/src/pages/ApplicationDetails/components/Branding/NonThirdPartyBrandingForm.tsx similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/NonThirdPartyBrandingForm.tsx rename to packages/console/src/pages/ApplicationDetails/components/Branding/NonThirdPartyBrandingForm.tsx diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.module.scss b/packages/console/src/pages/ApplicationDetails/components/Branding/index.module.scss similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.module.scss rename to packages/console/src/pages/ApplicationDetails/components/Branding/index.module.scss diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.tsx b/packages/console/src/pages/ApplicationDetails/components/Branding/index.tsx similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.tsx rename to packages/console/src/pages/ApplicationDetails/components/Branding/index.tsx diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/use-application-sign-in-experience-swr.ts b/packages/console/src/pages/ApplicationDetails/components/Branding/use-application-sign-in-experience-swr.ts similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/use-application-sign-in-experience-swr.ts rename to packages/console/src/pages/ApplicationDetails/components/Branding/use-application-sign-in-experience-swr.ts diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/use-sign-in-experience-swr.ts b/packages/console/src/pages/ApplicationDetails/components/Branding/use-sign-in-experience-swr.ts similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/use-sign-in-experience-swr.ts rename to packages/console/src/pages/ApplicationDetails/components/Branding/use-sign-in-experience-swr.ts diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/utils.ts b/packages/console/src/pages/ApplicationDetails/components/Branding/utils.ts similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/utils.ts rename to packages/console/src/pages/ApplicationDetails/components/Branding/utils.ts diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/constants.ts b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/constants.ts similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/constants.ts rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/constants.ts diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/index.tsx b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/index.tsx similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/index.tsx rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/index.tsx diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/type.ts b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/type.ts similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/type.ts rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/type.ts diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-application-scopes-assignment.ts b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-application-scopes-assignment.ts similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-application-scopes-assignment.ts rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-application-scopes-assignment.ts diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-organization-scopes-assignment.ts b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-organization-scopes-assignment.ts similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-organization-scopes-assignment.ts rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-organization-scopes-assignment.ts diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-resource-scopes-assignment.ts b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-resource-scopes-assignment.ts similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-resource-scopes-assignment.ts rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-resource-scopes-assignment.ts diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-user-scopes-assignment.ts b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-user-scopes-assignment.ts similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-user-scopes-assignment.ts rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesAssignmentModal/use-user-scopes-assignment.ts diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesManagementModal/index.tsx b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesManagementModal/index.tsx similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/ApplicationScopesManagementModal/index.tsx rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/ApplicationScopesManagementModal/index.tsx diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/index.module.scss b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/index.module.scss similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/index.module.scss rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/index.module.scss diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/index.tsx b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/index.tsx similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/index.tsx rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/index.tsx diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/use-scopes-table.tsx b/packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/use-scopes-table.tsx similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/PermissionsCard/use-scopes-table.tsx rename to packages/console/src/pages/ApplicationDetails/components/Permissions/PermissionsCard/use-scopes-table.tsx diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/index.module.scss b/packages/console/src/pages/ApplicationDetails/components/Permissions/index.module.scss similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/index.module.scss rename to packages/console/src/pages/ApplicationDetails/components/Permissions/index.module.scss diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/index.tsx b/packages/console/src/pages/ApplicationDetails/components/Permissions/index.tsx similarity index 100% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Permissions/index.tsx rename to packages/console/src/pages/ApplicationDetails/components/Permissions/index.tsx diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index f0840a1c6..5e393155b 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -12,6 +12,7 @@ import useTenantPathname from '@/hooks/use-tenant-pathname'; import ApplicationDetailsContent from './ApplicationDetailsContent'; import { type ApplicationSecretRow } from './ApplicationDetailsContent/EndpointsAndCredentials'; import GuideModal from './GuideModal'; +import SamlApplicationDetailsContent, { isSamlApplication } from './SamlApplicationDetailsContent'; function ApplicationDetails() { const { id, guideId } = useParams(); @@ -60,14 +61,19 @@ function ApplicationDetails() { }} > - {data && oidcConfig.data && secrets.data && ( - - )} + {data && + oidcConfig.data && + secrets.data && + (isSamlApplication(data) ? ( + + ) : ( + + ))} ); } diff --git a/packages/console/src/utils/downloader.ts b/packages/console/src/utils/downloader.ts new file mode 100644 index 000000000..e65117d12 --- /dev/null +++ b/packages/console/src/utils/downloader.ts @@ -0,0 +1,15 @@ +export const downloadText = ( + text: string, + filename: string, + type: BlobPropertyBag['type'] = 'text/plain' +) => { + const blob = new Blob([text], { type }); + const downloadLink = document.createElement('a'); + // eslint-disable-next-line @silverhand/fp/no-mutation + downloadLink.href = URL.createObjectURL(blob); + // eslint-disable-next-line @silverhand/fp/no-mutation + downloadLink.download = filename; + downloadLink.click(); + downloadLink.remove(); + window.URL.revokeObjectURL(downloadLink.href); +}; diff --git a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts index 12179e4ba..ae6ff8260 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts @@ -198,6 +198,22 @@ const application_details = { edited: 'The secret {{name}} has been successfully edited.', }, }, + saml_idp_config: { + title: 'SAML IdP metadata', + description: + 'Use the following metadata and certificate to configure the SAML IdP in your application.', + metadata_url_label: 'IdP metadata URL', + single_sign_on_service_url_label: 'Single sign-on service URL', + idp_entity_id_label: 'IdP entity ID', + }, + saml_idp_certificates: { + title: 'SAML signing certificate', + expires_at: 'Expires at', + finger_print: 'Fingerprint', + status: 'Status', + active: 'Active', + inactive: 'Inactive', + }, }; export default Object.freeze(application_details); diff --git a/packages/phrases/src/locales/en/translation/admin-console/general.ts b/packages/phrases/src/locales/en/translation/admin-console/general.ts index 719de37b7..216d78ce3 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/general.ts @@ -13,6 +13,7 @@ const general = { save_changes: 'Save changes', saved: 'Saved', discard: 'Discard', + download: 'Download', loading: 'Loading...', redirecting: 'Redirecting...', add: 'Add',