0
Fork 0
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:
Xiao Yijun 2022-03-18 16:02:37 +08:00 committed by GitHub
parent 97c3aa37ea
commit 3c0ff36817
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 254 additions and 102 deletions

View file

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

View 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;

View file

@ -7,6 +7,7 @@
padding: _.unit(0.5) _.unit(1);
border-radius: _.unit(1);
text-decoration: none;
cursor: pointer;
svg {
fill: var(--color-primary);

View 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;

View file

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

View file

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

View file

@ -1,6 +1,7 @@
const translation = {
general: {
placeholder: 'Placeholder',
add_another: '+ Add Another',
},
sign_in: {
action: 'Sign In',

View file

@ -3,6 +3,7 @@ import en from './en';
const translation = {
general: {
placeholder: '占位符',
add_another: '+ Add Another',
},
sign_in: {
action: '登录',

View file

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