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:
parent
3cea0735c4
commit
70d8b1de2f
10 changed files with 102 additions and 76 deletions
|
@ -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 /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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 /> },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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' });
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
8
packages/console/src/pages/ApiResourceDetails/types.ts
Normal file
8
packages/console/src/pages/ApiResourceDetails/types.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import type { Resource } from '@logto/schemas';
|
||||
|
||||
export type ApiResourceDetailsOutletContext = {
|
||||
resource: Resource;
|
||||
isDeleting: boolean;
|
||||
isLogtoManagementApiResource: boolean;
|
||||
onResourceUpdated: (resource: Resource) => void;
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { type OrganizationRole } from '@logto/schemas';
|
||||
|
||||
export type OrganizationRoleDetailsOutletContext = {
|
||||
organizationRole: OrganizationRole;
|
||||
isDeleting: boolean;
|
||||
onOrganizationRoleUpdated: (organizationRole: OrganizationRole) => void;
|
||||
};
|
Loading…
Add table
Reference in a new issue