0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-14 23:11:31 -05:00

feat(console,phrases): implement environment variables input field (2/2) (#5473)

feat(console,phrases): implement environment variables input field

implement environment variables input field
This commit is contained in:
simeng-li 2024-03-07 16:40:37 +08:00 committed by GitHub
parent fa3577e491
commit 4e27e3465e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 194 additions and 12 deletions

View file

@ -0,0 +1,114 @@
import { useCallback, useMemo } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import FormField from '@/ds-components/FormField';
import KeyValueInputField from '@/ds-components/KeyValueInputField';
import { type JwtClaimsFormType } from '../type';
const isValidKey = (key: string) => {
return /^\w+$/.test(key);
};
function EnvironmentVariablesField() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
register,
getValues,
trigger,
formState: {
errors: { environmentVariables: envVariableErrors },
submitCount,
},
} = useFormContext<JwtClaimsFormType>();
// Read the form controller from the context @see {@link https://react-hook-form.com/docs/usefieldarray}
const { fields, remove, append } = useFieldArray<JwtClaimsFormType>({
name: 'environmentVariables',
});
const keyValidator = useCallback(
(key: string, index: number) => {
const envVariables = getValues('environmentVariables');
if (!envVariables) {
return true;
}
// Unique key validation
if (envVariables.filter(({ key: _key }) => _key.length > 0 && _key === key).length > 1) {
// Reuse the same error phrase key from webhook settings
return t('webhook_details.settings.key_duplicated_error');
}
// Empty key validation (if value is present)
const correspondValue = getValues(`environmentVariables.${index}.value`);
if (correspondValue) {
// Reuse the same error phrase key from webhook settings
return Boolean(key) || t('webhook_details.settings.key_missing_error');
}
// Key format validation
if (Boolean(key) && !isValidKey(key)) {
// Reuse the same error phrase key from webhook settings
return t('webhook_details.settings.invalid_key_error');
}
return true;
},
[getValues, t]
);
const valueValidator = useCallback(
(value: string, index: number) => {
return getValues(`environmentVariables.${index}.value`)
? Boolean(value) || t('webhook_details.settings.value_missing_error')
: true;
},
[getValues, t]
);
const revalidate = useCallback(() => {
for (const [index] of fields.entries()) {
void trigger(`environmentVariables.${index}.key`);
// Only trigger value validation if the form has been submitted
if (submitCount > 0) {
void trigger(`environmentVariables.${index}.value`);
}
}
}, [fields, submitCount, trigger]);
const getInputFieldProps = useMemo(
() => ({
key: (index: number) =>
register(`environmentVariables.${index}.key`, {
validate: (key) => keyValidator(key, index),
onChange: revalidate,
}),
value: (index: number) =>
register(`environmentVariables.${index}.value`, {
validate: (value) => valueValidator(value, index),
onChange: revalidate,
}),
}),
[register, revalidate, keyValidator, valueValidator]
);
return (
<FormField title="jwt_claims.environment_variables.input_field_title">
<KeyValueInputField
fields={fields}
// Force envVariableErrors to be an array, otherwise return undefined
errors={envVariableErrors?.map?.((error) => error)}
getInputFieldProps={getInputFieldProps}
onAppend={append}
onRemove={remove}
/>
</FormField>
);
}
export default EnvironmentVariablesField;

View file

@ -16,6 +16,7 @@ import {
fetchExternalDataCodeExample,
} from '../config';
import EnvironmentVariablesField from './EnvironmentVariablesField';
import GuideCard, { CardType } from './GuideCard';
import * as styles from './index.module.scss';
@ -107,7 +108,19 @@ function RightPanel({ tokenType }: Props) {
options={fetchExternalDataEditorOptions}
/>
</GuideCard>
<GuideCard name={CardType.EnvironmentVariables} />
<GuideCard name={CardType.EnvironmentVariables}>
{/**
* We use useFieldArray hook to manage the list of environment variables in the EnvironmentVariablesField component.
* useFieldArray will read the form context and return the necessary methods and values to manage the list.
* The form context will mutate when the tokenType changes. It will provide different form state and methods based on the tokenType. (@see JwtClaims component.)
* However, the form context/controller updates did not trigger a re-render of the useFieldArray hook. (@see {@link https://github.com/react-hook-form/react-hook-form/blob/master/src/useFieldArray.ts#L95})
*
* This cause issues when the tokenType changes and the environment variables list is not rerendered. The form state will be stale.
* In order to fix this, we need to re-render the EnvironmentVariablesField component when the tokenType changes.
* Achieve this by adding a key to the EnvironmentVariablesField component. Force a re-render when the tokenType changes.
*/}
<EnvironmentVariablesField key={tokenType} />
</GuideCard>
</div>
</div>
);

View file

@ -2,6 +2,7 @@ import { withAppInsights } from '@logto/app-insights/react/AppInsightsReact';
import classNames from 'classnames';
import type { TFuncKey } from 'i18next';
import { useMemo } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import Card from '@/ds-components/Card';
@ -13,6 +14,7 @@ import { type Model } from './MonacoCodeEditor/type';
import RightPanel from './RightPanel';
import { userJwtFile, machineToMachineJwtFile, JwtTokenType } from './config';
import * as styles from './index.module.scss';
import { type JwtClaimsFormType } from './type';
export { JwtTokenType } from './config';
@ -39,6 +41,18 @@ const getPath = (tab: JwtTokenType) => `/jwt-claims/${tab}`;
function JwtClaims({ tab }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const userJwtClaimsForm = useForm<JwtClaimsFormType>({
defaultValues: {
environmentVariables: [{ key: '', value: '' }],
},
});
const machineToMachineJwtClaimsForm = useForm<JwtClaimsFormType>({
defaultValues: {
environmentVariables: [{ key: '', value: '' }],
},
});
// TODO: API integration, read/write the custom claims code value
const activeModel = useMemo<Model>(() => {
return tab === JwtTokenType.UserAccessToken ? userJwtFile : machineToMachineJwtFile;
@ -58,17 +72,23 @@ function JwtClaims({ tab }: Props) {
</TabNavItem>
))}
</TabNav>
<form className={classNames(styles.tabContent)}>
<Card className={styles.codePanel}>
<div className={styles.cardTitle}>
{t('jwt_claims.code_editor_title', {
token: t(`jwt_claims.${phrases.token[tab]}`),
})}
</div>
<MonacoCodeEditor className={styles.flexGrow} models={[activeModel]} />
</Card>
<RightPanel tokenType={tab} />
</form>
<FormProvider
{...(tab === JwtTokenType.UserAccessToken
? userJwtClaimsForm
: machineToMachineJwtClaimsForm)}
>
<form className={classNames(styles.tabContent)}>
<Card className={styles.codePanel}>
<div className={styles.cardTitle}>
{t('jwt_claims.code_editor_title', {
token: t(`jwt_claims.${phrases.token[tab]}`),
})}
</div>
<MonacoCodeEditor className={styles.flexGrow} models={[activeModel]} />
</Card>
<RightPanel tokenType={tab} />
</form>
</FormProvider>
</div>
);
}

View file

@ -0,0 +1,6 @@
export type JwtClaimsFormType = {
script?: string;
environmentVariables?: Array<{ key: string; value: string }>;
contextSample?: string;
tokenSample?: string;
};

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -31,6 +31,7 @@ const jwt_claims = {
title: 'Set environment variables',
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
input_field_title: 'Add environment variables',
},
jwt_claims_hint:
'Limit custom claims to under 50KB. Default JWT claims are automatically included in the token and can not be overridden.',

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -50,6 +50,8 @@ const jwt_claims = {
/** UNTRANSLATED */
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
},
/** UNTRANSLATED */
jwt_claims_hint: