mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor(console): update tenant settings access per user tenant scopes (#5571)
This commit is contained in:
parent
664c67dc6d
commit
a2899a8101
10 changed files with 288 additions and 161 deletions
|
@ -13,8 +13,10 @@ import useActionTranslation from '@/hooks/use-action-translation';
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
/** A function that will be called when the user confirms the deletion. */
|
||||
onDelete: () => void | Promise<void>;
|
||||
/** A function that will be called when the user confirms the deletion. If not provided,
|
||||
* the delete button will not be displayed.
|
||||
*/
|
||||
onDelete?: () => void | Promise<void>;
|
||||
/**
|
||||
* A function that will be called when the user clicks the edit button. If not provided,
|
||||
* the edit button will not be displayed.
|
||||
|
@ -48,6 +50,10 @@ function ActionsButton({ onDelete, onEdit, deleteConfirmation, fieldName, textOv
|
|||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!onDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await onDelete();
|
||||
|
@ -69,31 +75,35 @@ function ActionsButton({ onDelete, onEdit, deleteConfirmation, fieldName, textOv
|
|||
)}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
<ActionMenuItem
|
||||
icon={<Delete />}
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{textOverrides?.delete ? (
|
||||
<DynamicT forKey={textOverrides.delete} />
|
||||
) : (
|
||||
tAction('delete', fieldName)
|
||||
)}
|
||||
</ActionMenuItem>
|
||||
{onDelete && (
|
||||
<ActionMenuItem
|
||||
icon={<Delete />}
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{textOverrides?.delete ? (
|
||||
<DynamicT forKey={textOverrides.delete} />
|
||||
) : (
|
||||
tAction('delete', fieldName)
|
||||
)}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
</ActionMenu>
|
||||
<ConfirmModal
|
||||
isOpen={isModalOpen}
|
||||
confirmButtonText={textOverrides?.deleteConfirmation ?? 'general.delete'}
|
||||
isLoading={isDeleting}
|
||||
onCancel={() => {
|
||||
setIsModalOpen(false);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
<DynamicT forKey={deleteConfirmation} />
|
||||
</ConfirmModal>
|
||||
{onDelete && (
|
||||
<ConfirmModal
|
||||
isOpen={isModalOpen}
|
||||
confirmButtonText={textOverrides?.deleteConfirmation ?? 'general.delete'}
|
||||
isLoading={isDeleting}
|
||||
onCancel={() => {
|
||||
setIsModalOpen(false);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
<DynamicT forKey={deleteConfirmation} />
|
||||
</ConfirmModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
import ApiResourceDetails from '@/pages/ApiResourceDetails';
|
||||
import ApiResourcePermissions from '@/pages/ApiResourceDetails/ApiResourcePermissions';
|
||||
import ApiResourceSettings from '@/pages/ApiResourceDetails/ApiResourceSettings';
|
||||
|
@ -74,6 +75,7 @@ import * as styles from './index.module.scss';
|
|||
function ConsoleContent() {
|
||||
const { scrollableContent } = useOutletContext<AppContentOutletContext>();
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const { canManageTenant } = useCurrentTenantScopes();
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
|
@ -197,13 +199,25 @@ function ConsoleContent() {
|
|||
<Route path="signing-keys" element={<SigningKeys />} />
|
||||
{isCloud && (
|
||||
<Route path="tenant-settings" element={<TenantSettings />}>
|
||||
<Route index element={<Navigate replace to={TenantSettingsTabs.Settings} />} />
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<Navigate
|
||||
replace
|
||||
to={
|
||||
canManageTenant ? TenantSettingsTabs.Settings : TenantSettingsTabs.Members
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path={TenantSettingsTabs.Settings} element={<TenantBasicSettings />} />
|
||||
{isDevFeaturesEnabled && (
|
||||
<Route path={`${TenantSettingsTabs.Members}/*`} element={<TenantMembers />} />
|
||||
)}
|
||||
<Route path={TenantSettingsTabs.Domains} element={<TenantDomainSettings />} />
|
||||
{!isDevTenant && (
|
||||
{canManageTenant && (
|
||||
<Route path={TenantSettingsTabs.Domains} element={<TenantDomainSettings />} />
|
||||
)}
|
||||
{!isDevTenant && canManageTenant && (
|
||||
<>
|
||||
<Route path={TenantSettingsTabs.Subscription} element={<Subscription />} />
|
||||
<Route path={TenantSettingsTabs.BillingHistory} element={<BillingHistory />} />
|
||||
|
|
61
packages/console/src/hooks/use-current-tenant-scopes.ts
Normal file
61
packages/console/src/hooks/use-current-tenant-scopes.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import { TenantScope, getTenantOrganizationId } from '@logto/schemas';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
|
||||
const useCurrentTenantScopes = () => {
|
||||
const { currentTenantId, isInitComplete } = useContext(TenantsContext);
|
||||
const { isAuthenticated, getOrganizationTokenClaims } = useLogto();
|
||||
|
||||
const [scopes, setScopes] = useState<string[]>([]);
|
||||
const [canInviteMember, setCanInviteMember] = useState(false);
|
||||
const [canRemoveMember, setCanRemoveMember] = useState(false);
|
||||
const [canUpdateMemberRole, setCanUpdateMemberRole] = useState(false);
|
||||
const [canManageTenant, setCanManageTenant] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (isAuthenticated && isInitComplete) {
|
||||
const organizationId = getTenantOrganizationId(currentTenantId);
|
||||
const claims = await getOrganizationTokenClaims(organizationId);
|
||||
const allScopes = claims?.scope?.split(' ') ?? [];
|
||||
setScopes(allScopes);
|
||||
|
||||
for (const scope of allScopes) {
|
||||
switch (scope) {
|
||||
case TenantScope.InviteMember: {
|
||||
setCanInviteMember(true);
|
||||
break;
|
||||
}
|
||||
case TenantScope.RemoveMember: {
|
||||
setCanRemoveMember(true);
|
||||
break;
|
||||
}
|
||||
case TenantScope.UpdateMemberRole: {
|
||||
setCanUpdateMemberRole(true);
|
||||
break;
|
||||
}
|
||||
case TenantScope.ManageTenant: {
|
||||
setCanManageTenant(true);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [currentTenantId, getOrganizationTokenClaims, isAuthenticated, isInitComplete]);
|
||||
|
||||
return {
|
||||
canInviteMember,
|
||||
canRemoveMember,
|
||||
canUpdateMemberRole,
|
||||
canManageTenant,
|
||||
scopes,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCurrentTenantScopes;
|
|
@ -4,6 +4,7 @@ import FormCard from '@/components/FormCard';
|
|||
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
|
||||
import { type TenantSettingsForm } from '../types.js';
|
||||
|
||||
|
@ -15,6 +16,7 @@ type Props = {
|
|||
};
|
||||
|
||||
function ProfileForm({ currentTenantId }: Props) {
|
||||
const { canManageTenant } = useCurrentTenantScopes();
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
|
@ -29,6 +31,7 @@ function ProfileForm({ currentTenantId }: Props) {
|
|||
<FormField isRequired title="tenants.settings.tenant_name">
|
||||
<TextInput
|
||||
{...register('profile.name', { required: true })}
|
||||
readOnly={!canManageTenant}
|
||||
error={Boolean(errors.profile?.name)}
|
||||
/>
|
||||
</FormField>
|
||||
|
|
|
@ -12,6 +12,7 @@ import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar'
|
|||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import DeleteCard from './DeleteCard';
|
||||
|
@ -28,6 +29,7 @@ const tenantProfileToForm = (tenant?: TenantResponse): TenantSettingsForm => {
|
|||
|
||||
function TenantBasicSettings() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { canManageTenant } = useCurrentTenantScopes();
|
||||
const api = useCloudApi();
|
||||
const {
|
||||
currentTenant,
|
||||
|
@ -120,26 +122,34 @@ function TenantBasicSettings() {
|
|||
<FormProvider {...methods}>
|
||||
<div className={styles.fields}>
|
||||
<ProfileForm currentTenantId={currentTenantId} />
|
||||
<DeleteCard currentTenantId={currentTenantId} onClick={onClickDeletionButton} />
|
||||
{canManageTenant && (
|
||||
<DeleteCard currentTenantId={currentTenantId} onClick={onClickDeletionButton} />
|
||||
)}
|
||||
</div>
|
||||
</FormProvider>
|
||||
<SubmitFormChangesActionBar
|
||||
isOpen={isDirty}
|
||||
isSubmitting={isSubmitting}
|
||||
onDiscard={reset}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
{canManageTenant && (
|
||||
<SubmitFormChangesActionBar
|
||||
isOpen={isDirty}
|
||||
isSubmitting={isSubmitting}
|
||||
onDiscard={reset}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||
<DeleteModal
|
||||
isOpen={isDeletionModalOpen}
|
||||
isLoading={isDeleting}
|
||||
tenant={watch('profile')}
|
||||
onClose={() => {
|
||||
setIsDeletionModalOpen(false);
|
||||
}}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
{canManageTenant && (
|
||||
<>
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||
<DeleteModal
|
||||
isOpen={isDeletionModalOpen}
|
||||
isLoading={isDeleting}
|
||||
tenant={watch('profile')}
|
||||
onClose={() => {
|
||||
setIsDeletionModalOpen(false);
|
||||
}}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { OrganizationInvitationStatus } from '@logto/schemas';
|
||||
import { condArray, conditional } from '@silverhand/essentials';
|
||||
import { format } from 'date-fns';
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
@ -13,7 +14,7 @@ import Redo from '@/assets/icons/redo.svg';
|
|||
import UsersEmptyDark from '@/assets/images/users-empty-dark.svg';
|
||||
import UsersEmpty from '@/assets/images/users-empty.svg';
|
||||
import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type TenantInvitationResponse } from '@/cloud/types/router';
|
||||
import type { InvitationResponse, TenantInvitationResponse } from '@/cloud/types/router';
|
||||
import { RoleOption } from '@/components/OrganizationRolesSelect';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import ActionMenu, { ActionMenuItem } from '@/ds-components/ActionMenu';
|
||||
|
@ -24,6 +25,7 @@ import TablePlaceholder from '@/ds-components/Table/TablePlaceholder';
|
|||
import Tag, { type Props as TagProps } from '@/ds-components/Tag';
|
||||
import { type RequestError } from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
|
||||
import InviteMemberModal from '../InviteMemberModal';
|
||||
|
||||
|
@ -50,6 +52,7 @@ function Invitations() {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenant_members' });
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { canInviteMember, canRemoveMember } = useCurrentTenantScopes();
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR<TenantInvitationResponse[], RequestError>(
|
||||
`api/tenant/${currentTenantId}/invitations`,
|
||||
|
@ -105,17 +108,19 @@ function Invitations() {
|
|||
imageDark={<UsersEmptyDark />}
|
||||
title="tenant_members.invitation_empty_placeholder.title"
|
||||
description="tenant_members.invitation_empty_placeholder.description"
|
||||
action={
|
||||
<Button
|
||||
title="tenant_members.invite_members"
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<Plus />}
|
||||
onClick={() => {
|
||||
setShowInviteModal(true);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
action={conditional(
|
||||
canInviteMember && (
|
||||
<Button
|
||||
title="tenant_members.invite_members"
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<Plus />}
|
||||
onClick={() => {
|
||||
setShowInviteModal(true);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
}
|
||||
isLoading={isLoading}
|
||||
|
@ -166,60 +171,64 @@ function Invitations() {
|
|||
title: t('expiration_date'),
|
||||
render: ({ expiresAt }) => <span>{format(expiresAt, 'MMM do, yyyy')}</span>,
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: null,
|
||||
render: ({ id, status }) => (
|
||||
<ActionMenu
|
||||
icon={<More />}
|
||||
iconSize="small"
|
||||
title={<DynamicT forKey="general.more_options" />}
|
||||
>
|
||||
{status !== OrganizationInvitationStatus.Accepted && (
|
||||
<ActionMenuItem
|
||||
icon={<Invite />}
|
||||
onClick={async () => {
|
||||
await cloudApi.post(
|
||||
'/api/tenants/:tenantId/invitations/:invitationId/message',
|
||||
{
|
||||
params: { tenantId: currentTenantId, invitationId: id },
|
||||
}
|
||||
);
|
||||
toast.success(t('messages.invitation_sent'));
|
||||
}}
|
||||
...condArray(
|
||||
(canInviteMember || canRemoveMember) && [
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: null,
|
||||
render: ({ id, status }: InvitationResponse) => (
|
||||
<ActionMenu
|
||||
icon={<More />}
|
||||
iconSize="small"
|
||||
title={<DynamicT forKey="general.more_options" />}
|
||||
>
|
||||
{t('menu_options.resend_invite')}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
{status === OrganizationInvitationStatus.Pending && (
|
||||
<ActionMenuItem
|
||||
icon={<Redo />}
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
void handleRevoke(id);
|
||||
}}
|
||||
>
|
||||
{t('menu_options.revoke')}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
{status !== OrganizationInvitationStatus.Pending && (
|
||||
<ActionMenuItem
|
||||
icon={<Delete />}
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
void handleDelete(id);
|
||||
}}
|
||||
>
|
||||
{t('menu_options.delete_invitation_record')}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
</ActionMenu>
|
||||
),
|
||||
},
|
||||
{status !== OrganizationInvitationStatus.Accepted && canInviteMember && (
|
||||
<ActionMenuItem
|
||||
icon={<Invite />}
|
||||
onClick={async () => {
|
||||
await cloudApi.post(
|
||||
'/api/tenants/:tenantId/invitations/:invitationId/message',
|
||||
{
|
||||
params: { tenantId: currentTenantId, invitationId: id },
|
||||
}
|
||||
);
|
||||
toast.success(t('messages.invitation_sent'));
|
||||
}}
|
||||
>
|
||||
{t('menu_options.resend_invite')}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
{status === OrganizationInvitationStatus.Pending && canRemoveMember && (
|
||||
<ActionMenuItem
|
||||
icon={<Redo />}
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
void handleRevoke(id);
|
||||
}}
|
||||
>
|
||||
{t('menu_options.revoke')}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
{status !== OrganizationInvitationStatus.Pending && canRemoveMember && (
|
||||
<ActionMenuItem
|
||||
icon={<Delete />}
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
void handleDelete(id);
|
||||
}}
|
||||
>
|
||||
{t('menu_options.delete_invitation_record')}
|
||||
</ActionMenuItem>
|
||||
)}
|
||||
</ActionMenu>
|
||||
),
|
||||
},
|
||||
]
|
||||
),
|
||||
]}
|
||||
rowIndexKey="id"
|
||||
/>
|
||||
{showInviteModal && (
|
||||
{canInviteMember && (
|
||||
<InviteMemberModal
|
||||
isOpen={showInviteModal}
|
||||
onClose={() => {
|
||||
|
|
|
@ -28,12 +28,9 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const { currentTenantId, isDevTenant } = useContext(TenantsContext);
|
||||
const tenantMembersMaxLimit = useMemo(() => {
|
||||
if (isDevTenant) {
|
||||
if (currentPlan.id === ReservedPlanId.Pro || isDevTenant) {
|
||||
return 10;
|
||||
}
|
||||
if (currentPlan.id === ReservedPlanId.Pro) {
|
||||
return 3;
|
||||
}
|
||||
// Free plan can only have 1 admin, no other members allowed.
|
||||
return 1;
|
||||
}, [currentPlan.id, isDevTenant]);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { condArray, conditional } from '@silverhand/essentials';
|
||||
import { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
@ -12,6 +13,7 @@ import { TenantsContext } from '@/contexts/TenantsProvider';
|
|||
import Table from '@/ds-components/Table';
|
||||
import Tag from '@/ds-components/Tag';
|
||||
import { type RequestError } from '@/hooks/use-api';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
|
||||
import EditMemberModal from '../EditMemberModal';
|
||||
|
||||
|
@ -19,6 +21,7 @@ function Members() {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenant_members' });
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { canInviteMember, canRemoveMember, canUpdateMemberRole } = useCurrentTenantScopes();
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR<TenantMemberResponse[], RequestError>(
|
||||
`api/tenant/${currentTenantId}/members`,
|
||||
|
@ -59,35 +62,45 @@ function Members() {
|
|||
));
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: null,
|
||||
colSpan: 1,
|
||||
render: (user) => (
|
||||
<ActionsButton
|
||||
deleteConfirmation="tenant_members.delete_user_confirm"
|
||||
fieldName="tenant_members.user"
|
||||
textOverrides={{
|
||||
edit: 'tenant_members.menu_options.edit',
|
||||
delete: 'tenant_members.menu_options.delete',
|
||||
deleteConfirmation: 'general.remove',
|
||||
}}
|
||||
onEdit={() => {
|
||||
setUserToBeEdited(user);
|
||||
}}
|
||||
onDelete={async () => {
|
||||
await cloudApi.delete(`/api/tenants/:tenantId/members/:userId`, {
|
||||
params: { tenantId: currentTenantId, userId: user.id },
|
||||
});
|
||||
void mutate();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...condArray(
|
||||
(canUpdateMemberRole || canRemoveMember) && [
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: null,
|
||||
colSpan: 1,
|
||||
render: (user: TenantMemberResponse) => (
|
||||
<ActionsButton
|
||||
deleteConfirmation="tenant_members.delete_user_confirm"
|
||||
fieldName="tenant_members.user"
|
||||
textOverrides={{
|
||||
edit: 'tenant_members.menu_options.edit',
|
||||
delete: 'tenant_members.menu_options.delete',
|
||||
deleteConfirmation: 'general.remove',
|
||||
}}
|
||||
onEdit={conditional(
|
||||
canUpdateMemberRole &&
|
||||
(() => {
|
||||
setUserToBeEdited(user);
|
||||
})
|
||||
)}
|
||||
onDelete={conditional(
|
||||
canRemoveMember &&
|
||||
(async () => {
|
||||
await cloudApi.delete(`/api/tenants/:tenantId/members/:userId`, {
|
||||
params: { tenantId: currentTenantId, userId: user.id },
|
||||
});
|
||||
void mutate();
|
||||
})
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
),
|
||||
]}
|
||||
rowIndexKey="id"
|
||||
/>
|
||||
{userToBeEdited && (
|
||||
{canUpdateMemberRole && userToBeEdited && (
|
||||
<EditMemberModal
|
||||
isOpen
|
||||
user={userToBeEdited}
|
||||
|
|
|
@ -8,6 +8,7 @@ import PlusIcon from '@/assets/icons/plus.svg';
|
|||
import { TenantSettingsTabs } from '@/consts';
|
||||
import Button from '@/ds-components/Button';
|
||||
import Spacer from '@/ds-components/Spacer';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
|
@ -21,6 +22,7 @@ const invitationsRoute = 'invitations';
|
|||
function TenantMembers() {
|
||||
const { navigate, match } = useTenantPathname();
|
||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||
const { canInviteMember } = useCurrentTenantScopes();
|
||||
|
||||
const isInvitationTab = match(
|
||||
`/tenant-settings/${TenantSettingsTabs.Members}/${invitationsRoute}`
|
||||
|
@ -37,31 +39,35 @@ function TenantMembers() {
|
|||
navigate('.');
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className={classNames(styles.button, isInvitationTab && styles.active)}
|
||||
icon={<InvitationIcon />}
|
||||
title="tenant_members.invitations"
|
||||
onClick={() => {
|
||||
navigate('invitations');
|
||||
}}
|
||||
/>
|
||||
{canInviteMember && (
|
||||
<Button
|
||||
className={classNames(styles.button, isInvitationTab && styles.active)}
|
||||
icon={<InvitationIcon />}
|
||||
title="tenant_members.invitations"
|
||||
onClick={() => {
|
||||
navigate('invitations');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Spacer />
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<PlusIcon />}
|
||||
title="tenant_members.invite_members"
|
||||
onClick={() => {
|
||||
setShowInviteModal(true);
|
||||
}}
|
||||
/>
|
||||
{canInviteMember && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<PlusIcon />}
|
||||
title="tenant_members.invite_members"
|
||||
onClick={() => {
|
||||
setShowInviteModal(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
<Route index element={<Members />} />
|
||||
<Route path={invitationsRoute} element={<Invitations />} />
|
||||
{canInviteMember && <Route path={invitationsRoute} element={<Invitations />} />}
|
||||
</Routes>
|
||||
{showInviteModal && (
|
||||
{canInviteMember && (
|
||||
<InviteMemberModal
|
||||
isOpen={showInviteModal}
|
||||
onClose={(isSuccessful) => {
|
||||
|
|
|
@ -7,11 +7,13 @@ import { TenantsContext } from '@/contexts/TenantsProvider';
|
|||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
function TenantSettings() {
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const { canManageTenant } = useCurrentTenantScopes();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
|
@ -29,10 +31,12 @@ function TenantSettings() {
|
|||
<DynamicT forKey="tenants.tabs.members" />
|
||||
</TabNavItem>
|
||||
)}
|
||||
<TabNavItem href={`/tenant-settings/${TenantSettingsTabs.Domains}`}>
|
||||
<DynamicT forKey="tenants.tabs.domains" />
|
||||
</TabNavItem>
|
||||
{!isDevTenant && (
|
||||
{canManageTenant && (
|
||||
<TabNavItem href={`/tenant-settings/${TenantSettingsTabs.Domains}`}>
|
||||
<DynamicT forKey="tenants.tabs.domains" />
|
||||
</TabNavItem>
|
||||
)}
|
||||
{!isDevTenant && canManageTenant && (
|
||||
<>
|
||||
<TabNavItem href={`/tenant-settings/${TenantSettingsTabs.Subscription}`}>
|
||||
<DynamicT forKey="tenants.tabs.subscription" />
|
||||
|
|
Loading…
Add table
Reference in a new issue