0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

refactor(console): improve ux

This commit is contained in:
Gao Sun 2024-07-28 11:47:40 +08:00
parent dce56eeb3f
commit 01df8f46a2
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
26 changed files with 256 additions and 156 deletions

View file

@ -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();`}

View file

@ -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
};
`}

View file

@ -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}",
}
// ...

View file

@ -63,7 +63,7 @@ Add the following configuration to your `application.properties` file:
<Code className="language-properties" title="application.properties">
{`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

View file

@ -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',

View file

@ -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',
},

View file

@ -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',

View file

@ -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
<Code title=".env" className="language-bash">
{`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
`}
</Code>

View file

@ -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}",
),
);`}
</Code>

View file

@ -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}",
)
)`}
</Code>

View file

@ -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) },

View file

@ -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
);`}

View file

@ -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);
}
}

View file

@ -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) => <span key={index} className={styles.dot} />
);
}, [hasVisibilityToggle, showHiddenContent, value]);
useEffect(() => {
@ -107,7 +110,7 @@ function CopyToClipboard(
{variant !== 'icon' && (
<div
className={classNames(styles.content, isWordWrapAllowed && styles.wrapContent)}
style={valueStyle}
style={{ width: `${value.length}ch`, ...valueStyle }}
>
{displayValue}
</div>
@ -124,11 +127,7 @@ function CopyToClipboard(
</IconButton>
</Tooltip>
)}
<Tooltip
isSuccessful={copyState === 'copied'}
anchorClassName={styles.copyToolTipAnchor}
content={t(copyState)}
>
<Tooltip isSuccessful={copyState === 'copied'} content={t(copyState)}>
<IconButton
ref={copyIconReference}
className={styles.iconButton}

View file

@ -36,7 +36,7 @@ function CreateSecretModal({ appId, isOpen, onClose }: Props) {
formState: { errors, isSubmitting },
handleSubmit,
reset,
} = useForm<FormData>({ defaultValues: { name: '', expiration: String(days[0]) } });
} = useForm<FormData>({ defaultValues: { name: '', expiration: neverExpires } });
const onCloseHandler = useCallback(
(created?: ApplicationSecret) => {
reset();

View file

@ -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;
}

View file

@ -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,20 +22,19 @@ 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';
export { type ApplicationSecretRow } from './use-secret-table-columns';
const isLegacySecret = (secret: string) => !secret.startsWith(internalPrefix);
type ApplicationSecretRow = Pick<ApplicationSecret, 'name' | 'value' | 'expiresAt'> & {
isLegacy?: boolean;
};
type Props = {
readonly app: Application;
readonly oidcConfig: SnakeCaseOidcConfig;
@ -55,7 +52,6 @@ function EndpointsAndCredentials({
const { data: customDomain, applyDomain: applyCustomDomain } = useCustomDomain();
const [showCreateSecretModal, setShowCreateSecretModal] = useState(false);
const secrets = useSWR<ApplicationSecretRow[], RequestError>(`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<Column<ApplicationSecretRow>> = useMemo(
() => [
{
title: t('general.name'),
dataIndex: 'name',
colSpan: 3,
render: ({ name }) => <span>{name}</span>,
},
{
title: t('application_details.secrets.value'),
dataIndex: 'value',
colSpan: 6,
render: ({ value }) => (
<CopyToClipboard hasVisibilityToggle displayType="block" value={value} variant="text" />
),
},
{
title: t('application_details.secrets.expires_at'),
dataIndex: 'expiresAt',
colSpan: 3,
render: ({ expiresAt }) => (
<span>
{expiresAt
? new Date(expiresAt).toLocaleString()
: t('application_details.secrets.never')}
</span>
),
},
{
title: '',
dataIndex: 'actions',
render: ({ name, isLegacy }) => (
<ActionsButton
fieldName="application_details.application_secret"
deleteConfirmation="application_details.secrets.delete_confirmation"
onDelete={async () => {
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 (
<FormCard
title="application_details.endpoints_and_credentials"

View file

@ -0,0 +1,100 @@
import { type ApplicationSecret } from '@logto/schemas';
import { compareDesc } from 'date-fns';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import ActionsButton from '@/components/ActionsButton';
import CopyToClipboard from '@/ds-components/CopyToClipboard';
import { type Column } from '@/ds-components/Table/types';
import { Tooltip } from '@/ds-components/Tip';
import useApi from '@/hooks/use-api';
import styles from './index.module.scss';
export type ApplicationSecretRow = Pick<ApplicationSecret, 'name' | 'value' | 'expiresAt'> & {
isLegacy?: boolean;
};
function Expired({ expiresAt }: { readonly expiresAt: Date }) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<Tooltip
content={t('application_details.secrets.expired_tooltip', {
date: expiresAt.toLocaleString(),
})}
>
<span className={styles.expired}>{t('application_details.secrets.expired')}</span>
</Tooltip>
);
}
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<Column<ApplicationSecretRow>> = useMemo(
() => [
{
title: t('general.name'),
dataIndex: 'name',
colSpan: 3,
render: ({ name }) => <span>{name}</span>,
},
{
title: t('application_details.secrets.value'),
dataIndex: 'value',
colSpan: 6,
render: ({ value }) => (
<CopyToClipboard
hasVisibilityToggle
displayType="block"
value={value}
className={styles.copyToClipboard}
variant="text"
/>
),
},
{
title: t('application_details.secrets.expires_at'),
dataIndex: 'expiresAt',
colSpan: 3,
render: ({ expiresAt }) => (
<span>
{expiresAt ? (
compareDesc(expiresAt, new Date()) === 1 ? (
<Expired expiresAt={new Date(expiresAt)} />
) : (
new Date(expiresAt).toLocaleString()
)
) : (
t('application_details.secrets.never')
)}
</span>
),
},
{
title: '',
dataIndex: 'actions',
render: ({ name, isLegacy }) => (
<ActionsButton
fieldName="application_details.application_secret"
deleteConfirmation="application_details.secrets.delete_confirmation"
onDelete={async () => {
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;
};

View file

@ -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<SelectedGuide>();
@ -89,6 +91,7 @@ function GuideDrawer({ app, onClose }: Props) {
className={styles.guide}
guideId={selectedGuide.id}
app={app}
secrets={secrets}
onClose={() => {
setSelectedGuide(undefined);
}}

View file

@ -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;
}

View file

@ -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
]}
/>
<Drawer isOpen={isReadmeOpen} onClose={onCloseDrawer}>
<GuideDrawer app={data} onClose={onCloseDrawer} />
<GuideDrawer app={data} secrets={secrets} onClose={onCloseDrawer} />
</Drawer>
<DeleteConfirmModal
isOpen={isDeleteFormOpen}

View file

@ -4,17 +4,19 @@ import Modal from 'react-modal';
import ModalHeader from '@/components/Guide/ModalHeader';
import modalStyles from '@/scss/modal.module.scss';
import { type ApplicationSecretRow } from '../ApplicationDetailsContent/EndpointsAndCredentials';
import AppGuide from '../components/AppGuide';
import styles from './index.module.scss';
type Props = {
readonly guideId: string;
readonly app?: ApplicationResponse;
readonly app: ApplicationResponse;
readonly secrets: ApplicationSecretRow[];
readonly onClose: () => void;
};
function GuideModal({ guideId, app, onClose }: Props) {
function GuideModal({ guideId, app, secrets, onClose }: Props) {
return (
<Modal shouldCloseOnEsc isOpen className={modalStyles.fullScreen} onRequestClose={onClose}>
<div className={styles.modalContainer}>
@ -27,7 +29,13 @@ function GuideModal({ guideId, app, onClose }: Props) {
requestSuccessMessage="guide.request_guide_successfully"
onClose={onClose}
/>
<AppGuide className={styles.guide} guideId={guideId} app={app} onClose={onClose} />
<AppGuide
className={styles.guide}
guideId={guideId}
app={app}
secrets={secrets}
onClose={onClose}
/>
</div>
</Modal>
);

View file

@ -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) {
<Guide
className={className}
guideId={guideId}
isEmpty={!app && !guide}
isEmpty={!guide}
isLoading={!app}
onClose={onClose}
/>

View file

@ -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<ApplicationResponse, RequestError>(
id && `api/applications/${id}`
);
const secrets = useSWR<ApplicationSecretRow[], RequestError>(`api/applications/${id}/secrets`);
const oidcConfig = useSWR<SnakeCaseOidcConfig, RequestError>(openIdProviderConfigPath);
const {
data: oidcConfig,
error: fetchOidcConfigError,
mutate: mutateOidcConfig,
} = useSWR<SnakeCaseOidcConfig, RequestError>(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 <Daisy />;
}
return (
<GuideModal
guideId={guideId}
app={data}
secrets={secrets.data}
onClose={() => {
navigate(`/applications/${id}`);
}}
@ -49,14 +55,16 @@ function ApplicationDetails() {
error={requestError}
onRetry={() => {
void mutate();
void mutateOidcConfig();
void oidcConfig.mutate();
void secrets.mutate();
}}
>
<PageMeta titleKey="application_details.page_title" />
{data && oidcConfig && (
{data && oidcConfig.data && secrets.data && (
<ApplicationDetailsContent
data={data}
oidcConfig={oidcConfig}
oidcConfig={oidcConfig.data}
secrets={secrets.data}
onApplicationUpdated={mutate}
/>
)}

View file

@ -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<Array<{ role: Role }>>) =>
roles.some(({ role: { name } }) => name === InternalRole.Admin);

View file

@ -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',
},