0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): application details (#2460)

This commit is contained in:
Xiao Yijun 2022-11-18 16:59:56 +08:00 committed by GitHub
parent 3c9edb9ca4
commit e4f77447c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 173 additions and 117 deletions

View file

@ -55,7 +55,6 @@ const Main = () => {
<Route path=":id">
<Route index element={<Navigate replace to="settings" />} />
<Route path="settings" element={<ApplicationDetails />} />
<Route path="advanced-settings" element={<ApplicationDetails />} />
</Route>
</Route>
<Route path="api-resources">

View file

@ -7,6 +7,7 @@
flex-shrink: 0;
width: 248px;
overflow-y: auto;
margin-bottom: _.unit(6);
> div + div {
margin-top: _.unit(6);

View file

@ -15,13 +15,12 @@
.content {
flex-grow: 1;
display: flex;
margin-bottom: _.unit(6);
overflow: hidden;
}
.main {
flex-grow: 1;
padding: 0 _.unit(3) 0 _.unit(2);
padding: 0 _.unit(2);
overflow-y: scroll;
> * {

View file

@ -0,0 +1,35 @@
@use '@/scss/underscore' as _;
.container {
padding: _.unit(6) _.unit(8);
display: flex;
}
.introduction {
width: 296px;
margin-right: _.unit(14);
flex-shrink: 0;
> :not(:first-child) {
margin-top: _.unit(2);
}
.title {
@include _.subhead-cap;
color: var(--color-neutral-variant-60);
}
.description {
font: var(--font-body-medium);
color: var(--color-text-secondary);
a {
color: var(--color-text-link);
text-decoration: none;
}
}
}
.form {
flex-grow: 1;
}

View file

@ -0,0 +1,32 @@
import type { AdminConsoleKey } from '@logto/phrases';
import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Card from '../Card';
import * as styles from './index.module.scss';
type Props = {
title: AdminConsoleKey;
description: AdminConsoleKey;
children: ReactNode;
};
const FormCard = ({ title, description, children }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<Card className={styles.container}>
<div className={styles.introduction}>
<div className={styles.title}>{t(title)}</div>
<div className={styles.description}>
{t(description)} {/* TODO: @Yijun update this link when @Guamian is ready for this */}{' '}
<Link to="#">{t('general.learn_more')}</Link>
</div>
</div>
<div className={styles.form}>{children}</div>
</Card>
);
};
export default FormCard;

View file

@ -3,5 +3,5 @@
.nav {
border-bottom: 1px solid var(--color-divider);
display: flex;
margin: _.unit(1) 0;
margin-top: _.unit(1);
}

View file

@ -1,44 +1,31 @@
import type { Application, SnakeCaseOidcConfig } from '@logto/schemas';
import { ApplicationType, UserRole } from '@logto/schemas';
import { useEffect } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import CopyToClipboard from '@/components/CopyToClipboard';
import FormCard from '@/components/FormCard';
import FormField from '@/components/FormField';
import Switch from '@/components/Switch';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import * as styles from '../index.module.scss';
type Props = {
applicationType: ApplicationType;
oidcConfig: SnakeCaseOidcConfig;
defaultData: Application;
isDeleted: boolean;
};
const AdvancedSettings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props) => {
const {
control,
reset,
formState: { isDirty },
} = useFormContext<Application>();
const AdvancedSettings = ({ applicationType, oidcConfig }: Props) => {
const { control } = useFormContext<Application>();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
useEffect(() => {
reset(defaultData);
return () => {
reset(defaultData);
};
}, [reset, defaultData]);
return (
<>
<FormCard
title="application_details.advanced_settings"
description="application_details.advanced_settings_description"
>
<FormField
title="application_details.authorization_endpoint"
className={styles.textField}
tooltip="application_details.authorization_endpoint_tip"
>
<CopyToClipboard
@ -83,8 +70,7 @@ const AdvancedSettings = ({ applicationType, oidcConfig, defaultData, isDeleted
/>
</FormField>
)}
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} />
</>
</FormCard>
);
};

View file

@ -1,43 +1,32 @@
import type { Application, SnakeCaseOidcConfig } from '@logto/schemas';
import type { Application } from '@logto/schemas';
import { ApplicationType, validateRedirectUrl } from '@logto/schemas';
import { useEffect } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import CopyToClipboard from '@/components/CopyToClipboard';
import FormCard from '@/components/FormCard';
import FormField from '@/components/FormField';
import MultiTextInput from '@/components/MultiTextInput';
import type { MultiTextInputRule } from '@/components/MultiTextInput/types';
import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils';
import TextInput from '@/components/TextInput';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import { uriOriginValidator } from '@/utilities/validator';
import * as styles from '../index.module.scss';
type Props = {
applicationType: ApplicationType;
oidcConfig: SnakeCaseOidcConfig;
defaultData: Application;
isDeleted: boolean;
data: Application;
};
const Settings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props) => {
const Settings = ({ data }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
control,
register,
reset,
formState: { errors, isDirty },
formState: { errors },
} = useFormContext<Application>();
useEffect(() => {
reset(defaultData);
return () => {
reset(defaultData);
};
}, [reset, defaultData]);
const { id, secret, type: applicationType } = data;
const isNativeApp = applicationType === ApplicationType.Native;
const uriPatternRules: MultiTextInputRule = {
@ -48,36 +37,35 @@ const Settings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props
};
return (
<>
<FormField
isRequired
title="application_details.application_name"
className={styles.textField}
>
<FormCard
title="application_details.settings"
description="application_details.settings_description"
>
<FormField isRequired title="application_details.application_name">
<TextInput
{...register('name', { required: true })}
hasError={Boolean(errors.name)}
placeholder={t('application_details.application_name_placeholder')}
/>
</FormField>
<FormField title="application_details.description" className={styles.textField}>
<FormField title="application_details.description">
<TextInput
{...register('description')}
placeholder={t('application_details.description_placeholder')}
/>
</FormField>
<FormField title="application_details.application_id" className={styles.textField}>
<CopyToClipboard className={styles.textField} value={defaultData.id} variant="border" />
<FormField title="application_details.application_id">
<CopyToClipboard value={id} variant="border" className={styles.textField} />
</FormField>
{[ApplicationType.Traditional, ApplicationType.MachineToMachine].includes(
applicationType
) && (
<FormField title="application_details.application_secret" className={styles.textField}>
<FormField title="application_details.application_secret">
<CopyToClipboard
hasVisibilityToggle
className={styles.textField}
value={defaultData.secret}
value={secret}
variant="border"
className={styles.textField}
/>
</FormField>
)}
@ -85,7 +73,6 @@ const Settings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props
<FormField
isRequired
title="application_details.redirect_uris"
className={styles.textField}
tooltip="application_details.redirect_uri_tip"
>
<Controller
@ -117,7 +104,6 @@ const Settings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props
{applicationType !== ApplicationType.MachineToMachine && (
<FormField
title="application_details.post_sign_out_redirect_uris"
className={styles.textField}
tooltip="application_details.post_sign_out_redirect_uri_tip"
>
<Controller
@ -142,7 +128,6 @@ const Settings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props
{applicationType !== ApplicationType.MachineToMachine && (
<FormField
title="application_details.cors_allowed_origins"
className={styles.textField}
tooltip="application_details.cors_allowed_origins_tip"
>
<Controller
@ -169,8 +154,7 @@ const Settings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props
/>
</FormField>
)}
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} />
</>
</FormCard>
);
};

View file

@ -18,18 +18,19 @@
}
}
.body {
> :first-child {
margin-top: 0;
.formContent {
>:not(:first-child) {
margin-top: _.unit(4);
}
.form {
margin-top: _.unit(8);
}
.fieldsContent {
> :not(:first-child) {
margin-top: _.unit(4);
}
.fields {
padding-bottom: _.unit(10);
flex: 1;
&:last-child {
margin-bottom: _.unit(6);
}
}
.textField {

View file

@ -1,6 +1,5 @@
import type { Application, SnakeCaseOidcConfig } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
@ -20,7 +19,9 @@ import DeleteConfirmModal from '@/components/DeleteConfirmModal';
import DetailsSkeleton from '@/components/DetailsSkeleton';
import Drawer from '@/components/Drawer';
import LinkButton from '@/components/LinkButton';
import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar';
import TabNav, { TabNavItem } from '@/components/TabNav';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useDocumentationUrl from '@/hooks/use-documentation-url';
@ -62,7 +63,7 @@ const ApplicationDetails = () => {
const {
handleSubmit,
reset,
formState: { isSubmitting },
formState: { isSubmitting, isDirty },
} = formMethods;
useEffect(() => {
@ -123,8 +124,6 @@ const ApplicationDetails = () => {
setIsReadmeOpen(false);
};
const isAdvancedSettings = pathname.includes('advanced-settings');
return (
<div className={detailsStyles.container}>
<LinkButton
@ -200,51 +199,29 @@ const ApplicationDetails = () => {
</DeleteConfirmModal>
</div>
</Card>
<Card className={classNames(styles.body, detailsStyles.body)}>
<TabNav>
<TabNavItem href={`/applications/${data.id}/settings`}>
{t('general.settings_nav')}
</TabNavItem>
<TabNavItem href={`/applications/${data.id}/advanced-settings`}>
{t('application_details.advanced_settings')}
</TabNavItem>
</TabNav>
<FormProvider {...formMethods}>
<form className={classNames(styles.form, detailsStyles.body)} onSubmit={onSubmit}>
<div className={styles.fields}>
{isAdvancedSettings && (
<AdvancedSettings
applicationType={data.type}
oidcConfig={oidcConfig}
defaultData={data}
isDeleted={isDeleted}
/>
)}
{!isAdvancedSettings && (
<Settings
applicationType={data.type}
oidcConfig={oidcConfig}
defaultData={data}
isDeleted={isDeleted}
/>
)}
</div>
<div className={detailsStyles.footer}>
<div className={detailsStyles.footerMain}>
<Button
isLoading={isSubmitting}
htmlType="submit"
type="primary"
size="large"
title="general.save_changes"
/>
</div>
</div>
</form>
</FormProvider>
</Card>
<TabNav>
<TabNavItem href={`/applications/${data.id}/settings`}>
{t('general.settings_nav')}
</TabNavItem>
</TabNav>
<FormProvider {...formMethods}>
<form className={styles.formContent} onSubmit={onSubmit}>
<div className={styles.fieldsContent}>
<Settings data={data} />
<AdvancedSettings applicationType={data.type} oidcConfig={oidcConfig} />
</div>
<SubmitFormChangesActionBar
isOpen={isDirty}
isSubmitting={isSubmitting}
onDiscard={() => {
reset();
}}
/>
</form>
</FormProvider>
</>
)}
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} />
</div>
);
};

View file

@ -22,7 +22,7 @@
}
@mixin form-text-field {
width: dim.$form-text-field-width;
width: 100%;
}
@mixin vertical-bar {

View file

@ -1,7 +1,12 @@
const application_details = {
back_to_applications: 'Zurück zu Anwendungen',
check_guide: 'Zur Anleitung',
settings: 'Settings', // UNTRANSLATED
settings_description:
'With Applications you can. Setup a mobile, web or IoT application to use Auth0 for Authentication. Configure the allowed Callback URLs and Secrets for your Application.', // UNTRANSLATED
advanced_settings: 'Erweiterte Einstellungen',
advanced_settings_description:
'It real sent your at. Amounted all shy set why followed declared.', // UNTRANSLATED
application_name: 'Anwendungsname',
application_name_placeholder: 'Meine App',
description: 'Beschreibung',

View file

@ -40,6 +40,7 @@ const general = {
type_to_search: 'Tippe um zu suchen',
got_it: 'Got it', // UNTRANSLATED
page_info: '{{min, number}}-{{max, number}} of {{total, number}}', // UNTRANSLATED
learn_more: 'Learn more', // UNTRANSLATED
};
export default general;

View file

@ -1,7 +1,12 @@
const application_details = {
back_to_applications: 'Back to Applications',
check_guide: 'Check Guide',
settings: 'Settings',
settings_description:
'With Applications you can. Setup a mobile, web or IoT application to use Auth0 for Authentication. Configure the allowed Callback URLs and Secrets for your Application.',
advanced_settings: 'Advanced settings',
advanced_settings_description:
'It real sent your at. Amounted all shy set why followed declared.',
application_name: 'Application name',
application_name_placeholder: 'My App',
description: 'Description',

View file

@ -39,6 +39,7 @@ const general = {
type_to_search: 'Type to search',
got_it: 'Got it',
page_info: '{{min, number}}-{{max, number}} of {{total, number}}',
learn_more: 'Learn more',
};
export default general;

View file

@ -1,7 +1,12 @@
const application_details = {
back_to_applications: 'Retour aux applications',
check_guide: 'Aller voir le guide',
settings: 'Settings', // UNTRANSLATED
settings_description:
'With Applications you can. Setup a mobile, web or IoT application to use Auth0 for Authentication. Configure the allowed Callback URLs and Secrets for your Application.', // UNTRANSLATED
advanced_settings: 'Paramètres avancés',
advanced_settings_description:
'It real sent your at. Amounted all shy set why followed declared.', // UNTRANSLATED
application_name: "Nom de l'application",
application_name_placeholder: 'Mon App',
description: 'Description',

View file

@ -40,6 +40,7 @@ const general = {
type_to_search: 'Type to search', // UNTRANSLATED
got_it: 'Got it', // UNTRANSLATED
page_info: '{{min, number}}-{{max, number}} of {{total, number}}', // UNTRANSLATED
learn_more: 'Learn more', // UNTRANSLATED
};
export default general;

View file

@ -1,7 +1,12 @@
const application_details = {
back_to_applications: '어플리케이션으로 돌아가기',
check_guide: '가이드 확인',
settings: 'Settings', // UNTRANSLATED
settings_description:
'With Applications you can. Setup a mobile, web or IoT application to use Auth0 for Authentication. Configure the allowed Callback URLs and Secrets for your Application.', // UNTRANSLATED
advanced_settings: '고급 설정',
advanced_settings_description:
'It real sent your at. Amounted all shy set why followed declared.', // UNTRANSLATED
application_name: '어플리케이션 이름',
application_name_placeholder: '나의 앱',
description: '설명',

View file

@ -39,6 +39,7 @@ const general = {
type_to_search: 'Type to search', // UNTRANSLATED
got_it: 'Got it', // UNTRANSLATED
page_info: '{{min, number}}-{{max, number}} of {{total, number}}', // UNTRANSLATED
learn_more: 'Learn more', // UNTRANSLATED
};
export default general;

View file

@ -1,7 +1,12 @@
const application_details = {
back_to_applications: 'Voltar para aplicações',
check_guide: 'Guia de verificação',
settings: 'Settings', // UNTRANSLATED
settings_description:
'With Applications you can. Setup a mobile, web or IoT application to use Auth0 for Authentication. Configure the allowed Callback URLs and Secrets for your Application.', // UNTRANSLATED
advanced_settings: 'Configurações avançadas',
advanced_settings_description:
'It real sent your at. Amounted all shy set why followed declared.', // UNTRANSLATED
application_name: 'Nome da aplicação',
application_name_placeholder: 'Ex: Site Empresa',
description: 'Descrição',

View file

@ -39,6 +39,7 @@ const general = {
type_to_search: 'Type to search', // UNTRANSLATED
got_it: 'Got it', // UNTRANSLATED
page_info: '{{min, number}}-{{max, number}} of {{total, number}}', // UNTRANSLATED
learn_more: 'Learn more', // UNTRANSLATED
};
export default general;

View file

@ -1,7 +1,12 @@
const application_details = {
back_to_applications: 'Uygulamalara geri dön',
check_guide: 'Kılavuza Göz At',
settings: 'Settings', // UNTRANSLATED
settings_description:
'With Applications you can. Setup a mobile, web or IoT application to use Auth0 for Authentication. Configure the allowed Callback URLs and Secrets for your Application.', // UNTRANSLATED
advanced_settings: 'Gelişmiş Ayarlar',
advanced_settings_description:
'It real sent your at. Amounted all shy set why followed declared.', // UNTRANSLATED
application_name: 'Uygulama Adı',
application_name_placeholder: 'Uygulamam',
description: 'Açıklama',

View file

@ -40,6 +40,7 @@ const general = {
type_to_search: 'Type to search', // UNTRANSLATED
got_it: 'Got it', // UNTRANSLATED
page_info: '{{min, number}}-{{max, number}} of {{total, number}}', // UNTRANSLATED
learn_more: 'Learn more', // UNTRANSLATED
};
export default general;

View file

@ -1,7 +1,12 @@
const application_details = {
back_to_applications: '返回全部应用',
check_guide: '查看指南',
settings: '设置',
settings_description:
'With Applications you can. Setup a mobile, web or IoT application to use Auth0 for Authentication. Configure the allowed Callback URLs and Secrets for your Application.', // UNTRANSLATED
advanced_settings: '高级设置',
advanced_settings_description:
'It real sent your at. Amounted all shy set why followed declared.', // UNTRANSLATED
application_name: '应用名称',
application_name_placeholder: '我的应用',
description: '描述',

View file

@ -39,6 +39,7 @@ const general = {
type_to_search: '输入搜索',
got_it: 'Got it', // UNTRANSLATED
page_info: '{{min, number}}-{{max, number}} 共 {{total, number}} 条', // UNTRANSLATED
learn_more: 'Learn more', // UNTRANSLATED
};
export default general;