Skip to content

Commit

Permalink
feat: New users page pending tab (#31987)
Browse files Browse the repository at this point in the history
Co-authored-by: Tasso <[email protected]>
  • Loading branch information
rique223 and tassoevan authored Jun 21, 2024
1 parent 363a011 commit 5f95c4e
Show file tree
Hide file tree
Showing 18 changed files with 408 additions and 155 deletions.
13 changes: 13 additions & 0 deletions .changeset/metal-candles-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": patch
"@rocket.chat/i18n": patch
---

Implemented a new "Pending Users" tab on the users page to list users who have not yet been activated and/or have not logged in for the first time.
Additionally, added a "Pending Action" column to aid administrators in identifying necessary actions for each user. Incorporated a "Reason for Joining" field
into the user info contextual bar, along with a callout for exceeding the seats cap in the users page header. Finally, introduced a new logic to disable user creation buttons upon surpassing the seats cap.




1 change: 1 addition & 0 deletions apps/meteor/app/api/server/lib/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export async function findPaginatedUsersByStatus({
lastLogin: 1,
type: 1,
reason: 1,
federated: 1,
};

const actualSort: Record<string, 1 | -1> = sort || { username: 1 };
Expand Down
12 changes: 10 additions & 2 deletions apps/meteor/client/components/Page/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,20 @@ const PageHeader: FC<PageHeaderProps> = ({ children = undefined, title, onClickB
<Box
is='header'
borderBlockEndWidth='default'
minHeight='x64'
pb={8}
borderBlockEndColor={borderBlockEndColor ?? border ? 'extra-light' : 'transparent'}
{...props}
>
<Box height='100%' marginInline={24} display='flex' flexDirection='row' flexWrap='wrap' alignItems='center' color='default'>
<Box
height='100%'
marginInline={24}
minHeight='x64'
display='flex'
flexDirection='row'
flexWrap='wrap'
alignItems='center'
color='default'
>
{isMobile && (
<HeaderToolbar>
<SidebarToggler />
Expand Down
49 changes: 26 additions & 23 deletions apps/meteor/client/components/UserInfo/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type UserInfoProps = UserInfoDataProps & {
verified?: boolean;
actions: ReactElement;
roles: ReactElement[];
reason?: string;
};

const UserInfo = ({
Expand All @@ -59,6 +60,7 @@ const UserInfo = ({
customFields,
canViewAllInfo,
actions,
reason,
...props
}: UserInfoProps): ReactElement => {
const t = useTranslation();
Expand All @@ -79,53 +81,47 @@ const UserInfo = ({

<InfoPanel.Section>
{userDisplayName && <InfoPanel.Title icon={status} title={userDisplayName} />}

{statusText && (
<InfoPanel.Text>
<MarkdownText content={statusText} parseEmoji={true} />
<MarkdownText content={statusText} parseEmoji={true} variant='inline' />
</InfoPanel.Text>
)}
</InfoPanel.Section>

<InfoPanel.Section>
{roles.length !== 0 && (
{reason && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Roles')}</InfoPanel.Label>
<UserCardRoles>{roles}</UserCardRoles>
<InfoPanel.Label>{t('Reason_for_joining')}</InfoPanel.Label>
<InfoPanel.Text>{reason}</InfoPanel.Text>
</InfoPanel.Field>
)}

{Number.isInteger(utcOffset) && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Local_Time')}</InfoPanel.Label>
<InfoPanel.Text>{utcOffset && <UTCClock utcOffset={utcOffset} />}</InfoPanel.Text>
</InfoPanel.Field>
)}

{username && username !== name && (
{nickname && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Username')}</InfoPanel.Label>
<InfoPanel.Text data-qa='UserInfoUserName'>{username}</InfoPanel.Text>
<InfoPanel.Label>{t('Nickname')}</InfoPanel.Label>
<InfoPanel.Text>{nickname}</InfoPanel.Text>
</InfoPanel.Field>
)}

{canViewAllInfo && (
{roles.length !== 0 && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Last_login')}</InfoPanel.Label>
<InfoPanel.Text>{lastLogin ? timeAgo(lastLogin) : t('Never')}</InfoPanel.Text>
<InfoPanel.Label>{t('Roles')}</InfoPanel.Label>
<UserCardRoles>{roles}</UserCardRoles>
</InfoPanel.Field>
)}

{name && (
{username && username !== name && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Full_Name')}</InfoPanel.Label>
<InfoPanel.Text>{name}</InfoPanel.Text>
<InfoPanel.Label>{t('Username')}</InfoPanel.Label>
<InfoPanel.Text data-qa='UserInfoUserName'>{username}</InfoPanel.Text>
</InfoPanel.Field>
)}

{nickname && (
{Number.isInteger(utcOffset) && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Nickname')}</InfoPanel.Label>
<InfoPanel.Text>{nickname}</InfoPanel.Text>
<InfoPanel.Label>{t('Local_Time')}</InfoPanel.Label>
<InfoPanel.Text>{utcOffset && <UTCClock utcOffset={utcOffset} />}</InfoPanel.Text>
</InfoPanel.Field>
)}

Expand All @@ -138,6 +134,13 @@ const UserInfo = ({
</InfoPanel.Field>
)}

{Number.isInteger(utcOffset) && canViewAllInfo && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Last_login')}</InfoPanel.Label>
<InfoPanel.Text>{lastLogin ? timeAgo(lastLogin) : t('Never')}</InfoPanel.Text>
</InfoPanel.Field>
)}

{phone && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Phone')}</InfoPanel.Label>
Expand Down
63 changes: 63 additions & 0 deletions apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { LicenseBehavior, LicenseLimitKind } from '@rocket.chat/core-typings';
import { validateWarnLimit } from '@rocket.chat/license/src/validation/validateLimit';

import { useLicense } from './useLicense';

type LicenseLimitsByBehavior = Record<LicenseBehavior, LicenseLimitKind[]>;

export const useLicenseLimitsByBehavior = () => {
const result = useLicense({ loadValues: true });

if (result.isLoading || result.isError) {
return null;
}

const { license, limits } = result.data;

if (!license || !limits) {
return null;
}

const keyLimits = Object.keys(limits) as Array<keyof typeof limits>;

// Get the rule with the highest limit that applies to this key
const rules = keyLimits
.map((key) => {
const rule = license.limits[key]
?.filter((limit) => validateWarnLimit(limit.max, limits[key].value ?? 0, limit.behavior))
.reduce<{ max: number; behavior: LicenseBehavior } | null>(
(maxLimit, currentLimit) => (!maxLimit || currentLimit.max > maxLimit.max ? currentLimit : maxLimit),
null,
);

if (!rule) {
return undefined;
}

if (rule.max === 0) {
return undefined;
}

if (rule.max === -1) {
return undefined;
}

return [key, rule.behavior];
})
.filter(Boolean) as Array<[keyof typeof limits, LicenseBehavior]>;

if (!rules.length) {
return null;
}

// Group by behavior
return rules.reduce((acc, [key, behavior]) => {
if (!acc[behavior]) {
acc[behavior] = [];
}

acc[behavior].push(key);

return acc;
}, {} as LicenseLimitsByBehavior);
};
Original file line number Diff line number Diff line change
@@ -1,80 +1,33 @@
import type { LicenseBehavior } from '@rocket.chat/core-typings';
import type { LicenseInfo } from '@rocket.chat/core-typings';
import { Callout } from '@rocket.chat/fuselage';
import { validateWarnLimit } from '@rocket.chat/license/src/validation/validateLimit';
import { ExternalLink } from '@rocket.chat/ui-client';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';

import { useLicense } from '../../../hooks/useLicense';
import { useLicenseLimitsByBehavior } from '../../../hooks/useLicenseLimitsByBehavior';
import { useCheckoutUrl } from './hooks/useCheckoutUrl';

export const SubscriptionCalloutLimits = () => {
const manageSubscriptionUrl = useCheckoutUrl();

const { t } = useTranslation();
const result = useLicense({ loadValues: true });

if (result.isLoading || result.isError) {
return null;
}

const { license, limits } = result.data;
const licenseLimits = useLicenseLimitsByBehavior();

if (!license || !limits) {
if (!licenseLimits) {
return null;
}

const keyLimits = Object.keys(limits) as Array<keyof typeof limits>;

// Get the rule with the highest limit that applies to this key

const rules = keyLimits
.map((key) => {
const rule = license.limits[key]
?.filter((limit) => validateWarnLimit(limit.max, limits[key].value ?? 0, limit.behavior))
.sort((a, b) => b.max - a.max)[0];
const { prevent_action, disable_modules, invalidate_license, start_fair_policy } = licenseLimits;

if (!rule) {
return undefined;
}

if (rule.max === 0) {
return undefined;
}

if (rule.max === -1) {
return undefined;
}

return [key, rule.behavior];
})
.filter(Boolean) as Array<[keyof typeof limits, LicenseBehavior]>;

if (!rules.length) {
return null;
}

// Group by behavior
const groupedRules = rules.reduce((acc, [key, behavior]) => {
if (!acc[behavior]) {
acc[behavior] = [];
}

acc[behavior].push(key);

return acc;
}, {} as Record<LicenseBehavior, (keyof typeof limits)[]>);

const { prevent_action, disable_modules, invalidate_license, start_fair_policy } = groupedRules;

const map = (key: keyof typeof limits) => t(`subscription.callout.${key}`);
const toTranslationKey = (key: keyof LicenseInfo['limits']) => t(`subscription.callout.${key}`);

return (
<>
{start_fair_policy && (
<Callout type='warning' title={t('subscription.callout.servicesDisruptionsMayOccur')} m={8}>
<Trans i18nKey='subscription.callout.description.limitsReached' count={start_fair_policy.length}>
Your workspace reached the <>{{ val: start_fair_policy.map(map) }}</> limit.
Your workspace reached the <>{{ val: start_fair_policy.map(toTranslationKey) }}</> limit.
<ExternalLink
to={manageSubscriptionUrl({
target: 'callout',
Expand All @@ -88,10 +41,11 @@ export const SubscriptionCalloutLimits = () => {
</Trans>
</Callout>
)}

{prevent_action && (
<Callout type='danger' title={t('subscription.callout.servicesDisruptionsOccurring')} m={8}>
<Trans i18nKey='subscription.callout.description.limitsExceeded' count={prevent_action.length}>
Your workspace exceeded the <>{{ val: prevent_action.map(map) }}</> license limit.
Your workspace exceeded the <>{{ val: prevent_action.map(toTranslationKey) }}</> license limit.
<ExternalLink
to={manageSubscriptionUrl({
target: 'callout',
Expand All @@ -109,7 +63,7 @@ export const SubscriptionCalloutLimits = () => {
{disable_modules && (
<Callout type='danger' title={t('subscription.callout.capabilitiesDisabled')} m={8}>
<Trans i18nKey='subscription.callout.description.limitsExceeded' count={disable_modules.length}>
Your workspace exceeded the <>{{ val: disable_modules.map(map) }}</> license limit.
Your workspace exceeded the <>{{ val: disable_modules.map(toTranslationKey) }}</> license limit.
<ExternalLink
to={manageSubscriptionUrl({
target: 'callout',
Expand All @@ -127,7 +81,7 @@ export const SubscriptionCalloutLimits = () => {
{invalidate_license && (
<Callout type='danger' title={t('subscription.callout.allPremiumCapabilitiesDisabled')} m={8}>
<Trans i18nKey='subscription.callout.description.limitsExceeded' count={disable_modules.length}>
Your workspace exceeded the <>{{ val: invalidate_license.map(map) }}</> license limit.
Your workspace exceeded the <>{{ val: invalidate_license.map(toTranslationKey) }}</> license limit.
<ExternalLink
to={manageSubscriptionUrl({
target: 'callout',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { isUserFederated } from '@rocket.chat/core-typings';
import type { IUser } from '@rocket.chat/core-typings';
import { Callout } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
Expand Down Expand Up @@ -69,6 +68,7 @@ const AdminUserInfoWithData = ({ uid, onReload }: AdminUserInfoWithDataProps): R
lastLogin,
nickname,
canViewAllInfo,
reason,
} = data.user;

return {
Expand All @@ -91,6 +91,7 @@ const AdminUserInfoWithData = ({ uid, onReload }: AdminUserInfoWithDataProps): R
status: <UserStatus status={status} />,
statusText,
nickname,
reason,
};
}, [approveManuallyUsers, data, getRoles]);

Expand Down Expand Up @@ -119,7 +120,7 @@ const AdminUserInfoWithData = ({ uid, onReload }: AdminUserInfoWithDataProps): R
isAdmin={data?.user.roles?.includes('admin')}
userId={data?.user._id}
username={user.username}
isFederatedUser={isUserFederated(data?.user as unknown as IUser)}
isFederatedUser={!!data.user.federated}
onChange={onChange}
onReload={onReload}
/>
Expand Down
Loading

0 comments on commit 5f95c4e

Please sign in to comment.