Skip to content

Commit

Permalink
feat(console): add org role details general settings page (#5610)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoyijun authored Apr 2, 2024
1 parent e035957 commit 0f5347b
Show file tree
Hide file tree
Showing 36 changed files with 721 additions and 15 deletions.
5 changes: 5 additions & 0 deletions packages/console/src/consts/page-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@ export enum OrganizationTemplateTabs {
OrganizationRoles = 'organization-roles',
OrganizationPermissions = 'organization-permissions',
}

export enum OrganizationRoleDetailsTabs {
Permissions = 'permissions',
General = 'general',
}
31 changes: 19 additions & 12 deletions packages/console/src/containers/ConsoleContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import GetStarted from '@/pages/GetStarted';
import Mfa from '@/pages/Mfa';
import NotFound from '@/pages/NotFound';
import OrganizationDetails from '@/pages/OrganizationDetails';
import OrganizationRoleDetails from '@/pages/OrganizationRoleDetails';
import OrganizationTemplate from '@/pages/OrganizationTemplate';
import OrganizationPermissions from '@/pages/OrganizationTemplate/OrganizationPermissions';
import OrganizationRoles from '@/pages/OrganizationTemplate/OrganizationRoles';
Expand Down Expand Up @@ -187,20 +188,26 @@ function ConsoleContent() {
</Route>
</Route>
{isDevFeaturesEnabled && (
<Route path="organization-template" element={<OrganizationTemplate />}>
<>
<Route path="organization-template" element={<OrganizationTemplate />}>
<Route
index
element={<Navigate replace to={OrganizationTemplateTabs.OrganizationRoles} />}
/>
<Route
path={OrganizationTemplateTabs.OrganizationRoles}
element={<OrganizationRoles />}
/>
<Route
path={OrganizationTemplateTabs.OrganizationPermissions}
element={<OrganizationPermissions />}
/>
</Route>
<Route
index
element={<Navigate replace to={OrganizationTemplateTabs.OrganizationRoles} />}
/>
<Route
path={OrganizationTemplateTabs.OrganizationRoles}
element={<OrganizationRoles />}
path={`organization-template/${OrganizationTemplateTabs.OrganizationRoles}/:id/*`}
element={<OrganizationRoleDetails />}
/>
<Route
path={OrganizationTemplateTabs.OrganizationPermissions}
element={<OrganizationPermissions />}
/>
</Route>
</>
)}
<Route path="organizations">
<Route index element={<Organizations />} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type Props = {
organizationRoleId: string;
};

function Permissions({ organizationRoleId }: Props) {
return <div>TBD</div>;
}

export default Permissions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { type OrganizationRole } from '@logto/schemas';
import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';

import DetailsForm from '@/components/DetailsForm';
import FormCard from '@/components/FormCard';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import { organizationRoleLink } from '@/consts';
import FormField from '@/ds-components/FormField';
import TextInput from '@/ds-components/TextInput';
import useApi from '@/hooks/use-api';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import { trySubmitSafe } from '@/utils/form';

type Props = {
data: OrganizationRole;
onUpdate: (updatedData: OrganizationRole) => void;
};

function Settings({ data, onUpdate }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();
const {
register,
handleSubmit,
reset,
formState: { errors, isDirty, isSubmitting },
} = useForm<OrganizationRole>({ defaultValues: data });

const api = useApi();

const onSubmit = handleSubmit(
trySubmitSafe(async (formData) => {
const updatedData = await api
.patch(`api/organization-roles/${data.id}`, { json: formData })
.json<OrganizationRole>();
reset(updatedData);
onUpdate(updatedData);
toast.success(t('general.saved'));
})
);

return (
<DetailsForm
isDirty={isDirty}
isSubmitting={isSubmitting}
onSubmit={onSubmit}
onDiscard={reset}
>
<FormCard
title="organization_role_details.general.settings"
description="organization_role_details.general.description"
learnMoreLink={{
href: getDocumentationUrl(organizationRoleLink),
targetBlank: 'noopener',
}}
>
<FormField isRequired title="organization_role_details.general.name_field">
<TextInput
placeholder="viewer"
error={Boolean(errors.name)}
{...register('name', { required: true })}
/>
</FormField>
<FormField title="organization_role_details.general.description_field">
<TextInput
placeholder={t('organization_role_details.general.description_field_placeholder')}
{...register('description')}
/>
</FormField>
</FormCard>
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
</DetailsForm>
);
}

export default Settings;
126 changes: 126 additions & 0 deletions packages/console/src/pages/OrganizationRoleDetails/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { withAppInsights } from '@logto/app-insights/react/AppInsightsReact';
import { type OrganizationRole } from '@logto/schemas';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { Navigate, Route, Routes, useParams } from 'react-router-dom';
import useSWR, { useSWRConfig } from 'swr';

import Delete from '@/assets/icons/delete.svg';
import OrgRoleIcon from '@/assets/icons/role-feature.svg';
import DetailsPage from '@/components/DetailsPage';
import DetailsPageHeader from '@/components/DetailsPage/DetailsPageHeader';
import PageMeta from '@/components/PageMeta';
import ThemedIcon from '@/components/ThemedIcon';
import { OrganizationRoleDetailsTabs, OrganizationTemplateTabs } from '@/consts';
import ConfirmModal from '@/ds-components/ConfirmModal';
import DynamicT from '@/ds-components/DynamicT';
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import useApi, { type RequestError } from '@/hooks/use-api';
import useTenantPathname from '@/hooks/use-tenant-pathname';

import Permissions from './Permissions';
import Settings from './Settings';

const orgRolesPath = `/organization-template/${OrganizationTemplateTabs.OrganizationRoles}`;

function OrganizationRoleDetails() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });

const { id } = useParams();
const { navigate } = useTenantPathname();

const { data, error, mutate, isLoading } = useSWR<OrganizationRole, RequestError>(
id && `api/organization-roles/${id}`
);
const api = useApi();
const { mutate: mutateGlobal } = useSWRConfig();
const [isDeletionAlertOpen, setIsDeletionAlertOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);

const handleDelete = async () => {
if (!data) {
return;
}

setIsDeleting(true);

try {
await api.delete(`api/organization-roles/${data.id}`);
toast.success(t('organization_role_details.deleted', { name: data.name }));
await mutateGlobal('api/roles');
navigate(orgRolesPath, { replace: true });
} finally {
setIsDeleting(false);
}
};

return (
<DetailsPage
backLink={orgRolesPath}
backLinkTitle="organization_role_details.back_to_org_roles"
isLoading={isLoading}
error={error}
onRetry={mutate}
>
<PageMeta titleKey="organization_role_details.page_title" />
{data && (
<>
<DetailsPageHeader
icon={<ThemedIcon for={OrgRoleIcon} size={60} />}
title={data.name}
primaryTag={t('organization_role_details.org_role')}
identifier={{ name: 'ID', value: data.id }}
actionMenuItems={[
{
title: 'general.delete',
icon: <Delete />,
type: 'danger',
onClick: () => {
setIsDeletionAlertOpen(true);
},
},
]}
/>
<ConfirmModal
isOpen={isDeletionAlertOpen}
isLoading={isDeleting}
confirmButtonText="general.delete"
onCancel={() => {
setIsDeletionAlertOpen(false);
}}
onConfirm={handleDelete}
>
<DynamicT forKey="organization_role_details.delete_confirm" />
</ConfirmModal>
<TabNav>
<TabNavItem
href={`${orgRolesPath}/${data.id}/${OrganizationRoleDetailsTabs.Permissions}`}
>
<DynamicT forKey="organization_role_details.permissions.tab" />
</TabNavItem>
<TabNavItem href={`${orgRolesPath}/${data.id}/${OrganizationRoleDetailsTabs.General}`}>
<DynamicT forKey="organization_role_details.general.tab" />
</TabNavItem>
</TabNav>
<Routes>
<Route
index
element={<Navigate replace to={OrganizationRoleDetailsTabs.Permissions} />}
/>
<Route
path={OrganizationRoleDetailsTabs.Permissions}
element={<Permissions organizationRoleId={data.id} />}
/>
<Route
path={OrganizationRoleDetailsTabs.General}
element={<Settings data={data} onUpdate={mutate} />}
/>
</Routes>
</>
)}
</DetailsPage>
);
}

export default withAppInsights(OrganizationRoleDetails);
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ import Tag from '@/ds-components/Tag';
import { type RequestError } from '@/hooks/use-api';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import { buildUrl } from '@/utils/url';

import * as styles from './index.module.scss';

function OrganizationRoles() {
const { getDocumentationUrl } = useDocumentationUrl();

const { navigate } = useTenantPathname();
const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
page: 1,
});
Expand All @@ -49,8 +50,8 @@ function OrganizationRoles() {
title: <DynamicT forKey="organization_template.roles.role_column" />,
dataIndex: 'name',
colSpan: 4,
render: ({ name }) => {
return <ItemPreview title={name} icon={<ThemedIcon for={OrgRoleIcon} />} />;
render: ({ id, name }) => {
return <ItemPreview title={name} icon={<ThemedIcon for={OrgRoleIcon} />} to={id} />;
},
},
{
Expand All @@ -72,6 +73,9 @@ function OrganizationRoles() {
},
},
]}
rowClickHandler={({ id }) => {
navigate(id);
}}
filter={
<div className={styles.filter}>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import organization_details from './organization-details.js';
import organization_role_details from './organization-role-details.js';
import organization_template from './organization-template.js';
import organizations from './organizations.js';
import permissions from './permissions.js';
Expand Down Expand Up @@ -95,6 +96,7 @@ const admin_console = {
invitation,
signing_keys,
organization_template,
organization_role_details,
};

export default Object.freeze(admin_console);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const organization_role_details = {
page_title: 'Organisationsrollendetails',
back_to_org_roles: 'Zurück zu den Org-Rollen',
org_role: 'Org-Rolle',
delete_confirm:
'Dadurch werden die mit dieser Rolle verbundenen Berechtigungen von den betroffenen Benutzern entfernt und die Beziehungen zwischen Organisationsrollen, Mitgliedern in der Organisation und Organisationsberechtigungen gelöscht.',
deleted: 'Organisationsrolle {{name}} wurde erfolgreich gelöscht.',
permissions: {
tab: 'Berechtigungen',
name_column: 'Berechtigung',
description_column: 'Beschreibung',
type_column: 'Berechtigungstyp',
type: {
api: 'API-Berechtigung',
org: 'Org-Berechtigung',
},
assign_permissions: 'Berechtigungen zuweisen',
},
general: {
tab: 'Allgemein',
settings: 'Einstellungen',
description:
'Die Organisationsrolle ist eine Gruppierung von Berechtigungen, die Benutzern zugewiesen werden können. Die Berechtigungen müssen aus den vordefinierten Organisationsberechtigungen stammen.',
name_field: 'Name',
description_field: 'Beschreibung',
description_field_placeholder: 'Benutzer mit nur Leseberechtigungen',
},
};

export default Object.freeze(organization_role_details);
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import organization_details from './organization-details.js';
import organization_role_details from './organization-role-details.js';
import organization_template from './organization-template.js';
import organizations from './organizations.js';
import permissions from './permissions.js';
Expand Down Expand Up @@ -95,6 +96,7 @@ const admin_console = {
invitation,
signing_keys,
organization_template,
organization_role_details,
};

export default Object.freeze(admin_console);
Loading

0 comments on commit 0f5347b

Please sign in to comment.