From 01df8f46a2d4ea7938e73acc3135f4e7597aedf8 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sun, 28 Jul 2024 11:47:40 +0800 Subject: [PATCH] refactor(console): improve ux --- .../fragments/_add-authentication.mdx | 2 +- .../assets/docs/guides/web-express/README.mdx | 2 +- .../src/assets/docs/guides/web-go/README.mdx | 2 +- .../guides/web-java-spring-boot/README.mdx | 2 +- .../guides/web-next-app-router/README.mdx | 2 +- .../docs/guides/web-next-auth/README.mdx | 2 +- .../assets/docs/guides/web-next/README.mdx | 2 +- .../assets/docs/guides/web-nuxt/README.mdx | 4 +- .../src/assets/docs/guides/web-php/README.mdx | 2 +- .../assets/docs/guides/web-python/README.mdx | 2 +- .../assets/docs/guides/web-ruby/README.mdx | 4 +- .../docs/guides/web-sveltekit/README.mdx | 2 +- .../CopyToClipboard/index.module.scss | 27 +++-- .../ds-components/CopyToClipboard/index.tsx | 13 ++- .../CreateSecretModal.tsx | 2 +- .../EndpointsAndCredentials/index.module.scss | 39 +++++++ .../index.tsx} | 80 ++++---------- .../use-secret-table-columns.tsx | 100 ++++++++++++++++++ .../GuideDrawer/index.tsx | 5 +- .../index.module.scss | 29 ----- .../ApplicationDetailsContent/index.tsx | 7 +- .../ApplicationDetails/GuideModal/index.tsx | 14 ++- .../components/AppGuide/index.tsx | 31 +++--- .../src/pages/ApplicationDetails/index.tsx | 30 ++++-- .../src/routes/applications/application.ts | 3 +- .../admin-console/application-details.ts | 4 +- 26 files changed, 256 insertions(+), 156 deletions(-) create mode 100644 packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials/index.module.scss rename packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/{EndpointsAndCredentials.tsx => EndpointsAndCredentials/index.tsx} (80%) create mode 100644 packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials/use-secret-table-columns.tsx diff --git a/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/fragments/_add-authentication.mdx b/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/fragments/_add-authentication.mdx index 34b1fe74690..62a183eab90 100644 --- a/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/fragments/_add-authentication.mdx +++ b/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/fragments/_add-authentication.mdx @@ -9,7 +9,7 @@ builder.Services.AddLogtoAuthentication(options => { options.Endpoint = "${props.endpoint}"; options.AppId = "${props.app.id}"; - options.AppSecret = "${props.app.secret}"; + options.AppSecret = "${props.secrets[0]?.value ?? props.app.secret}"; }); app.UseAuthentication();`} diff --git a/packages/console/src/assets/docs/guides/web-express/README.mdx b/packages/console/src/assets/docs/guides/web-express/README.mdx index 4325a964589..9fcb5f7a336 100644 --- a/packages/console/src/assets/docs/guides/web-express/README.mdx +++ b/packages/console/src/assets/docs/guides/web-express/README.mdx @@ -31,7 +31,7 @@ Prepare configuration for the Logto client: const config: LogtoExpressConfig = { endpoint: '${props.endpoint}', appId: '${props.app.id}', - appSecret: '${props.app.secret}', + appSecret: '${props.secrets[0]?.value ?? props.app.secret}', baseUrl: 'http://localhost:3000', // Change to your own base URL }; `} diff --git a/packages/console/src/assets/docs/guides/web-go/README.mdx b/packages/console/src/assets/docs/guides/web-go/README.mdx index 26e34567b4a..478f88527a2 100644 --- a/packages/console/src/assets/docs/guides/web-go/README.mdx +++ b/packages/console/src/assets/docs/guides/web-go/README.mdx @@ -145,7 +145,7 @@ First, create a Logto config: logtoConfig := &client.LogtoConfig{ Endpoint: "${props.endpoint}", AppId: "${props.app.id}", - AppSecret: "${props.app.secret}", + AppSecret: "${props.secrets[0]?.value ?? props.app.secret}", } // ... diff --git a/packages/console/src/assets/docs/guides/web-java-spring-boot/README.mdx b/packages/console/src/assets/docs/guides/web-java-spring-boot/README.mdx index 92625af90a4..80f83361dc6 100644 --- a/packages/console/src/assets/docs/guides/web-java-spring-boot/README.mdx +++ b/packages/console/src/assets/docs/guides/web-java-spring-boot/README.mdx @@ -63,7 +63,7 @@ Add the following configuration to your `application.properties` file: {`spring.security.oauth2.client.registration.logto.client-name=logto spring.security.oauth2.client.registration.logto.client-id=${props.app.id} -spring.security.oauth2.client.registration.logto.client-secret=${props.app.secret} +spring.security.oauth2.client.registration.logto.client-secret=${props.secrets[0]?.value ?? props.app.secret} spring.security.oauth2.client.registration.logto.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} spring.security.oauth2.client.registration.logto.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.logto.scope=openid,profile,email,offline_access diff --git a/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx b/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx index 5c6196036df..1ca7e409158 100644 --- a/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx +++ b/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx @@ -26,7 +26,7 @@ Prepare configuration for the Logto client: {`export const logtoConfig = { endpoint: '${props.endpoint}', appId: '${props.app.id}', - appSecret: '${props.app.secret}', + appSecret: '${props.secrets[0]?.value ?? props.app.secret}', baseUrl: 'http://localhost:3000', // Change to your own base URL cookieSecret: '${generateStandardSecret()}', // Auto-generated 32 digit secret cookieSecure: process.env.NODE_ENV === 'production', diff --git a/packages/console/src/assets/docs/guides/web-next-auth/README.mdx b/packages/console/src/assets/docs/guides/web-next-auth/README.mdx index badcf966507..f9959c3bc52 100644 --- a/packages/console/src/assets/docs/guides/web-next-auth/README.mdx +++ b/packages/console/src/assets/docs/guides/web-next-auth/README.mdx @@ -45,7 +45,7 @@ const handler = NextAuth({ wellKnown: '${props.endpoint}oidc/.well-known/openid-configuration', authorization: { params: { scope: 'openid offline_access profile email' } }, clientId: '${props.app.id}'', - clientSecret: '${props.app.secret}', + clientSecret: '${props.secrets[0]?.value ?? props.app.secret}', client: { id_token_signed_response_alg: 'ES384', }, diff --git a/packages/console/src/assets/docs/guides/web-next/README.mdx b/packages/console/src/assets/docs/guides/web-next/README.mdx index 961465f691c..6cb14c8c16f 100644 --- a/packages/console/src/assets/docs/guides/web-next/README.mdx +++ b/packages/console/src/assets/docs/guides/web-next/README.mdx @@ -28,7 +28,7 @@ Import and initialize LogtoClient: export const logtoClient = new LogtoClient({ endpoint: '${props.endpoint}', appId: '${props.app.id}', - appSecret: '${props.app.secret}', + appSecret: '${props.secrets[0]?.value ?? props.app.secret}', baseUrl: '${defaultBaseUrl}', // Change to your own base URL cookieSecret: '${generateStandardSecret()}', // Auto-generated 32 digit secret cookieSecure: process.env.NODE_ENV === 'production', diff --git a/packages/console/src/assets/docs/guides/web-nuxt/README.mdx b/packages/console/src/assets/docs/guides/web-nuxt/README.mdx index b274d4849e7..29c58fa6663 100644 --- a/packages/console/src/assets/docs/guides/web-nuxt/README.mdx +++ b/packages/console/src/assets/docs/guides/web-nuxt/README.mdx @@ -35,7 +35,7 @@ In your Nuxt config file, add the Logto module and configure it: logto: { endpoint: '${props.endpoint}', appId: '${props.app.id}', - appSecret: '${props.app.secret}', + appSecret: '${props.secrets[0]?.value ?? props.app.secret}', cookieEncryptionKey: '${cookieEncryptionKey}', // Random-generated }, }, @@ -48,7 +48,7 @@ Since these information are sensitive, it's recommended to use environment varia {`NUXT_LOGTO_ENDPOINT=${props.endpoint} NUXT_LOGTO_APP_ID=${props.app.id} -NUXT_LOGTO_APP_SECRET=${props.app.secret} +NUXT_LOGTO_APP_SECRET=${props.secrets[0]?.value ?? props.app.secret} NUXT_LOGTO_COOKIE_ENCRYPTION_KEY=${cookieEncryptionKey} # Random-generated `} diff --git a/packages/console/src/assets/docs/guides/web-php/README.mdx b/packages/console/src/assets/docs/guides/web-php/README.mdx index 6dfe1d3d7af..32ea660d4fa 100644 --- a/packages/console/src/assets/docs/guides/web-php/README.mdx +++ b/packages/console/src/assets/docs/guides/web-php/README.mdx @@ -35,7 +35,7 @@ $client = new LogtoClient( new LogtoConfig( endpoint: "${props.endpoint}", appId: "${props.app.id}", - appSecret: "${props.app.secret}", + appSecret: "${props.secrets[0]?.value ?? props.app.secret}", ), );`} diff --git a/packages/console/src/assets/docs/guides/web-python/README.mdx b/packages/console/src/assets/docs/guides/web-python/README.mdx index 52fd2300a3b..1616da8d390 100644 --- a/packages/console/src/assets/docs/guides/web-python/README.mdx +++ b/packages/console/src/assets/docs/guides/web-python/README.mdx @@ -32,7 +32,7 @@ client = LogtoClient( LogtoConfig( endpoint="${props.endpoint}", appId="${props.app.id}", - appSecret="${props.app.secret}", + appSecret="${props.secrets[0]?.value ?? props.app.secret}", ) )`} diff --git a/packages/console/src/assets/docs/guides/web-ruby/README.mdx b/packages/console/src/assets/docs/guides/web-ruby/README.mdx index 63d9e9e5470..89f05318152 100644 --- a/packages/console/src/assets/docs/guides/web-ruby/README.mdx +++ b/packages/console/src/assets/docs/guides/web-ruby/README.mdx @@ -41,7 +41,7 @@ In the file where you want to initialize the Logto client (e.g. a base controlle config: LogtoClient::Config.new( endpoint: "${props.endpoint}", app_id: "${props.app.id}", - app_secret: "${props.app.secret}" + app_secret: "${props.secrets[0]?.value ?? props.app.secret}" ), navigate: ->(uri) { a_redirect_method(uri) }, storage: LogtoClient::SessionStorage.new(the_session_object) @@ -64,7 +64,7 @@ class SampleController < ApplicationController config: LogtoClient::Config.new( endpoint: "${props.endpoint}", app_id: "${props.app.id}", - app_secret: "${props.app.secret}" + app_secret: "${props.secrets[0]?.value ?? props.app.secret}" ), # Allow the client to redirect to other hosts (i.e. your Logto tenant) navigate: ->(uri) { redirect_to(uri, allow_other_host: true) }, diff --git a/packages/console/src/assets/docs/guides/web-sveltekit/README.mdx b/packages/console/src/assets/docs/guides/web-sveltekit/README.mdx index bd361427691..d3b1cbff7b8 100644 --- a/packages/console/src/assets/docs/guides/web-sveltekit/README.mdx +++ b/packages/console/src/assets/docs/guides/web-sveltekit/README.mdx @@ -40,7 +40,7 @@ export const handle = handleLogto( { endpoint: '${props.endpoint}', appId: '${props.app.id}', - appSecret: '${props.app.secret}', + appSecret: '${props.secrets[0]?.value ?? props.app.secret}', }, { encryptionKey: '${cookieEncryptionKey}' } // Random-generated key );`} diff --git a/packages/console/src/ds-components/CopyToClipboard/index.module.scss b/packages/console/src/ds-components/CopyToClipboard/index.module.scss index 1e577900678..3989a2726d7 100644 --- a/packages/console/src/ds-components/CopyToClipboard/index.module.scss +++ b/packages/console/src/ds-components/CopyToClipboard/index.module.scss @@ -28,36 +28,34 @@ align-items: center; justify-content: space-between; cursor: text; + gap: _.unit(2); .content { flex: 1; overflow: hidden; text-overflow: ellipsis; + display: inline-flex; + align-items: center; + gap: _.unit(2); + margin-right: _.unit(1); + flex-wrap: nowrap; &.wrapContent { text-overflow: unset; word-break: break-all; } } - - .copyToolTipAnchor { - margin-left: _.unit(2); - } } &.default { .row { - .copyToolTipAnchor { - margin-left: _.unit(3); - } + gap: _.unit(2); } } &.small { .row { - .copyToolTipAnchor { - margin-left: _.unit(1); - } + gap: _.unit(0.5); .iconButton { height: 20px; @@ -72,4 +70,13 @@ } } } + + .dot { + flex-shrink: 0; + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-text-secondary); + } } diff --git a/packages/console/src/ds-components/CopyToClipboard/index.tsx b/packages/console/src/ds-components/CopyToClipboard/index.tsx index 962ff8406e9..2fe3e52f2f6 100644 --- a/packages/console/src/ds-components/CopyToClipboard/index.tsx +++ b/packages/console/src/ds-components/CopyToClipboard/index.tsx @@ -60,7 +60,10 @@ function CopyToClipboard( return value; } - return '•'.repeat(value.length); + return Array.from({ length: Math.max(Math.floor((value.length / 5) * 3), 1) }).map( + // eslint-disable-next-line react/no-array-index-key -- No need to persist the key + (_, index) => + ); }, [hasVisibilityToggle, showHiddenContent, value]); useEffect(() => { @@ -107,7 +110,7 @@ function CopyToClipboard( {variant !== 'icon' && (
{displayValue}
@@ -124,11 +127,7 @@ function CopyToClipboard( )} - + ({ defaultValues: { name: '', expiration: String(days[0]) } }); + } = useForm({ defaultValues: { name: '', expiration: neverExpires } }); const onCloseHandler = useCallback( (created?: ApplicationSecret) => { reset(); diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials/index.module.scss b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials/index.module.scss new file mode 100644 index 00000000000..c017eeb912c --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials/index.module.scss @@ -0,0 +1,39 @@ +@use '@/scss/underscore' as _; + +.customEndpointNotes { + margin-top: _.unit(6); + font: var(--font-body-2); + color: var(--color-text-secondary); +} + +.fieldButton { + margin-top: _.unit(2); +} + +.trailingIcon { + width: 16px; + height: 16px; +} + +button.add { + margin-top: _.unit(2); +} + +.table { + margin-top: _.unit(2); + word-break: break-word; +} + +.empty { + font: var(--font-body-2); + color: var(--color-text-secondary); + margin: _.unit(3) 0; +} + +.expired { + color: var(--color-text-secondary); +} + +.copyToClipboard { + width: fit-content; +} diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials/index.tsx similarity index 80% rename from packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials.tsx rename to packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials/index.tsx index 942145a9102..b57b89e3f2f 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials/index.tsx @@ -1,5 +1,4 @@ import { - type ApplicationSecret, DomainStatus, type Application, type SnakeCaseOidcConfig, @@ -15,7 +14,6 @@ import CaretDown from '@/assets/icons/caret-down.svg?react'; import CaretUp from '@/assets/icons/caret-up.svg?react'; import CirclePlus from '@/assets/icons/circle-plus.svg?react'; import Plus from '@/assets/icons/plus.svg?react'; -import ActionsButton from '@/components/ActionsButton'; import FormCard from '@/components/FormCard'; import { openIdProviderConfigPath, openIdProviderPath } from '@/consts/oidc'; import { AppDataContext } from '@/contexts/AppDataProvider'; @@ -24,19 +22,18 @@ import CopyToClipboard from '@/ds-components/CopyToClipboard'; import DynamicT from '@/ds-components/DynamicT'; import FormField from '@/ds-components/FormField'; import Table from '@/ds-components/Table'; -import { type Column } from '@/ds-components/Table/types'; import TextLink from '@/ds-components/TextLink'; -import useApi, { type RequestError } from '@/hooks/use-api'; +import { type RequestError } from '@/hooks/use-api'; import useCustomDomain from '@/hooks/use-custom-domain'; -import CreateSecretModal from './CreateSecretModal'; +import CreateSecretModal from '../CreateSecretModal'; + import styles from './index.module.scss'; +import { type ApplicationSecretRow, useSecretTableColumns } from './use-secret-table-columns'; -const isLegacySecret = (secret: string) => !secret.startsWith(internalPrefix); +export { type ApplicationSecretRow } from './use-secret-table-columns'; -type ApplicationSecretRow = Pick & { - isLegacy?: boolean; -}; +const isLegacySecret = (secret: string) => !secret.startsWith(internalPrefix); type Props = { readonly app: Application; @@ -55,7 +52,6 @@ function EndpointsAndCredentials({ const { data: customDomain, applyDomain: applyCustomDomain } = useCustomDomain(); const [showCreateSecretModal, setShowCreateSecretModal] = useState(false); const secrets = useSWR(`api/applications/${id}/secrets`); - const api = useApi(); const shouldShowAppSecrets = hasSecrets(type); const toggleShowMoreEndpoints = useCallback(() => { @@ -80,57 +76,21 @@ function EndpointsAndCredentials({ ], [secret, secrets.data, t] ); - const tableColumns: Array> = useMemo( - () => [ - { - title: t('general.name'), - dataIndex: 'name', - colSpan: 3, - render: ({ name }) => {name}, - }, - { - title: t('application_details.secrets.value'), - dataIndex: 'value', - colSpan: 6, - render: ({ value }) => ( - - ), - }, - { - title: t('application_details.secrets.expires_at'), - dataIndex: 'expiresAt', - colSpan: 3, - render: ({ expiresAt }) => ( - - {expiresAt - ? new Date(expiresAt).toLocaleString() - : t('application_details.secrets.never')} - - ), - }, - { - title: '', - dataIndex: 'actions', - render: ({ name, isLegacy }) => ( - { - if (isLegacy) { - await api.delete(`api/applications/${id}/legacy-secret`); - onApplicationUpdated(); - } else { - await api.delete(`api/applications/${id}/secrets/${encodeURIComponent(name)}`); - void secrets.mutate(); - } - }} - /> - ), - }, - ], - [api, id, onApplicationUpdated, secrets, t] - ); + const onUpdated = useCallback( + (isLegacy: boolean) => { + if (isLegacy) { + onApplicationUpdated(); + } else { + void secrets.mutate(); + } + }, + [onApplicationUpdated, secrets] + ); + const tableColumns = useSecretTableColumns({ + appId: id, + onUpdated, + }); return ( & { + isLegacy?: boolean; +}; + +function Expired({ expiresAt }: { readonly expiresAt: Date }) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + return ( + + {t('application_details.secrets.expired')} + + ); +} + +type UseSecretTableColumns = { + appId: string; + onUpdated: (isLegacy: boolean) => void; +}; + +export const useSecretTableColumns = ({ appId, onUpdated }: UseSecretTableColumns) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const api = useApi(); + const tableColumns: Array> = useMemo( + () => [ + { + title: t('general.name'), + dataIndex: 'name', + colSpan: 3, + render: ({ name }) => {name}, + }, + { + title: t('application_details.secrets.value'), + dataIndex: 'value', + colSpan: 6, + render: ({ value }) => ( + + ), + }, + { + title: t('application_details.secrets.expires_at'), + dataIndex: 'expiresAt', + colSpan: 3, + render: ({ expiresAt }) => ( + + {expiresAt ? ( + compareDesc(expiresAt, new Date()) === 1 ? ( + + ) : ( + new Date(expiresAt).toLocaleString() + ) + ) : ( + t('application_details.secrets.never') + )} + + ), + }, + { + title: '', + dataIndex: 'actions', + render: ({ name, isLegacy }) => ( + { + await (isLegacy + ? api.delete(`api/applications/${appId}/legacy-secret`) + : api.delete(`api/applications/${appId}/secrets/${encodeURIComponent(name)}`)); + onUpdated(isLegacy ?? false); + }} + /> + ), + }, + ], + [api, appId, onUpdated, t] + ); + + return tableColumns; +}; diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/GuideDrawer/index.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/GuideDrawer/index.tsx index 4c078f476aa..01abe683c2c 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/GuideDrawer/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/GuideDrawer/index.tsx @@ -11,15 +11,17 @@ import IconButton from '@/ds-components/IconButton'; import Spacer from '@/ds-components/Spacer'; import AppGuide from '../../components/AppGuide'; +import { type ApplicationSecretRow } from '../EndpointsAndCredentials'; import styles from './index.module.scss'; type Props = { readonly app: ApplicationResponse; + readonly secrets: ApplicationSecretRow[]; readonly onClose: () => void; }; -function GuideDrawer({ app, onClose }: Props) { +function GuideDrawer({ app, secrets, onClose }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.guide' }); const { getStructuredAppGuideMetadata } = useAppGuideMetadata(); const [selectedGuide, setSelectedGuide] = useState(); @@ -89,6 +91,7 @@ function GuideDrawer({ app, onClose }: Props) { className={styles.guide} guideId={selectedGuide.id} app={app} + secrets={secrets} onClose={() => { setSelectedGuide(undefined); }} diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.module.scss b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.module.scss index 8d15c85ceed..95808358eb9 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.module.scss +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.module.scss @@ -14,21 +14,6 @@ } } -.customEndpointNotes { - margin-top: _.unit(6); - font: var(--font-body-2); - color: var(--color-text-secondary); -} - -.fieldButton { - margin-top: _.unit(2); -} - -.trailingIcon { - width: 16px; - height: 16px; -} - .tabContainer { flex-direction: column; flex-grow: 1; @@ -37,17 +22,3 @@ display: flex; } } - -button.add { - margin-top: _.unit(2); -} - -.table { - margin-top: _.unit(2); -} - -.empty { - font: var(--font-body-2); - color: var(--color-text-secondary); - margin: _.unit(3) 0; -} diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx index 367a4868393..2a2db9a37ef 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx @@ -32,7 +32,7 @@ import { trySubmitSafe } from '@/utils/form'; import BackchannelLogout from './BackchannelLogout'; import Branding from './Branding'; -import EndpointsAndCredentials from './EndpointsAndCredentials'; +import EndpointsAndCredentials, { type ApplicationSecretRow } from './EndpointsAndCredentials'; import GuideDrawer from './GuideDrawer'; import MachineLogs from './MachineLogs'; import MachineToMachineApplicationRoles from './MachineToMachineApplicationRoles'; @@ -44,11 +44,12 @@ import { type ApplicationForm, applicationFormDataParser } from './utils'; type Props = { readonly data: ApplicationResponse; + readonly secrets: ApplicationSecretRow[]; readonly oidcConfig: SnakeCaseOidcConfig; readonly onApplicationUpdated: () => void; }; -function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: Props) { +function ApplicationDetailsContent({ data, secrets, oidcConfig, onApplicationUpdated }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { tab } = useParams(); const { navigate } = useTenantPathname(); @@ -154,7 +155,7 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P ]} /> - + void; }; -function GuideModal({ guideId, app, onClose }: Props) { +function GuideModal({ guideId, app, secrets, onClose }: Props) { return (
@@ -27,7 +29,13 @@ function GuideModal({ guideId, app, onClose }: Props) { requestSuccessMessage="guide.request_guide_successfully" onClose={onClose} /> - +
); diff --git a/packages/console/src/pages/ApplicationDetails/components/AppGuide/index.tsx b/packages/console/src/pages/ApplicationDetails/components/AppGuide/index.tsx index a66017cb53e..12ac43f7aec 100644 --- a/packages/console/src/pages/ApplicationDetails/components/AppGuide/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/components/AppGuide/index.tsx @@ -7,15 +7,18 @@ import Guide, { GuideContext, type GuideContextType } from '@/components/Guide'; import { AppDataContext } from '@/contexts/AppDataProvider'; import useCustomDomain from '@/hooks/use-custom-domain'; +import { type ApplicationSecretRow } from '../../ApplicationDetailsContent/EndpointsAndCredentials'; + type Props = { readonly className?: string; readonly guideId: string; - readonly app?: ApplicationResponse; + readonly app: ApplicationResponse; + readonly secrets: ApplicationSecretRow[]; readonly isCompact?: boolean; readonly onClose: () => void; }; -function AppGuide({ className, guideId, app, isCompact, onClose }: Props) { +function AppGuide({ className, guideId, app, secrets, isCompact, onClose }: Props) { const { tenantEndpoint } = useContext(AppDataContext); const { applyDomain: applyCustomDomain } = useCustomDomain(); const guide = guides.find(({ id }) => id === guideId); @@ -23,18 +26,18 @@ function AppGuide({ className, guideId, app, isCompact, onClose }: Props) { const memorizedContext = useMemo( () => conditional( - !!guide && - !!app && { - metadata: guide.metadata, - Logo: guide.Logo, - app, - endpoint: applyCustomDomain(tenantEndpoint?.href ?? ''), - redirectUris: app.oidcClientMetadata.redirectUris, - postLogoutRedirectUris: app.oidcClientMetadata.postLogoutRedirectUris, - isCompact: Boolean(isCompact), - } + !!guide && { + metadata: guide.metadata, + Logo: guide.Logo, + app, + secrets, + endpoint: applyCustomDomain(tenantEndpoint?.href ?? ''), + redirectUris: app.oidcClientMetadata.redirectUris, + postLogoutRedirectUris: app.oidcClientMetadata.postLogoutRedirectUris, + isCompact: Boolean(isCompact), + } ) satisfies GuideContextType | undefined, - [guide, app, tenantEndpoint?.href, applyCustomDomain, isCompact] + [guide, app, secrets, applyCustomDomain, tenantEndpoint?.href, isCompact] ); return memorizedContext ? ( @@ -42,7 +45,7 @@ function AppGuide({ className, guideId, app, isCompact, onClose }: Props) { diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index 14f6a1e5369..f0840a1c688 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -5,10 +5,12 @@ import useSWR from 'swr'; import DetailsPage from '@/components/DetailsPage'; import PageMeta from '@/components/PageMeta'; import { openIdProviderConfigPath } from '@/consts/oidc'; +import { Daisy } from '@/ds-components/Spinner'; import type { RequestError } from '@/hooks/use-api'; import useTenantPathname from '@/hooks/use-tenant-pathname'; import ApplicationDetailsContent from './ApplicationDetailsContent'; +import { type ApplicationSecretRow } from './ApplicationDetailsContent/EndpointsAndCredentials'; import GuideModal from './GuideModal'; function ApplicationDetails() { @@ -19,21 +21,25 @@ function ApplicationDetails() { const { data, error, mutate } = useSWR( id && `api/applications/${id}` ); + const secrets = useSWR(`api/applications/${id}/secrets`); + const oidcConfig = useSWR(openIdProviderConfigPath); - const { - data: oidcConfig, - error: fetchOidcConfigError, - mutate: mutateOidcConfig, - } = useSWR(openIdProviderConfigPath); - - const isLoading = (!data && !error) || (!oidcConfig && !fetchOidcConfigError); - const requestError = error ?? fetchOidcConfigError; + const isLoading = + (!data && !error) || + (!oidcConfig.data && !oidcConfig.error) || + (!secrets.data && !secrets.error); + const requestError = error ?? oidcConfig.error ?? secrets.error; if (isGuideView) { + if (!data || !secrets.data) { + return ; + } + return ( { navigate(`/applications/${id}`); }} @@ -49,14 +55,16 @@ function ApplicationDetails() { error={requestError} onRetry={() => { void mutate(); - void mutateOidcConfig(); + void oidcConfig.mutate(); + void secrets.mutate(); }} > - {data && oidcConfig && ( + {data && oidcConfig.data && secrets.data && ( )} diff --git a/packages/core/src/routes/applications/application.ts b/packages/core/src/routes/applications/application.ts index 2537c59de53..535fd258374 100644 --- a/packages/core/src/routes/applications/application.ts +++ b/packages/core/src/routes/applications/application.ts @@ -13,6 +13,7 @@ import { generateStandardId, generateStandardSecret } from '@logto/shared'; import { conditional } from '@silverhand/essentials'; import { boolean, object, string, z } from 'zod'; +import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; @@ -26,8 +27,6 @@ import applicationCustomDataRoutes from './application-custom-data.js'; import { generateInternalSecret } from './application-secret.js'; import { applicationCreateGuard, applicationPatchGuard } from './types.js'; -import { EnvSet } from '#src/src/env-set/index.js'; - const includesInternalAdminRole = (roles: Readonly>) => roles.some(({ role: { name } }) => name === InternalRole.Admin); 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 f47cd9687e0..879cc78c63f 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 @@ -172,12 +172,14 @@ const application_details = { delete_confirmation: 'This action cannot be undone. Are you sure you want to delete this secret?', legacy_secret: 'Legacy secret', + expired: 'Expired', + expired_tooltip: 'This secret was expired on {{date}}.', create_modal: { title: 'Create application secret', expiration: 'Expiration', expiration_description: 'The secret will expire at {{date}}.', expiration_description_never: - 'The secret will never expire. We strongly recommend setting an expiration date.', + 'The secret will never expire. We recommend setting an expiration date for better security.', days: '{{count}} day', days_other: '{{count}} days', },