0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(console): update console routes (#5715)

This commit is contained in:
Xiao Yijun 2024-04-15 19:31:11 +08:00 committed by GitHub
parent 3cea0735c4
commit 70d8b1de2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 102 additions and 76 deletions

View file

@ -1,6 +1,9 @@
import { type RouteObject } from 'react-router-dom';
import { Navigate, type RouteObject } from 'react-router-dom';
import { ApiResourceDetailsTabs } from '@/consts';
import ApiResourceDetails from '@/pages/ApiResourceDetails';
import ApiResourcePermissions from '@/pages/ApiResourceDetails/ApiResourcePermissions';
import ApiResourceSettings from '@/pages/ApiResourceDetails/ApiResourceSettings';
import ApiResources from '@/pages/ApiResources';
export const apiResources: RouteObject = {
@ -9,6 +12,14 @@ export const apiResources: RouteObject = {
{ index: true, element: <ApiResources /> },
{ path: 'create', element: <ApiResources /> },
{ path: ':id/guide/:guideId', element: <ApiResourceDetails /> },
{ path: ':id/*', element: <ApiResourceDetails /> },
{
path: ':id/*',
element: <ApiResourceDetails />,
children: [
{ index: true, element: <Navigate replace to={ApiResourceDetailsTabs.Permissions} /> },
{ path: ApiResourceDetailsTabs.Permissions, element: <ApiResourcePermissions /> },
{ path: ApiResourceDetailsTabs.General, element: <ApiResourceSettings /> },
],
},
],
};

View file

@ -1,7 +1,9 @@
import { Navigate, type RouteObject } from 'react-router-dom';
import { OrganizationTemplateTabs } from '@/consts';
import { OrganizationRoleDetailsTabs, OrganizationTemplateTabs } from '@/consts';
import OrganizationRoleDetails from '@/pages/OrganizationRoleDetails';
import Permissions from '@/pages/OrganizationRoleDetails/Permissions';
import Settings from '@/pages/OrganizationRoleDetails/Settings';
import OrganizationTemplate from '@/pages/OrganizationTemplate';
import OrganizationPermissions from '@/pages/OrganizationTemplate/OrganizationPermissions';
import OrganizationRoles from '@/pages/OrganizationTemplate/OrganizationRoles';
@ -25,5 +27,13 @@ export const organizationTemplate: RouteObject[] = [
{
path: `organization-template/${OrganizationTemplateTabs.OrganizationRoles}/:id/*`,
element: <OrganizationRoleDetails />,
children: [
{
index: true,
element: <Navigate replace to={OrganizationRoleDetailsTabs.Permissions} />,
},
{ path: OrganizationRoleDetailsTabs.Permissions, element: <Permissions /> },
{ path: OrganizationRoleDetailsTabs.General, element: <Settings /> },
],
},
];

View file

@ -1,8 +1,9 @@
import { isManagementApi, type Resource, type ScopeResponse } from '@logto/schemas';
import { type ScopeResponse } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
import useSWR from 'swr';
import PermissionsTable from '@/components/PermissionsTable';
@ -12,17 +13,17 @@ import useApi from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import { type ApiResourceDetailsOutletContext } from '../types';
import CreatePermissionModal from './components/CreatePermissionModal';
const pageSize = defaultPageSize;
type Props = {
resource: Resource;
};
function ApiResourcePermissions({ resource }: Props) {
const { id: resourceId, indicator } = resource;
const isLogtoManagementApiResource = isManagementApi(indicator);
function ApiResourcePermissions() {
const {
resource: { id: resourceId },
isLogtoManagementApiResource,
} = useOutletContext<ApiResourceDetailsOutletContext>();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });

View file

@ -1,7 +1,8 @@
import { isManagementApi, type Resource } from '@logto/schemas';
import { type Resource } from '@logto/schemas';
import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
import DetailsForm from '@/components/DetailsForm';
import FormCard from '@/components/FormCard';
@ -14,14 +15,11 @@ import useApi from '@/hooks/use-api';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import { trySubmitSafe } from '@/utils/form';
type Props = {
resource: Resource;
isDeleting: boolean;
onResourceUpdated: (updatedData: Resource) => void;
};
import { type ApiResourceDetailsOutletContext } from '../types';
function ApiResourceSettings({ resource, isDeleting, onResourceUpdated }: Props) {
const isLogtoManagementApiResource = isManagementApi(resource.indicator);
function ApiResourceSettings() {
const { resource, isDeleting, isLogtoManagementApiResource, onResourceUpdated } =
useOutletContext<ApiResourceDetailsOutletContext>();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();

View file

@ -6,9 +6,7 @@ import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { toast } from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next';
// FIXME: @yijun
// eslint-disable-next-line no-restricted-imports
import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom';
import { Outlet, useLocation, useParams } from 'react-router-dom';
import useSWR from 'swr';
import ApiResourceDark from '@/assets/icons/api-resource-dark.svg';
@ -30,12 +28,11 @@ import useDocumentationUrl from '@/hooks/use-documentation-url';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import useTheme from '@/hooks/use-theme';
import ApiResourcePermissions from './ApiResourcePermissions';
import ApiResourceSettings from './ApiResourceSettings';
import GuideDrawer from './components/GuideDrawer';
import GuideModal from './components/GuideModal';
import ManagementApiNotice from './components/ManagementApiNotice';
import * as styles from './index.module.scss';
import { type ApiResourceDetailsOutletContext } from './types';
const icons = {
[Theme.Light]: { ApiIcon: ApiResource, ManagementApiIcon: ManagementApiResource },
@ -180,25 +177,16 @@ function ApiResourceDetails() {
{t('api_resource_details.general_tab')}
</TabNavItem>
</TabNav>
<Routes>
<Route index element={<Navigate replace to={ApiResourceDetailsTabs.Permissions} />} />
<Route
path={ApiResourceDetailsTabs.Permissions}
element={<ApiResourcePermissions resource={data} />}
/>
<Route
path={ApiResourceDetailsTabs.General}
element={
<ApiResourceSettings
resource={data}
isDeleting={isDeleting}
onResourceUpdated={(updatedData: Resource) => {
void mutate(updatedData);
}}
/>
}
/>
</Routes>
<Outlet
context={
{
resource: data,
isDeleting,
isLogtoManagementApiResource,
onResourceUpdated: mutate,
} satisfies ApiResourceDetailsOutletContext
}
/>
</>
)}
</DetailsPage>

View file

@ -0,0 +1,8 @@
import type { Resource } from '@logto/schemas';
export type ApiResourceDetailsOutletContext = {
resource: Resource;
isDeleting: boolean;
isLogtoManagementApiResource: boolean;
onResourceUpdated: (resource: Resource) => void;
};

View file

@ -2,6 +2,7 @@ import { type Scope, type OrganizationScope } from '@logto/schemas';
import { useCallback, useMemo, useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
import Plus from '@/assets/icons/plus.svg';
import ActionsButton from '@/components/ActionsButton';
@ -17,6 +18,8 @@ import Tag from '@/ds-components/Tag';
import useApi from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { type OrganizationRoleDetailsOutletContext } from '../types';
import ResourceName from './ResourceName';
import * as styles from './index.module.scss';
import useOrganizationRoleScopes from './use-organization-role-scopes';
@ -25,11 +28,11 @@ type OrganizationRoleScope = OrganizationScope | Scope;
const isResourceScope = (scope: OrganizationRoleScope): scope is Scope => 'resourceId' in scope;
type Props = {
organizationRoleId: string;
};
function Permissions() {
const {
organizationRole: { id: organizationRoleId },
} = useOutletContext<OrganizationRoleDetailsOutletContext>();
function Permissions({ organizationRoleId }: Props) {
const organizationRolePath = `api/organization-roles/${organizationRoleId}`;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const api = useApi();

View file

@ -2,6 +2,7 @@ import { type OrganizationRole } from '@logto/schemas';
import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
import DetailsForm from '@/components/DetailsForm';
import FormCard from '@/components/FormCard';
@ -13,12 +14,12 @@ 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;
};
import { type OrganizationRoleDetailsOutletContext } from '../types';
function Settings() {
const { organizationRole, isDeleting, onOrganizationRoleUpdated } =
useOutletContext<OrganizationRoleDetailsOutletContext>();
function Settings({ data, onUpdate }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();
const {
@ -26,17 +27,17 @@ function Settings({ data, onUpdate }: Props) {
handleSubmit,
reset,
formState: { errors, isDirty, isSubmitting },
} = useForm<OrganizationRole>({ defaultValues: data });
} = useForm<OrganizationRole>({ defaultValues: organizationRole });
const api = useApi();
const onSubmit = handleSubmit(
trySubmitSafe(async (formData) => {
const updatedData = await api
.patch(`api/organization-roles/${data.id}`, { json: formData })
.patch(`api/organization-roles/${organizationRole.id}`, { json: formData })
.json<OrganizationRole>();
reset(updatedData);
onUpdate(updatedData);
onOrganizationRoleUpdated(updatedData);
toast.success(t('general.saved'));
})
);
@ -70,7 +71,9 @@ function Settings({ data, onUpdate }: Props) {
/>
</FormField>
</FormCard>
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
<UnsavedChangesAlertModal
hasUnsavedChanges={!isDeleting && isDirty} // Should not block navigation back to list page when deleting
/>
</DetailsForm>
);
}

View file

@ -1,12 +1,10 @@
import { withAppInsights } from '@logto/app-insights/react/AppInsightsReact';
import { type OrganizationRole } from '@logto/schemas';
import classNames from 'classnames';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
// FIXME: @yijun
// eslint-disable-next-line no-restricted-imports
import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom';
import { Outlet, useLocation, useParams } from 'react-router-dom';
import useSWR, { useSWRConfig } from 'swr';
import Delete from '@/assets/icons/delete.svg';
@ -22,9 +20,8 @@ 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';
import * as styles from './index.module.scss';
import { type OrganizationRoleDetailsOutletContext } from './types';
const orgRolesPath = `/organization-template/${OrganizationTemplateTabs.OrganizationRoles}`;
@ -44,6 +41,11 @@ function OrganizationRoleDetails() {
const [isDeletionAlertOpen, setIsDeletionAlertOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// Close deletion alert when navigating to another page
useEffect(() => {
setIsDeletionAlertOpen(false);
}, [pathname]);
const handleDelete = async () => {
if (!data) {
return;
@ -110,20 +112,15 @@ function OrganizationRoleDetails() {
<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>
<Outlet
context={
{
organizationRole: data,
isDeleting,
onOrganizationRoleUpdated: mutate,
} satisfies OrganizationRoleDetailsOutletContext
}
/>
</>
)}
</DetailsPage>

View file

@ -0,0 +1,7 @@
import { type OrganizationRole } from '@logto/schemas';
export type OrganizationRoleDetailsOutletContext = {
organizationRole: OrganizationRole;
isDeleting: boolean;
onOrganizationRoleUpdated: (organizationRole: OrganizationRole) => void;
};