diff --git a/packages/console/src/consts/page-tabs.ts b/packages/console/src/consts/page-tabs.ts index e6d593026..890f776c4 100644 --- a/packages/console/src/consts/page-tabs.ts +++ b/packages/console/src/consts/page-tabs.ts @@ -52,3 +52,8 @@ export enum OrganizationTemplateTabs { OrganizationRoles = 'organization-roles', OrganizationPermissions = 'organization-permissions', } + +export enum OrganizationRoleDetailsTabs { + Permissions = 'permissions', + General = 'general', +} diff --git a/packages/console/src/containers/ConsoleContent/index.tsx b/packages/console/src/containers/ConsoleContent/index.tsx index 0c05bc80e..39f1f84c8 100644 --- a/packages/console/src/containers/ConsoleContent/index.tsx +++ b/packages/console/src/containers/ConsoleContent/index.tsx @@ -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'; @@ -187,20 +188,26 @@ function ConsoleContent() { {isDevFeaturesEnabled && ( - }> + <> + }> + } + /> + } + /> + } + /> + } + path={`organization-template/${OrganizationTemplateTabs.OrganizationRoles}/:id/*`} + element={} /> - } - /> - } - /> - + )} } /> diff --git a/packages/console/src/pages/OrganizationRoleDetails/Permissions/index.tsx b/packages/console/src/pages/OrganizationRoleDetails/Permissions/index.tsx new file mode 100644 index 000000000..1824f1775 --- /dev/null +++ b/packages/console/src/pages/OrganizationRoleDetails/Permissions/index.tsx @@ -0,0 +1,9 @@ +type Props = { + organizationRoleId: string; +}; + +function Permissions({ organizationRoleId }: Props) { + return
TBD
; +} + +export default Permissions; diff --git a/packages/console/src/pages/OrganizationRoleDetails/Settings/index.tsx b/packages/console/src/pages/OrganizationRoleDetails/Settings/index.tsx new file mode 100644 index 000000000..70615c58f --- /dev/null +++ b/packages/console/src/pages/OrganizationRoleDetails/Settings/index.tsx @@ -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({ defaultValues: data }); + + const api = useApi(); + + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + const updatedData = await api + .patch(`api/organization-roles/${data.id}`, { json: formData }) + .json(); + reset(updatedData); + onUpdate(updatedData); + toast.success(t('general.saved')); + }) + ); + + return ( + + + + + + + + + + + + ); +} + +export default Settings; diff --git a/packages/console/src/pages/OrganizationRoleDetails/index.tsx b/packages/console/src/pages/OrganizationRoleDetails/index.tsx new file mode 100644 index 000000000..9abcfcfdd --- /dev/null +++ b/packages/console/src/pages/OrganizationRoleDetails/index.tsx @@ -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( + 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 ( + + + {data && ( + <> + } + title={data.name} + primaryTag={t('organization_role_details.org_role')} + identifier={{ name: 'ID', value: data.id }} + actionMenuItems={[ + { + title: 'general.delete', + icon: , + type: 'danger', + onClick: () => { + setIsDeletionAlertOpen(true); + }, + }, + ]} + /> + { + setIsDeletionAlertOpen(false); + }} + onConfirm={handleDelete} + > + + + + + + + + + + + + } + /> + } + /> + } + /> + + + )} + + ); +} + +export default withAppInsights(OrganizationRoleDetails); diff --git a/packages/console/src/pages/OrganizationTemplate/OrganizationRoles/index.tsx b/packages/console/src/pages/OrganizationTemplate/OrganizationRoles/index.tsx index 46540c604..1cb6360c9 100644 --- a/packages/console/src/pages/OrganizationTemplate/OrganizationRoles/index.tsx +++ b/packages/console/src/pages/OrganizationTemplate/OrganizationRoles/index.tsx @@ -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, }); @@ -49,8 +50,8 @@ function OrganizationRoles() { title: , dataIndex: 'name', colSpan: 4, - render: ({ name }) => { - return } />; + render: ({ id, name }) => { + return } to={id} />; }, }, { @@ -72,6 +73,9 @@ function OrganizationRoles() { }, }, ]} + rowClickHandler={({ id }) => { + navigate(id); + }} filter={