Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
202 lines
7.2 KiB
TypeScript
202 lines
7.2 KiB
TypeScript
import type { LicenseInfo } from '@rocket.chat/core-typings';
|
|
import { Callout, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage';
|
|
import { useDebouncedValue, useStableCallback } from '@rocket.chat/fuselage-hooks';
|
|
import type { OptionProp } from '@rocket.chat/ui-client';
|
|
import {
|
|
ExternalLink,
|
|
ContextualbarHeader,
|
|
ContextualbarIcon,
|
|
ContextualbarTitle,
|
|
ContextualbarClose,
|
|
ContextualbarDialog,
|
|
usePagination,
|
|
useSort,
|
|
Page,
|
|
PageHeader,
|
|
PageContent,
|
|
} from '@rocket.chat/ui-client';
|
|
import { useRouteParameter, useTranslation, useRouter, useEndpoint } from '@rocket.chat/ui-contexts';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { Trans } from 'react-i18next';
|
|
|
|
import AdminInviteUsers from './AdminInviteUsers';
|
|
import AdminUserCreated from './AdminUserCreated';
|
|
import AdminUserForm from './AdminUserForm';
|
|
import AdminUserFormWithData from './AdminUserFormWithData';
|
|
import AdminUserInfoWithData from './AdminUserInfoWithData';
|
|
import AdminUserUpgrade from './AdminUserUpgrade';
|
|
import UsersPageHeaderContent from './UsersPageHeaderContent';
|
|
import UsersTable from './UsersTable';
|
|
import useFilteredUsers from './hooks/useFilteredUsers';
|
|
import usePendingUsersCount from './hooks/usePendingUsersCount';
|
|
import { useSeatsCap } from './useSeatsCap';
|
|
import { useLicenseLimitsByBehavior } from '../../../hooks/useLicenseLimitsByBehavior';
|
|
import { useShouldPreventAction } from '../../../hooks/useShouldPreventAction';
|
|
import { useCheckoutUrl } from '../subscription/hooks/useCheckoutUrl';
|
|
|
|
export type UsersFilters = {
|
|
text: string;
|
|
roles: OptionProp[];
|
|
};
|
|
|
|
export type AdminUsersTab = 'all' | 'active' | 'deactivated' | 'pending';
|
|
|
|
export type UsersTableSortingOption = 'name' | 'username' | 'emails.address' | 'status' | 'active' | 'freeSwitchExtension';
|
|
|
|
const AdminUsersPage = () => {
|
|
const t = useTranslation();
|
|
|
|
const seatsCap = useSeatsCap();
|
|
|
|
const isSeatsCapExceeded = useShouldPreventAction('activeUsers');
|
|
const { prevent_action: preventAction } = useLicenseLimitsByBehavior() ?? {};
|
|
const manageSubscriptionUrl = useCheckoutUrl();
|
|
|
|
const router = useRouter();
|
|
const context = useRouteParameter('context');
|
|
const id = useRouteParameter('id');
|
|
|
|
const isCreateUserDisabled = useShouldPreventAction('activeUsers');
|
|
|
|
const getRoles = useEndpoint('GET', '/v1/roles.list');
|
|
const { data, error } = useQuery({
|
|
queryKey: ['roles'],
|
|
queryFn: async () => getRoles(),
|
|
});
|
|
|
|
const paginationData = usePagination();
|
|
const sortData = useSort<UsersTableSortingOption>('name');
|
|
|
|
const [tab, setTab] = useState<AdminUsersTab>('all');
|
|
const [userFilters, setUserFilters] = useState<UsersFilters>({ text: '', roles: [] });
|
|
|
|
const searchTerm = useDebouncedValue(userFilters.text, 500);
|
|
const prevSearchTerm = useRef('');
|
|
|
|
const filteredUsersQueryResult = useFilteredUsers({
|
|
searchTerm,
|
|
prevSearchTerm,
|
|
sortData,
|
|
paginationData,
|
|
tab,
|
|
selectedRoles: useMemo(() => userFilters.roles.map((role) => role.id), [userFilters.roles]),
|
|
});
|
|
|
|
const pendingUsersCount = usePendingUsersCount(filteredUsersQueryResult.data?.users);
|
|
|
|
const handleReload = (): void => {
|
|
seatsCap?.reload();
|
|
filteredUsersQueryResult?.refetch();
|
|
};
|
|
|
|
const handleTabChange = (tab: AdminUsersTab) => {
|
|
setTab(tab);
|
|
|
|
paginationData.setCurrent(0);
|
|
sortData.setSort(tab === 'pending' ? 'active' : 'name', 'asc');
|
|
};
|
|
|
|
const handleCloseContextualbar = useStableCallback(() => router.navigate('/admin/users'));
|
|
|
|
useEffect(() => {
|
|
prevSearchTerm.current = searchTerm;
|
|
}, [searchTerm]);
|
|
|
|
const isRoutePrevented = useMemo(
|
|
() => context && ['new', 'invite'].includes(context) && isCreateUserDisabled,
|
|
[context, isCreateUserDisabled],
|
|
);
|
|
|
|
const toTranslationKey = (key: keyof LicenseInfo['limits']) => t(`subscription.callout.${key}`);
|
|
|
|
return (
|
|
<Page flexDirection='row'>
|
|
<Page>
|
|
<PageHeader title={t('Users')}>
|
|
<UsersPageHeaderContent isSeatsCapExceeded={isSeatsCapExceeded} seatsCap={seatsCap} />
|
|
</PageHeader>
|
|
{preventAction?.includes('activeUsers') && (
|
|
<Callout type='danger' title={t('subscription.callout.servicesDisruptionsOccurring')} marginBlockEnd={19} marginInline={24}>
|
|
<Trans
|
|
i18nKey='subscription.callout.description.limitsExceeded'
|
|
count={preventAction.length}
|
|
values={{ val: preventAction.map(toTranslationKey) }}
|
|
>
|
|
Your workspace exceeded the <>{preventAction.map(toTranslationKey)}</> license limit.
|
|
<ExternalLink
|
|
to={manageSubscriptionUrl({
|
|
target: 'callout',
|
|
action: 'prevent_action',
|
|
limits: preventAction.join(','),
|
|
})}
|
|
>
|
|
Manage your subscription
|
|
</ExternalLink>
|
|
to increase limits.
|
|
</Trans>
|
|
</Callout>
|
|
)}
|
|
<Tabs>
|
|
<TabsItem selected={!tab || tab === 'all'} onClick={() => handleTabChange('all')}>
|
|
{t('All')}
|
|
</TabsItem>
|
|
<TabsItem selected={tab === 'pending'} onClick={() => handleTabChange('pending')} display='flex' flexDirection='row'>
|
|
{`${t('Pending')} `}
|
|
{pendingUsersCount.isLoading && <Skeleton variant='circle' height='x16' width='x16' marginInlineStart={8} />}
|
|
{pendingUsersCount.isSuccess && `(${pendingUsersCount.data})`}
|
|
</TabsItem>
|
|
<TabsItem selected={tab === 'active'} onClick={() => handleTabChange('active')}>
|
|
{t('Active')}
|
|
</TabsItem>
|
|
<TabsItem selected={tab === 'deactivated'} onClick={() => handleTabChange('deactivated')}>
|
|
{t('Deactivated')}
|
|
</TabsItem>
|
|
</Tabs>
|
|
<PageContent>
|
|
<UsersTable
|
|
users={filteredUsersQueryResult.data?.users || []}
|
|
isLoading={filteredUsersQueryResult.isLoading}
|
|
isError={filteredUsersQueryResult.isError}
|
|
isSuccess={filteredUsersQueryResult.isSuccess}
|
|
total={filteredUsersQueryResult.data?.total || 0}
|
|
setUserFilters={setUserFilters}
|
|
paginationData={paginationData}
|
|
sortData={sortData}
|
|
tab={tab}
|
|
isSeatsCapExceeded={isSeatsCapExceeded}
|
|
roleData={data}
|
|
onReload={handleReload}
|
|
/>
|
|
</PageContent>
|
|
</Page>
|
|
{context && (
|
|
<ContextualbarDialog onClose={handleCloseContextualbar}>
|
|
<ContextualbarHeader>
|
|
{['new', 'created', 'upgrade'].includes(context) && <ContextualbarIcon name='user-plus' />}
|
|
<ContextualbarTitle>
|
|
{context === 'info' && t('User_Info')}
|
|
{context === 'edit' && t('Edit_User')}
|
|
{(context === 'new' || context === 'created') && t('New_user')}
|
|
{context === 'invite' && t('Invite_Users')}
|
|
</ContextualbarTitle>
|
|
<ContextualbarClose onClick={handleCloseContextualbar} />
|
|
</ContextualbarHeader>
|
|
{context === 'info' && id && <AdminUserInfoWithData uid={id} onReload={handleReload} tab={tab} />}
|
|
{context === 'edit' && id && (
|
|
<AdminUserFormWithData uid={id} onReload={handleReload} context={context} roleData={data} roleError={error} />
|
|
)}
|
|
{!isRoutePrevented && context === 'new' && (
|
|
<AdminUserForm onReload={handleReload} context={context} roleData={data} roleError={error} />
|
|
)}
|
|
{!isRoutePrevented && context === 'created' && id && <AdminUserCreated uid={id} />}
|
|
{!isRoutePrevented && context === 'invite' && <AdminInviteUsers />}
|
|
{isRoutePrevented && <AdminUserUpgrade />}
|
|
</ContextualbarDialog>
|
|
)}
|
|
</Page>
|
|
);
|
|
};
|
|
|
|
export default AdminUsersPage;
|