0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

refactor(console,phrases): move permissions and roles to step 2 in org guide (#4858)

This commit is contained in:
Charles Zhao 2023-11-13 10:46:19 +08:00 committed by GitHub
parent 73f348af89
commit a8b164ca54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 455 additions and 472 deletions

View file

@ -19,7 +19,7 @@ import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import useApi, { type RequestError } from '@/hooks/use-api';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import IntroductionAndPermissions from '../Organizations/Guide/IntroductionAndPermissions';
import IntroductionAndPermissions from '../Organizations/Guide/Introduction';
import Members from './Members';
import Settings from './Settings';

View file

@ -1,165 +0,0 @@
import { type OrganizationRoleWithScopes, Theme, type OrganizationRole } from '@logto/schemas';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import RbacFeatureDark from '@/assets/icons/rbac-feature-dark.svg';
import RbacFeature from '@/assets/icons/rbac-feature.svg';
import ActionBar from '@/components/ActionBar';
import OrganizationScopesSelect from '@/components/OrganizationScopesSelect';
import Button from '@/ds-components/Button';
import Card from '@/ds-components/Card';
import FormField from '@/ds-components/FormField';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import { type Option } from '@/ds-components/Select/MultiSelect';
import TextInput from '@/ds-components/TextInput';
import useApi, { type RequestError } from '@/hooks/use-api';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import useTheme from '@/hooks/use-theme';
import { trySubmitSafe } from '@/utils/form';
import { organizationRolePath } from '../../RoleModal';
import DynamicFormFields from '../DynamicFormFields';
import { steps } from '../const';
import styles from '../index.module.scss';
type RoleForm = {
roles: Array<Omit<OrganizationRole, 'tenantId' | 'id'> & { scopes: Array<Option<string>> }>;
};
const defaultRoles = { name: '', description: '', scopes: [] };
function CreateRoles() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
const theme = useTheme();
const Icon = theme === Theme.Light ? RbacFeature : RbacFeatureDark;
const { navigate } = useTenantPathname();
const api = useApi();
const { data, error } = useSWR<OrganizationRoleWithScopes[], RequestError>(
'api/organization-roles'
);
const [keyword, setKeyword] = useState('');
const {
control,
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isDirty },
} = useForm<RoleForm>({
defaultValues: {
roles: [defaultRoles],
},
});
useEffect(() => {
if (data?.length) {
reset({
roles: data.map(({ name, description, scopes }) => ({
name,
description,
scopes: scopes.map(({ id, name }) => ({ value: id, title: name })),
})),
});
}
}, [data, reset]);
const { fields, append, remove } = useFieldArray({ control, name: 'roles' });
const onSubmit = handleSubmit(
trySubmitSafe(async ({ roles }) => {
// If user has pre-saved data and no changes, skip submit and go directly to next step
if (data?.length && !isDirty) {
navigate(`../${steps.createOrganization}`);
return;
}
if (data?.length) {
await Promise.all(data.map(async ({ id }) => api.delete(`${organizationRolePath}/${id}`)));
}
await Promise.all(
roles.map(async ({ name, description, scopes }) => {
const { id } = await api
.post(organizationRolePath, { json: { name, description } })
.json<OrganizationRole>();
if (scopes.length > 0) {
await api.put(`${organizationRolePath}/${id}/scopes`, {
json: { organizationScopeIds: scopes.map(({ value }) => value) },
});
}
})
);
navigate(`../${steps.createOrganization}`);
})
);
const onNavigateBack = () => {
reset();
setKeyword('');
navigate(`../${steps.createPermissions}`);
};
return (
<>
<OverlayScrollbar className={styles.stepContainer}>
<div className={classNames(styles.content)}>
<Card className={styles.card}>
<Icon className={styles.icon} />
<div className={styles.section}>
<div className={styles.title}>{t('step_2')}</div>
<div className={styles.description}>{t('step_2_description')}</div>
</div>
<form>
<DynamicFormFields
isLoading={!data && !error}
title="organizations.guide.organization_roles"
fields={fields}
render={(index) => (
<div className={styles.fieldGroup}>
<FormField isRequired title="organizations.guide.role_name">
<TextInput
{...register(`roles.${index}.name`, { required: true })}
error={Boolean(errors.roles?.[index]?.name)}
/>
</FormField>
<FormField title="general.description">
<TextInput {...register(`roles.${index}.description`)} />
</FormField>
<FormField title="organizations.guide.permissions">
<Controller
name={`roles.${index}.scopes`}
control={control}
render={({ field: { onChange, value } }) => (
<OrganizationScopesSelect
keyword={keyword}
setKeyword={setKeyword}
value={value}
onChange={onChange}
/>
)}
/>
</FormField>
</div>
)}
onAdd={() => {
append(defaultRoles);
}}
onRemove={remove}
/>
</form>
</Card>
</div>
</OverlayScrollbar>
<ActionBar step={2} totalSteps={3}>
<Button isLoading={isSubmitting} title="general.next" type="primary" onClick={onSubmit} />
<Button title="general.back" onClick={onNavigateBack} />
</ActionBar>
</>
);
}
export default CreateRoles;

View file

@ -3,13 +3,7 @@ import * as styles from './index.module.scss';
function Diagram() {
return (
<svg
width="100%"
height="auto"
viewBox="0 0 762 630"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="100%" viewBox="0 0 762 630" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="762" height="629.523" fill="none" />
<path d="M93.234 89.1221L279.704 89.589" stroke="#8E9192" strokeWidth="0.71719" />
<path d="M283.131 89.561L278.11 93.0809L278.11 86.041L283.131 89.561Z" fill="#8E9192" />

View file

@ -0,0 +1,130 @@
import { Theme } from '@logto/schemas';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import OrganizationFeatureDark from '@/assets/icons/organization-feature-dark.svg';
import OrganizationFeature from '@/assets/icons/organization-feature.svg';
import ActionBar from '@/components/ActionBar';
import Button from '@/ds-components/Button';
import Card from '@/ds-components/Card';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import useTheme from '@/hooks/use-theme';
import { steps } from '../const';
import * as parentStyles from '../index.module.scss';
import Diagram from './Diagram';
import * as styles from './index.module.scss';
const icons = {
[Theme.Light]: { OrganizationIcon: OrganizationFeature },
[Theme.Dark]: { OrganizationIcon: OrganizationFeatureDark },
};
type Props = {
/* True if the guide is in the "Check guide" drawer of organization details page */
isReadonly?: boolean;
};
function Introduction({ isReadonly }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
const { navigate } = useTenantPathname();
const theme = useTheme();
const { OrganizationIcon } = icons[theme];
return (
<>
<OverlayScrollbar className={parentStyles.stepContainer}>
<div className={classNames(parentStyles.content)}>
<Card className={parentStyles.card}>
<OrganizationIcon className={parentStyles.icon} />
<div className={styles.container}>
<div className={styles.section}>
<div className={styles.title}>{t('introduction.section_1.title')}</div>
<div className={styles.description}>{t('introduction.section_1.description')}</div>
</div>
<div className={styles.title}>{t('introduction.section_2.title')}</div>
<div className={styles.section}>
<div className={styles.subtitle}>{t('organization_permissions')}</div>
<div className={styles.description}>
{t('introduction.section_2.organization_permission_description')}
</div>
<div className={styles.panel}>
<div className={styles.header}>{t('organization_permissions')}</div>
<div className={styles.body}>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('read_resource')}</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('edit_resource')}</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('delete_resource')}</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('ellipsis')}</div>
</div>
</div>
</div>
</div>
<div className={styles.section}>
<div className={styles.subtitle}>{t('organization_roles')}</div>
<div className={styles.description}>
{t('introduction.section_2.organization_role_description')}
</div>
<div className={styles.panel}>
<div className={styles.header}>{t('organization_roles')}</div>
<div className={styles.body}>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('admin')}</div>
<div className={styles.items}>
<div>{t('read_resource')}</div>
<div>{t('edit_resource')}</div>
<div>{t('delete_resource')}</div>
</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('member')}</div>
<div className={styles.items}>
<div>{t('read_resource')}</div>
<div>{t('edit_resource')}</div>
</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('guest')}</div>
<div className={styles.items}>
<div>{t('read_resource')}</div>
</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('ellipsis')}</div>
</div>
</div>
</div>
</div>
<div className={styles.section}>
<div className={styles.title}>{t('introduction.section_3.title')}</div>
<div className={styles.description}>{t('introduction.section_3.description')}</div>
<Diagram />
</div>
</div>
</Card>
</div>
</OverlayScrollbar>
{!isReadonly && (
<ActionBar step={1} totalSteps={3}>
<Button
title="general.next"
type="primary"
onClick={() => {
navigate(`../${steps.permissionsAndRoles}`);
}}
/>
</ActionBar>
)}
</>
);
}
export default Introduction;

View file

@ -1,83 +0,0 @@
import { useTranslation } from 'react-i18next';
import Diagram from './Diagram';
import * as styles from './index.module.scss';
function Introduction() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
return (
<div className={styles.container}>
<div className={styles.section}>
<div className={styles.title}>{t('introduction.section_1.title')}</div>
<div className={styles.description}>{t('introduction.section_1.description')}</div>
</div>
<div className={styles.title}>{t('introduction.section_2.title')}</div>
<div className={styles.section}>
<div className={styles.subtitle}>{t('organization_permissions')}</div>
<div className={styles.description}>
{t('introduction.section_2.organization_permission_description')}
</div>
<div className={styles.panel}>
<div className={styles.header}>{t('organization_permissions')}</div>
<div className={styles.body}>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('read_resource')}</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('edit_resource')}</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('delete_resource')}</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('ellipsis')}</div>
</div>
</div>
</div>
</div>
<div className={styles.section}>
<div className={styles.subtitle}>{t('organization_roles')}</div>
<div className={styles.description}>
{t('introduction.section_2.organization_role_description')}
</div>
<div className={styles.panel}>
<div className={styles.header}>{t('organization_roles')}</div>
<div className={styles.body}>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('admin')}</div>
<div className={styles.items}>
<div>{t('read_resource')}</div>
<div>{t('edit_resource')}</div>
<div>{t('delete_resource')}</div>
</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('member')}</div>
<div className={styles.items}>
<div>{t('read_resource')}</div>
<div>{t('edit_resource')}</div>
</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('guest')}</div>
<div className={styles.items}>
<div>{t('read_resource')}</div>
</div>
</div>
<div className={styles.cell}>
<div className={styles.cellTitle}>{t('ellipsis')}</div>
</div>
</div>
</div>
</div>
<div className={styles.section}>
<div className={styles.title}>{t('introduction.section_3.title')}</div>
<div className={styles.description}>{t('introduction.section_3.description')}</div>
<Diagram />
</div>
</div>
);
}
export default Introduction;

View file

@ -1,149 +0,0 @@
import { type OrganizationScope, Theme } from '@logto/schemas';
import classNames from 'classnames';
import { useEffect } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import OrganizationFeatureDark from '@/assets/icons/organization-feature-dark.svg';
import OrganizationFeature from '@/assets/icons/organization-feature.svg';
import PermissionFeatureDark from '@/assets/icons/permission-feature-dark.svg';
import PermissionFeature from '@/assets/icons/permission-feature.svg';
import ActionBar from '@/components/ActionBar';
import Button from '@/ds-components/Button';
import Card from '@/ds-components/Card';
import FormField from '@/ds-components/FormField';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import TextInput from '@/ds-components/TextInput';
import useApi, { type RequestError } from '@/hooks/use-api';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import useTheme from '@/hooks/use-theme';
import { trySubmitSafe } from '@/utils/form';
import { organizationScopesPath } from '../../PermissionModal';
import DynamicFormFields from '../DynamicFormFields';
import { steps } from '../const';
import * as styles from '../index.module.scss';
import Introduction from './Introduction';
const icons = {
[Theme.Light]: { OrganizationIcon: OrganizationFeature, PermissionIcon: PermissionFeature },
[Theme.Dark]: {
OrganizationIcon: OrganizationFeatureDark,
PermissionIcon: PermissionFeatureDark,
},
};
type PermissionForm = {
permissions: Array<Omit<OrganizationScope, 'id' | 'tenantId'>>;
};
type Props = {
/* True if the guide is in the "Check guide" drawer of organization details page */
isReadonly?: boolean;
};
const defaultPermission = { name: '', description: '' };
function IntroductionAndPermissions({ isReadonly }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
const theme = useTheme();
const { OrganizationIcon, PermissionIcon } = icons[theme];
const { navigate } = useTenantPathname();
const api = useApi();
const { data, error } = useSWR<OrganizationScope[], RequestError>('api/organization-scopes');
const {
control,
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isDirty },
} = useForm<PermissionForm>({
defaultValues: {
permissions: [defaultPermission],
},
});
useEffect(() => {
if (data?.length) {
reset({ permissions: data.map(({ name, description }) => ({ name, description })) });
}
}, [data, reset]);
const { fields, append, remove } = useFieldArray({ control, name: 'permissions' });
const onSubmit = handleSubmit(
trySubmitSafe(async ({ permissions }) => {
// If user has pre-saved data and no changes, skip submit and go directly to next step
if (data?.length && !isDirty) {
navigate(`../${steps.createRoles}`);
return;
}
if (data?.length) {
await Promise.all(
data.map(async ({ id }) => api.delete(`${organizationScopesPath}/${id}`))
);
}
await Promise.all(
permissions.map(async ({ name, description }) => {
await api.post(organizationScopesPath, { json: { name, description } });
})
);
navigate(`../${steps.createRoles}`);
})
);
return (
<>
<OverlayScrollbar className={styles.stepContainer}>
<div className={classNames(styles.content)}>
<Card className={styles.card}>
<OrganizationIcon className={styles.icon} />
<Introduction />
</Card>
{!isReadonly && (
<Card className={styles.card}>
<PermissionIcon className={styles.icon} />
<div className={styles.title}>{t('step_1')}</div>
<form>
<DynamicFormFields
isLoading={!data && !error}
title="organizations.guide.organization_permissions"
fields={fields}
render={(index) => (
<div className={styles.fieldGroup}>
<FormField isRequired title="organizations.guide.permission_name">
<TextInput
{...register(`permissions.${index}.name`, { required: true })}
error={Boolean(errors.permissions?.[index]?.name)}
/>
</FormField>
<FormField title="general.description">
<TextInput {...register(`permissions.${index}.description`)} />
</FormField>
</div>
)}
onAdd={() => {
append(defaultPermission);
}}
onRemove={remove}
/>
</form>
</Card>
)}
</div>
</OverlayScrollbar>
{!isReadonly && (
<ActionBar step={1} totalSteps={3}>
<Button isLoading={isSubmitting} title="general.next" type="primary" onClick={onSubmit} />
</ActionBar>
)}
</>
);
}
export default IntroductionAndPermissions;

View file

@ -25,8 +25,8 @@ type OrganizationForm = {
name: string;
};
function CreateOrganization() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
function OrganizationInfo() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations' });
const theme = useTheme();
const Icon = theme === Theme.Light ? OrganizationFeature : OrganizationFeatureDark;
const { navigate } = useTenantPathname();
@ -52,7 +52,7 @@ function CreateOrganization() {
const onNavigateBack = () => {
reset();
navigate(`../${steps.createRoles}`);
navigate(`../${steps.permissionsAndRoles}`);
};
return (
@ -62,19 +62,23 @@ function CreateOrganization() {
<Card className={styles.card}>
<Icon className={styles.icon} />
<div className={styles.section}>
<div className={styles.title}>{t('step_3')}</div>
<div className={styles.description}>{t('step_3_description')}</div>
<div className={styles.title}>{t('guide.step_3')}</div>
<div className={styles.description}>{t('guide.step_3_description')}</div>
</div>
<form>
<FormField isRequired title="organizations.guide.organization_name">
<TextInput {...register('name', { required: true })} error={Boolean(errors.name)} />
<TextInput
{...register('name', { required: true })}
error={Boolean(errors.name)}
placeholder={t('organization_name_placeholder')}
/>
</FormField>
</form>
</Card>
<Card className={styles.card}>
<div className={styles.section}>
<div className={styles.title}>{t('more_next_steps')}</div>
<div className={styles.subtitle}>{t('add_members')}</div>
<div className={styles.title}>{t('guide.more_next_steps')}</div>
<div className={styles.subtitle}>{t('guide.add_members')}</div>
{/* eslint-disable-next-line no-warning-comments */}
{/* TODO: @charles Documentation links will be updated later */}
<ul>
@ -84,11 +88,11 @@ function CreateOrganization() {
rel="noopener"
href="https://docs.logto.io/docs/tutorials/"
>
{t('add_members_action')}
{t('guide.add_members_action')}
</TextLink>
</li>
</ul>
<div className={styles.subtitle}>{t('add_enterprise_connector')}</div>
<div className={styles.subtitle}>{t('guide.add_enterprise_connector')}</div>
<ul>
<li>
<TextLink
@ -96,7 +100,7 @@ function CreateOrganization() {
rel="noopener"
href="https://docs.logto.io/docs/tutorials/"
>
{t('add_enterprise_connector_action')}
{t('guide.add_enterprise_connector_action')}
</TextLink>
</li>
</ul>
@ -112,4 +116,4 @@ function CreateOrganization() {
);
}
export default CreateOrganization;
export default OrganizationInfo;

View file

@ -0,0 +1,252 @@
import {
type OrganizationRoleWithScopes,
Theme,
type OrganizationRole,
type OrganizationScope,
} from '@logto/schemas';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import PermissionFeatureDark from '@/assets/icons/permission-feature-dark.svg';
import PermissionFeature from '@/assets/icons/permission-feature.svg';
import RbacFeatureDark from '@/assets/icons/rbac-feature-dark.svg';
import RbacFeature from '@/assets/icons/rbac-feature.svg';
import ActionBar from '@/components/ActionBar';
import OrganizationScopesSelect from '@/components/OrganizationScopesSelect';
import Button from '@/ds-components/Button';
import Card from '@/ds-components/Card';
import FormField from '@/ds-components/FormField';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import { type Option } from '@/ds-components/Select/MultiSelect';
import TextInput from '@/ds-components/TextInput';
import useApi, { type RequestError } from '@/hooks/use-api';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import useTheme from '@/hooks/use-theme';
import { trySubmitSafe } from '@/utils/form';
import { organizationScopesPath } from '../../PermissionModal';
import { organizationRolePath } from '../../RoleModal';
import DynamicFormFields from '../DynamicFormFields';
import { steps } from '../const';
import * as styles from '../index.module.scss';
type Form = {
permissions: Array<Omit<OrganizationScope, 'id' | 'tenantId'>>;
roles: Array<Omit<OrganizationRole, 'tenantId' | 'id'> & { scopes: Array<Option<string>> }>;
};
const icons = {
[Theme.Light]: { PermissionIcon: PermissionFeature, RbacIcon: RbacFeature },
[Theme.Dark]: { PermissionIcon: PermissionFeatureDark, RbacIcon: RbacFeatureDark },
};
const defaultPermission = { name: '', description: '' };
const defaultRoles = { name: '', description: '', scopes: [] };
function PermissionsAndRoles() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations' });
const theme = useTheme();
const { PermissionIcon, RbacIcon } = icons[theme];
const { navigate } = useTenantPathname();
const api = useApi();
const { data: permissionsData, error: permissionsError } = useSWR<
OrganizationScope[],
RequestError
>('api/organization-scopes');
const { data: rolesData, error: rolesError } = useSWR<OrganizationRoleWithScopes[], RequestError>(
'api/organization-roles'
);
const [keyword, setKeyword] = useState('');
const {
control,
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isDirty },
} = useForm<Form>({
defaultValues: {
permissions: [defaultPermission],
roles: [defaultRoles],
},
});
useEffect(() => {
if (permissionsData?.length) {
reset({
permissions: permissionsData.map(({ name, description }) => ({ name, description })),
});
}
}, [permissionsData, reset]);
useEffect(() => {
if (rolesData?.length) {
reset({
roles: rolesData.map(({ name, description, scopes }) => ({
name,
description,
scopes: scopes.map(({ id, name }) => ({ value: id, title: name })),
})),
});
}
}, [rolesData, reset]);
const permissionFields = useFieldArray({ control, name: 'permissions' });
const roleFields = useFieldArray({ control, name: 'roles' });
const onSubmit = handleSubmit(
trySubmitSafe(async ({ permissions, roles }) => {
// If user has pre-saved data but with no changes made this time,
// skip form submit and go directly to the next step.
if ((Boolean(permissionsData?.length) || Boolean(rolesData?.length)) && !isDirty) {
navigate(`../${steps.organizationInfo}`);
return;
}
// If there's pre-saved permissions, remove them first
if (permissionsData?.length) {
await Promise.all(
permissionsData.map(async ({ id }) => api.delete(`${organizationScopesPath}/${id}`))
);
}
// Create new permissions
if (permissions.length > 0) {
await Promise.all(
permissions.map(async ({ name, description }) => {
await api.post(organizationScopesPath, { json: { name, description } });
})
);
}
// Remove pre-saved roles
if (rolesData?.length) {
await Promise.all(
rolesData.map(async ({ id }) => api.delete(`${organizationRolePath}/${id}`))
);
}
// Create new roles
if (roles.length > 0) {
await Promise.all(
roles.map(async ({ name, description, scopes }) => {
const { id } = await api
.post(organizationRolePath, { json: { name, description } })
.json<OrganizationRole>();
if (scopes.length > 0) {
await api.put(`${organizationRolePath}/${id}/scopes`, {
json: { organizationScopeIds: scopes.map(({ value }) => value) },
});
}
})
);
}
navigate(`../${steps.organizationInfo}`);
})
);
const onNavigateBack = () => {
reset();
setKeyword('');
navigate(`../${steps.introduction}`);
};
return (
<>
<OverlayScrollbar className={styles.stepContainer}>
<div className={classNames(styles.content)}>
<Card className={styles.card}>
<PermissionIcon className={styles.icon} />
<div className={styles.title}>{t('guide.step_1')}</div>
<form>
<DynamicFormFields
isLoading={!permissionsData && !permissionsError}
title="organizations.guide.organization_permissions"
fields={permissionFields.fields}
render={(index) => (
<div className={styles.fieldGroup}>
<FormField title="organizations.guide.permission_name">
<TextInput
{...register(`permissions.${index}.name`)}
error={Boolean(errors.permissions?.[index]?.name)}
placeholder="read:appointment"
/>
</FormField>
<FormField title="general.description">
<TextInput
{...register(`permissions.${index}.description`)}
placeholder={t('create_permission_placeholder')}
/>
</FormField>
</div>
)}
onAdd={() => {
permissionFields.append(defaultPermission);
}}
onRemove={permissionFields.remove}
/>
</form>
</Card>
<Card className={styles.card}>
<RbacIcon className={styles.icon} />
<div className={styles.section}>
<div className={styles.title}>{t('guide.step_2')}</div>
<div className={styles.description}>{t('guide.step_2_description')}</div>
</div>
<form>
<DynamicFormFields
isLoading={!rolesData && !rolesError}
title="organizations.guide.organization_roles"
fields={roleFields.fields}
render={(index) => (
<div className={styles.fieldGroup}>
<FormField title="organizations.guide.role_name">
<TextInput
{...register(`roles.${index}.name`)}
error={Boolean(errors.roles?.[index]?.name)}
placeholder="viewer"
/>
</FormField>
<FormField title="general.description">
<TextInput
{...register(`roles.${index}.description`)}
placeholder={t('create_role_placeholder')}
/>
</FormField>
<FormField title="organizations.guide.permissions">
<Controller
name={`roles.${index}.scopes`}
control={control}
render={({ field: { onChange, value } }) => (
<OrganizationScopesSelect
keyword={keyword}
setKeyword={setKeyword}
value={value}
onChange={onChange}
/>
)}
/>
</FormField>
</div>
)}
onAdd={() => {
roleFields.append(defaultRoles);
}}
onRemove={roleFields.remove}
/>
</form>
</Card>
</div>
</OverlayScrollbar>
<ActionBar step={2} totalSteps={3}>
<Button isLoading={isSubmitting} title="general.next" type="primary" onClick={onSubmit} />
<Button title="general.back" onClick={onNavigateBack} />
</ActionBar>
</>
);
}
export default PermissionsAndRoles;

View file

@ -1,5 +1,5 @@
export const steps = Object.freeze({
createPermissions: 'create-permissions',
createRoles: 'create-roles',
createOrganization: 'create-organization',
introduction: 'introduction',
permissionsAndRoles: 'permissions-and-roles',
organizationInfo: 'organization-info',
});

View file

@ -6,9 +6,9 @@ import DsModalHeader from '@/ds-components/ModalHeader';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import * as modalStyles from '@/scss/modal.module.scss';
import CreateOrganization from './CreateOrganization';
import CreateRoles from './CreateRoles';
import IntroductionAndPermissions from './IntroductionAndPermissions';
import Introduction from './Introduction';
import OrganizationInfo from './OrganizationInfo';
import PermissionsAndRoles from './PermissionsAndRoles';
import { steps } from './const';
import * as styles from './index.module.scss';
@ -28,10 +28,10 @@ function Guide() {
onClose={onClose}
/>
<Routes>
<Route index element={<Navigate replace to={steps.createPermissions} />} />
<Route path={steps.createPermissions} element={<IntroductionAndPermissions />} />
<Route path={steps.createRoles} element={<CreateRoles />} />
<Route path={steps.createOrganization} element={<CreateOrganization />} />
<Route index element={<Navigate replace to={steps.introduction} />} />
<Route path={steps.introduction} element={<Introduction />} />
<Route path={steps.permissionsAndRoles} element={<PermissionsAndRoles />} />
<Route path={steps.organizationInfo} element={<OrganizationInfo />} />
</Routes>
</div>
</Modal>

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -11,14 +11,14 @@ const organization = {
organization_list_placeholder_text:
'Organization is usually used in SaaS or SaaS-like multi-tenancy apps. The Organizations feature allows your B2B customers to better manage their partners and customers, and to customize the ways that end-users access their applications.',
organization_name_placeholder: 'My organization',
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
organization_permission: 'Organization permission',
organization_permission_other: 'Organization permissions',
organization_permission_description:
'Organization permission refers to the authorization to access a resource in the context of organization. An organization permission should be represented as a meaningful string, also serving as the name and unique identifier.',
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
permission: 'Permission',
permission_other: 'Permissions',
organization_role: 'Organization role',
@ -28,7 +28,7 @@ const organization = {
organization_role_delete_confirm:
'Doing so will remove the permissions associated with this role from the affected users and delete the relations among organization roles, members in the organization, and organization permissions.',
role: 'Role',
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
search_placeholder: 'Search by organization name or ID',
search_permission_placeholder: 'Type to search and select permissions',
search_role_placeholder: 'Type to search and select roles',

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */

View file

@ -22,7 +22,7 @@ const organizations = {
/** UNTRANSLATED */
organization_name_placeholder: 'My organization',
/** UNTRANSLATED */
organization_description_placeholder: 'A brief description of the organization.',
organization_description_placeholder: 'A brief description of the organization',
/** UNTRANSLATED */
organization_permission: 'Organization permission',
/** UNTRANSLATED */
@ -34,7 +34,7 @@ const organizations = {
organization_permission_delete_confirm:
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
/** UNTRANSLATED */
create_permission_placeholder: 'Read appointment history.',
create_permission_placeholder: 'Read appointment history',
/** UNTRANSLATED */
permission: 'Permission',
/** UNTRANSLATED */
@ -52,7 +52,7 @@ const organizations = {
/** UNTRANSLATED */
role: 'Role',
/** UNTRANSLATED */
create_role_placeholder: 'Users with view-only permissions.',
create_role_placeholder: 'Users with view-only permissions',
/** UNTRANSLATED */
search_placeholder: 'Search by organization name or ID',
/** UNTRANSLATED */