mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(console): add another
for redirect uris in application details (#387)
This commit is contained in:
parent
97c3aa37ea
commit
3c0ff36817
9 changed files with 254 additions and 102 deletions
|
@ -0,0 +1,17 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.multilineInput {
|
||||
> :not(:first-child) {
|
||||
margin-top: _.unit(2);
|
||||
}
|
||||
|
||||
.deletableInput {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.textField {
|
||||
@include _.form-text-field;
|
||||
margin-right: _.unit(3);
|
||||
}
|
||||
}
|
||||
}
|
71
packages/console/src/components/MultilineInput/index.tsx
Normal file
71
packages/console/src/components/MultilineInput/index.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as textButtonStyles from '@/components/TextButton/index.module.scss';
|
||||
import Minus from '@/icons/Minus';
|
||||
|
||||
import IconButton from '../IconButton';
|
||||
import TextInput from '../TextInput';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
};
|
||||
|
||||
const MultilineInput = ({ value, onChange }: Props) => {
|
||||
const { t } = useTranslation(undefined, {
|
||||
keyPrefix: 'general',
|
||||
});
|
||||
|
||||
const fields = useMemo(() => {
|
||||
if (value.length === 0) {
|
||||
return [''];
|
||||
}
|
||||
|
||||
return value;
|
||||
}, [value]);
|
||||
|
||||
const handleAdd = () => {
|
||||
onChange([...fields, '']);
|
||||
};
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
onChange(fields.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleInputChange = (event: React.FormEvent<HTMLInputElement>, index: number) => {
|
||||
onChange(fields.map((value, i) => (i === index ? event.currentTarget.value : value)));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.multilineInput}>
|
||||
{fields.map((fieldValue, fieldIndex) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={fieldIndex} className={styles.deletableInput}>
|
||||
<TextInput
|
||||
className={styles.textField}
|
||||
value={fieldValue}
|
||||
onChange={(event) => {
|
||||
handleInputChange(event, fieldIndex);
|
||||
}}
|
||||
/>
|
||||
{fields.length > 1 && (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleRemove(fieldIndex);
|
||||
}}
|
||||
>
|
||||
<Minus />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className={textButtonStyles.button} onClick={handleAdd}>
|
||||
{t('add_another')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultilineInput;
|
|
@ -7,6 +7,7 @@
|
|||
padding: _.unit(0.5) _.unit(1);
|
||||
border-radius: _.unit(1);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
fill: var(--color-primary);
|
||||
|
|
12
packages/console/src/icons/Minus.tsx
Normal file
12
packages/console/src/icons/Minus.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React, { SVGProps } from 'react';
|
||||
|
||||
const Minus = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M9.00008 0.666748C7.35191 0.666748 5.74074 1.15549 4.37033 2.07117C2.99992 2.98685 1.93182 4.28834 1.30109 5.81105C0.670359 7.33377 0.505331 9.00933 0.826874 10.6258C1.14842 12.2423 1.94209 13.7272 3.10753 14.8926C4.27297 16.0581 5.75782 16.8517 7.37433 17.1733C8.99084 17.4948 10.6664 17.3298 12.1891 16.6991C13.7118 16.0683 15.0133 15.0002 15.929 13.6298C16.8447 12.2594 17.3334 10.6483 17.3334 9.00008C17.3334 7.90573 17.1179 6.8221 16.6991 5.81105C16.2803 4.80001 15.6665 3.88135 14.8926 3.10752C14.1188 2.3337 13.2002 1.71987 12.1891 1.30109C11.1781 0.882296 10.0944 0.666748 9.00008 0.666748ZM9.00008 15.6667C7.68154 15.6667 6.39261 15.2758 5.29628 14.5432C4.19996 13.8107 3.34547 12.7695 2.84089 11.5513C2.3363 10.3331 2.20428 8.99269 2.46152 7.69948C2.71875 6.40627 3.35369 5.21839 4.28604 4.28604C5.21839 3.35369 6.40628 2.71875 7.69948 2.46151C8.99269 2.20428 10.3331 2.3363 11.5513 2.84088C12.7695 3.34547 13.8107 4.19995 14.5432 5.29628C15.2758 6.39261 15.6668 7.68154 15.6668 9.00008C15.6668 10.7682 14.9644 12.4639 13.7141 13.7141C12.4639 14.9644 10.7682 15.6667 9.00008 15.6667ZM12.3334 8.16675H5.66675C5.44574 8.16675 5.23378 8.25455 5.0775 8.41083C4.92122 8.56711 4.83342 8.77907 4.83342 9.00008C4.83342 9.2211 4.92122 9.43306 5.0775 9.58934C5.23378 9.74562 5.44574 9.83342 5.66675 9.83342H12.3334C12.5544 9.83342 12.7664 9.74562 12.9227 9.58934C13.079 9.43306 13.1668 9.2211 13.1668 9.00008C13.1668 8.77907 13.079 8.56711 12.9227 8.41083C12.7664 8.25455 12.5544 8.16675 12.3334 8.16675Z"
|
||||
fill="#747778"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Minus;
|
|
@ -1,16 +1,21 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
> *:not(:first-child) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
> :not(:first-child) {
|
||||
margin-top: _.unit(4);
|
||||
}
|
||||
}
|
||||
|
||||
.container .header {
|
||||
padding: _.unit(8);
|
||||
flex: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: _.unit(8);
|
||||
|
||||
> *:not(:first-child) {
|
||||
margin-left: _.unit(6);
|
||||
|
@ -19,24 +24,21 @@
|
|||
.metadata {
|
||||
flex: 1;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: _.unit(2);
|
||||
}
|
||||
|
||||
> *:not(:first-child) {
|
||||
margin-left: _.unit(2);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font: var(--font-title-large);
|
||||
color: var(--color-component-text);
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: _.unit(2);
|
||||
|
||||
> :not(:first-child) {
|
||||
margin-left: _.unit(2);
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
background-color: var(--color-neutral-90);
|
||||
padding: _.unit(0.5) _.unit(2);
|
||||
|
@ -61,32 +63,46 @@
|
|||
}
|
||||
|
||||
.container .body {
|
||||
> :not(:first-child) {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.tabContent {
|
||||
form {
|
||||
>:not(:first-child) {
|
||||
margin-top: _.unit(6);
|
||||
.form {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: auto;
|
||||
|
||||
> *:not(:first-child) {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.fields {
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: _.unit(6) 0;
|
||||
overflow: auto;
|
||||
|
||||
> div {
|
||||
@include _.form-text-field;
|
||||
}
|
||||
|
||||
.fields {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: _.unit(6);
|
||||
.textField {
|
||||
@include _.form-text-field;
|
||||
}
|
||||
|
||||
> div {
|
||||
@include _.form-text-field;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@include _.form-text-field;
|
||||
.listFields {
|
||||
> *:not(:first-child) {
|
||||
margin-top: _.unit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
text-align: right;
|
||||
}
|
||||
.submit {
|
||||
flex: 0;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Application } from '@logto/schemas';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useController, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
@ -11,6 +11,7 @@ import Card from '@/components/Card';
|
|||
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||
import FormField from '@/components/FormField';
|
||||
import ImagePlaceholder from '@/components/ImagePlaceholder';
|
||||
import MultilineInput from '@/components/MultilineInput';
|
||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
|
@ -18,6 +19,7 @@ import { applicationTypeI18nKey } from '@/types/applications';
|
|||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
// TODO LOG-1908: OidcConfig in Application Details
|
||||
type OidcConfig = {
|
||||
authorization_endpoint: string;
|
||||
userinfo_endpoint: string;
|
||||
|
@ -27,45 +29,118 @@ type OidcConfig = {
|
|||
const ApplicationDetails = () => {
|
||||
const { id } = useParams();
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const { data, error } = useSWR<Application, RequestError>(id && `/api/applications/${id}`);
|
||||
// TODO LOG-1908: OidcConfig in Application Details
|
||||
const { data: oidcConfig, error: fetchOidcConfigError } = useSWR<OidcConfig, RequestError>(
|
||||
'/oidc/.well-known/openid-configuration'
|
||||
);
|
||||
const isLoading = !data && !error && !fetchOidcConfigError;
|
||||
const dataFetched = data && oidcConfig;
|
||||
const isLoading = !data && !error && !oidcConfig && !fetchOidcConfigError;
|
||||
|
||||
const { handleSubmit, register, reset } = useForm<Application>({
|
||||
defaultValues: data,
|
||||
});
|
||||
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const isAdvancedSettings = location.pathname.includes('advanced-settings');
|
||||
const { control, handleSubmit, register, reset } = useForm<Application>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
reset(data);
|
||||
}, [data, reset]);
|
||||
|
||||
const {
|
||||
field: { value: redirectUris, onChange: onRedirectUriChange },
|
||||
} = useController({
|
||||
control,
|
||||
name: 'oidcClientMetadata.redirectUris',
|
||||
defaultValue: [],
|
||||
});
|
||||
|
||||
const {
|
||||
field: { value: postSignOutRedirectUris, onChange: onPostSignOutRedirectUriChange },
|
||||
} = useController({
|
||||
control,
|
||||
name: 'oidcClientMetadata.postLogoutRedirectUris',
|
||||
defaultValue: [],
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit((formData) => {
|
||||
console.log(formData);
|
||||
});
|
||||
|
||||
const isAdvancedSettings = location.pathname.includes('advanced-settings');
|
||||
|
||||
const SettingsPage = useMemo(() => {
|
||||
return (
|
||||
oidcConfig && (
|
||||
<>
|
||||
<FormField isRequired title="admin_console.application_details.application_name">
|
||||
<TextInput {...register('name', { required: true })} />
|
||||
</FormField>
|
||||
<FormField title="admin_console.application_details.description">
|
||||
<TextInput {...register('description')} />
|
||||
</FormField>
|
||||
<FormField title="admin_console.application_details.authorization_endpoint">
|
||||
<CopyToClipboard
|
||||
className={styles.textField}
|
||||
value={oidcConfig.authorization_endpoint}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="admin_console.application_details.redirect_uri">
|
||||
<MultilineInput
|
||||
value={redirectUris}
|
||||
onChange={(value) => {
|
||||
onRedirectUriChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="admin_console.application_details.post_sign_out_redirect_uri">
|
||||
<MultilineInput
|
||||
value={postSignOutRedirectUris}
|
||||
onChange={(value) => {
|
||||
onPostSignOutRedirectUriChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}, [
|
||||
oidcConfig,
|
||||
onPostSignOutRedirectUriChange,
|
||||
onRedirectUriChange,
|
||||
postSignOutRedirectUris,
|
||||
redirectUris,
|
||||
register,
|
||||
]);
|
||||
|
||||
const AdvancedSettingsPage = useMemo(() => {
|
||||
return (
|
||||
oidcConfig && (
|
||||
<>
|
||||
<FormField title="admin_console.application_details.token_endpoint">
|
||||
<CopyToClipboard className={styles.textField} value={oidcConfig.token_endpoint} />
|
||||
</FormField>
|
||||
<FormField title="admin_console.application_details.user_info_endpoint">
|
||||
<CopyToClipboard className={styles.textField} value={oidcConfig.userinfo_endpoint} />
|
||||
</FormField>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}, [oidcConfig]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<BackLink to="/applications">{t('application_details.back_to_applications')}</BackLink>
|
||||
{isLoading && <div>loading</div>}
|
||||
{error && <div>{`error occurred: ${error.metadata.code}`}</div>}
|
||||
{dataFetched && (
|
||||
{data && oidcConfig && (
|
||||
<>
|
||||
<Card className={styles.header}>
|
||||
<ImagePlaceholder size={76} borderRadius={16} />
|
||||
<div className={styles.metadata}>
|
||||
<div className={styles.name}>{data.name}</div>
|
||||
<div>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.type}>{t(`${applicationTypeI18nKey[data.type]}.title`)}</div>
|
||||
<div className={styles.verticalBar} />
|
||||
<div className={styles.text}>App ID</div>
|
||||
|
@ -85,54 +160,18 @@ const ApplicationDetails = () => {
|
|||
{t('application_details.advanced_settings')}
|
||||
</TabNavLink>
|
||||
</TabNav>
|
||||
<div className={styles.tabContent}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className={styles.fields}>
|
||||
{!isAdvancedSettings && (
|
||||
<>
|
||||
<FormField
|
||||
isRequired
|
||||
title="admin_console.application_details.application_name"
|
||||
>
|
||||
<TextInput {...register('name', { required: true })} />
|
||||
</FormField>
|
||||
<FormField title="admin_console.application_details.description">
|
||||
<TextInput {...register('description')} />
|
||||
</FormField>
|
||||
<FormField title="admin_console.application_details.authorization_endpoint">
|
||||
<CopyToClipboard
|
||||
className={styles.copy}
|
||||
value={oidcConfig.authorization_endpoint}
|
||||
/>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
{isAdvancedSettings && (
|
||||
<>
|
||||
<FormField title="admin_console.application_details.token_endpoint">
|
||||
<CopyToClipboard
|
||||
className={styles.copy}
|
||||
value={oidcConfig.token_endpoint}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="admin_console.application_details.user_info_endpoint">
|
||||
<CopyToClipboard
|
||||
className={styles.copy}
|
||||
value={oidcConfig.userinfo_endpoint}
|
||||
/>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.submit}>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
title="admin_console.application_details.save_changes"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form className={styles.form} onSubmit={onSubmit}>
|
||||
<div className={styles.fields}>
|
||||
{isAdvancedSettings ? AdvancedSettingsPage : SettingsPage}
|
||||
</div>
|
||||
<div className={styles.submit}>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
title="admin_console.application_details.save_changes"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const translation = {
|
||||
general: {
|
||||
placeholder: 'Placeholder',
|
||||
add_another: '+ Add Another',
|
||||
},
|
||||
sign_in: {
|
||||
action: 'Sign In',
|
||||
|
|
|
@ -3,6 +3,7 @@ import en from './en';
|
|||
const translation = {
|
||||
general: {
|
||||
placeholder: '占位符',
|
||||
add_another: '+ Add Another',
|
||||
},
|
||||
sign_in: {
|
||||
action: '登录',
|
||||
|
|
|
@ -10108,12 +10108,6 @@ packages:
|
|||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/nanoid/3.3.0:
|
||||
resolution: {integrity: sha512-JzxqqT5u/x+/KOFSd7JP15DOo9nOoHpx6DYatqIHUW2+flybkm+mdcraotSQR5WcnZr+qhGVh8Ted0KdfSMxlg==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanoid/3.3.1:
|
||||
resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
|
@ -11514,7 +11508,7 @@ packages:
|
|||
resolution: {integrity: sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
dependencies:
|
||||
nanoid: 3.3.0
|
||||
nanoid: 3.3.1
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
|
Loading…
Reference in a new issue