mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat(console): add loading masks for protected app (#5357)
This commit is contained in:
parent
6f6d56ed7c
commit
a671a4d9bf
4 changed files with 95 additions and 5 deletions
|
@ -115,3 +115,28 @@
|
|||
padding-inline-start: _.unit(5);
|
||||
}
|
||||
}
|
||||
|
||||
.loadingSkeleton {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(2);
|
||||
padding: _.unit(6);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-divider);
|
||||
|
||||
.bone {
|
||||
@include _.shimmering-animation;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ import {
|
|||
type CustomDomain as CustomDomainType,
|
||||
} from '@logto/schemas';
|
||||
import { cond } from '@silverhand/essentials';
|
||||
import { type ChangeEvent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { type ChangeEvent, useState } from 'react';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
@ -41,10 +42,15 @@ const maxSessionDuration = 365; // 1 year
|
|||
function ProtectedAppSettings({ data }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const { data: customDomains = [], mutate } = useSWR<CustomDomainType[]>(
|
||||
const {
|
||||
data: customDomains = [],
|
||||
isLoading: isLoadingCustomDomain,
|
||||
mutate,
|
||||
} = useSWR<CustomDomainType[]>(
|
||||
`api/applications/${data.id}/protected-app-metadata/custom-domains`
|
||||
);
|
||||
const api = useApi();
|
||||
const [isDeletingCustomDomain, setIsDeletingCustomDomain] = useState(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
|
@ -69,6 +75,8 @@ function ProtectedAppSettings({ data }: Props) {
|
|||
customDomain?.status === DomainStatus.Active ? customDomain.domain : host
|
||||
}`;
|
||||
|
||||
const showCustomDomainLoadingMask = isLoadingCustomDomain || isDeletingCustomDomain;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormCard
|
||||
|
@ -124,7 +132,13 @@ function ProtectedAppSettings({ data }: Props) {
|
|||
</FormField>
|
||||
{!!host && (
|
||||
<FormField title="domain.custom.custom_domain_field">
|
||||
{!customDomain && (
|
||||
{showCustomDomainLoadingMask && (
|
||||
<div className={styles.loadingSkeleton}>
|
||||
<div className={classNames(styles.bone, styles.title)} />
|
||||
<div className={classNames(styles.bone, styles.description)} />
|
||||
</div>
|
||||
)}
|
||||
{!customDomain && !showCustomDomainLoadingMask && (
|
||||
<AddDomainForm
|
||||
className={styles.customDomain}
|
||||
onSubmitCustomDomain={async (json) => {
|
||||
|
@ -136,15 +150,17 @@ function ProtectedAppSettings({ data }: Props) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{customDomain && (
|
||||
{customDomain && !showCustomDomainLoadingMask && (
|
||||
<CustomDomain
|
||||
hasOpenExternalLink
|
||||
className={styles.customDomain}
|
||||
customDomain={customDomain}
|
||||
onDeleteCustomDomain={async () => {
|
||||
setIsDeletingCustomDomain(true);
|
||||
await api.delete(
|
||||
`api/applications/${data.id}/protected-app-metadata/custom-domains/${customDomain.domain}`
|
||||
);
|
||||
setIsDeletingCustomDomain(false);
|
||||
void mutate();
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -61,3 +61,39 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loadingSkeleton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: _.unit(6);
|
||||
padding: _.unit(6) _.unit(8);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-divider);
|
||||
|
||||
.bone {
|
||||
@include _.shimmering-animation;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.columnWrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(2);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { type Application } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -16,7 +17,7 @@ import * as styles from './index.module.scss';
|
|||
function ProtectedAppCreationForm() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getTo } = useTenantPathname();
|
||||
const { data, mutate } = useSWR<Application[]>('api/applications?types=Protected');
|
||||
const { data, isLoading, mutate } = useSWR<Application[]>('api/applications?types=Protected');
|
||||
const { hasAppsReachedLimit } = useApplicationsUsage();
|
||||
const hasProtectedApps = !!data?.length;
|
||||
const showCreationForm = !hasProtectedApps && !hasAppsReachedLimit;
|
||||
|
@ -28,6 +29,18 @@ function ProtectedAppCreationForm() {
|
|||
[data, mutate]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.loadingSkeleton}>
|
||||
<div className={classNames(styles.bone, styles.icon)} />
|
||||
<div className={styles.columnWrapper}>
|
||||
<div className={classNames(styles.bone, styles.title)} />
|
||||
<div className={classNames(styles.bone, styles.description)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ProtectedAppCard hasCreateButton={!showCreationForm} onCreateSuccess={mutateApps} />
|
||||
|
|
Loading…
Add table
Reference in a new issue