snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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 1s
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 1s
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import type { IStats } from '@rocket.chat/core-typings';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import DeploymentCard from './DeploymentCard';
|
||||
|
||||
export default {
|
||||
component: DeploymentCard,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
args: {
|
||||
statistics: {
|
||||
uniqueId: '',
|
||||
version: '1.0.0',
|
||||
process: {
|
||||
nodeVersion: '',
|
||||
pid: 0,
|
||||
},
|
||||
migration: {
|
||||
version: 272,
|
||||
lockedAt: '',
|
||||
},
|
||||
msEnabled: false,
|
||||
oplogEnabled: false,
|
||||
mongoVersion: '',
|
||||
mongoStorageEngine: '',
|
||||
} as IStats,
|
||||
instances: [],
|
||||
serverInfo: {
|
||||
info: {
|
||||
commit: {},
|
||||
marketplaceApiVersion: '',
|
||||
build: {
|
||||
date: '',
|
||||
arch: '',
|
||||
platform: '',
|
||||
cpus: 0,
|
||||
freeMemory: 0,
|
||||
totalMemory: 0,
|
||||
nodeVersion: '',
|
||||
osRelease: '',
|
||||
},
|
||||
version: '',
|
||||
},
|
||||
cloudWorkspaceId: '',
|
||||
minimumClientVersions: {
|
||||
desktop: '',
|
||||
mobile: '',
|
||||
},
|
||||
version: '',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof DeploymentCard>;
|
||||
|
||||
export const Example = {
|
||||
name: 'DeploymentCard',
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { IWorkspaceInfo, IStats } from '@rocket.chat/core-typings';
|
||||
import { Button, Card, CardBody, CardControls, Margins } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import type { IInstance } from '@rocket.chat/rest-typings';
|
||||
import { useSetModal } from '@rocket.chat/ui-contexts';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime';
|
||||
import WorkspaceCardSection from '../components/WorkspaceCardSection';
|
||||
import InstancesModal from './components/InstancesModal';
|
||||
import WorkspaceCardSectionTitle from '../components/WorkspaceCardSectionTitle';
|
||||
|
||||
export type DeploymentCardProps = {
|
||||
serverInfo: IWorkspaceInfo;
|
||||
instances: IInstance[];
|
||||
statistics: IStats;
|
||||
};
|
||||
|
||||
const DeploymentCard = ({
|
||||
serverInfo: { info, cloudWorkspaceId, workspaceUrl, hashedWorkspaceUrl },
|
||||
statistics,
|
||||
instances,
|
||||
}: DeploymentCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const formatDateAndTime = useFormatDateAndTime();
|
||||
const setModal = useSetModal();
|
||||
|
||||
const { commit = {}, marketplaceApiVersion: appsEngineVersion } = info || {};
|
||||
|
||||
const handleInstancesModal = useStableCallback(() => {
|
||||
setModal(<InstancesModal instances={instances} onClose={(): void => setModal()} />);
|
||||
});
|
||||
|
||||
return (
|
||||
<Card height='full'>
|
||||
<CardBody flexDirection='column'>
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Deployment')} variant='h4' />
|
||||
</WorkspaceCardSection>
|
||||
<Margins blockEnd={8}>
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Version')} />
|
||||
{statistics.version}
|
||||
</WorkspaceCardSection>
|
||||
{workspaceUrl && (
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Site_Url')} />
|
||||
{workspaceUrl}
|
||||
</WorkspaceCardSection>
|
||||
)}
|
||||
{hashedWorkspaceUrl && (
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Hashed_Site_Url')} />
|
||||
<span style={{ lineBreak: 'anywhere' }}>{hashedWorkspaceUrl}</span>
|
||||
</WorkspaceCardSection>
|
||||
)}
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Deployment_ID')} />
|
||||
{statistics.uniqueId}
|
||||
</WorkspaceCardSection>
|
||||
|
||||
{cloudWorkspaceId && (
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Cloud_Workspace_Id')} />
|
||||
{cloudWorkspaceId}
|
||||
</WorkspaceCardSection>
|
||||
)}
|
||||
|
||||
{appsEngineVersion && (
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Apps_Engine_Version')} />
|
||||
{appsEngineVersion}
|
||||
</WorkspaceCardSection>
|
||||
)}
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Node_version')} />
|
||||
{statistics.process.nodeVersion}
|
||||
</WorkspaceCardSection>
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('DB_Migration')} />
|
||||
{`${statistics.migration.version} (${formatDateAndTime(statistics.migration.lockedAt)})`}
|
||||
</WorkspaceCardSection>
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('MongoDB')} />
|
||||
{`${statistics.mongoVersion} / ${statistics.mongoStorageEngine}`}
|
||||
</WorkspaceCardSection>
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Commit_details')} />
|
||||
{t('github_HEAD')}: ({commit.hash ? commit.hash.slice(0, 9) : ''}) <br />
|
||||
{t('Branch')}: {commit.branch} <br />
|
||||
{commit.subject}
|
||||
</WorkspaceCardSection>
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('PID')} />
|
||||
{statistics.process.pid}
|
||||
</WorkspaceCardSection>
|
||||
</Margins>
|
||||
</CardBody>
|
||||
|
||||
{!!instances.length && (
|
||||
<CardControls>
|
||||
<Button medium onClick={handleInstancesModal}>
|
||||
{t('Instances')}
|
||||
</Button>
|
||||
</CardControls>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DeploymentCard);
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { StoryObj, Meta } from '@storybook/react';
|
||||
|
||||
import DescriptionList from './DescriptionList';
|
||||
import DescriptionListEntry from './DescriptionListEntry';
|
||||
|
||||
export default {
|
||||
component: DescriptionList,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
decorators: [(fn) => <div>{fn()}</div>],
|
||||
} satisfies Meta<typeof DescriptionList>;
|
||||
|
||||
export const Default: StoryObj<typeof DescriptionList> = {
|
||||
render: (args) => (
|
||||
<DescriptionList {...args}>
|
||||
<DescriptionListEntry label='Key'>Value</DescriptionListEntry>
|
||||
<DescriptionListEntry label='Key'>Value</DescriptionListEntry>
|
||||
<DescriptionListEntry label='Key'>Value</DescriptionListEntry>
|
||||
</DescriptionList>
|
||||
),
|
||||
|
||||
name: 'DescriptionList',
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Box, Table, TableBody } from '@rocket.chat/fuselage';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export type DescriptionListProps = {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const DescriptionList = ({ children, title }: DescriptionListProps) => (
|
||||
<>
|
||||
{title && (
|
||||
<Box display='flex' justifyContent='flex-end' width='30%' paddingInline={8}>
|
||||
{title}
|
||||
</Box>
|
||||
)}
|
||||
<Table striped marginBlockEnd={32} width='full'>
|
||||
<TableBody>{children}</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
|
||||
export default memo(DescriptionList);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { TableRow, TableCell } from '@rocket.chat/fuselage';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export type DescriptionListEntryProps = {
|
||||
label: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const DescriptionListEntry = ({ children, label }: DescriptionListEntryProps) => (
|
||||
<TableRow>
|
||||
<TableCell wordBreak='break-word' is='th' scope='col' align='end' color='hint' backgroundColor='surface' fontScale='p2m'>
|
||||
{label}
|
||||
</TableCell>
|
||||
<TableCell align='start' color='default'>
|
||||
{children}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
export default memo(DescriptionListEntry);
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import InstancesModal from './InstancesModal';
|
||||
|
||||
export default {
|
||||
component: InstancesModal,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
argTypes: {
|
||||
onClose: { action: 'onClose' },
|
||||
},
|
||||
} satisfies Meta<typeof InstancesModal>;
|
||||
|
||||
export const Default = {
|
||||
name: 'InstancesModal',
|
||||
|
||||
args: {
|
||||
instances: [
|
||||
{
|
||||
address: 'http://localhost:3000',
|
||||
broadcastAuth: false,
|
||||
currentStatus: {
|
||||
connected: true,
|
||||
},
|
||||
instanceRecord: {
|
||||
_createdAt: new Date(),
|
||||
_id: 'instance-id',
|
||||
name: 'instance-name',
|
||||
pid: 123,
|
||||
extraInformation: {
|
||||
host: '127.0.0.1',
|
||||
nodeVersion: 'v14.18.2',
|
||||
port: '',
|
||||
tcpPort: 123,
|
||||
os: {
|
||||
type: 'macOSX',
|
||||
platform: '',
|
||||
arch: 'x64',
|
||||
release: '236',
|
||||
uptime: 10,
|
||||
loadavg: [123, 123],
|
||||
totalmem: 123,
|
||||
freemem: 123,
|
||||
cpus: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Accordion, AccordionItem } from '@rocket.chat/fuselage';
|
||||
import type { IInstance } from '@rocket.chat/rest-typings';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DescriptionList from './DescriptionList';
|
||||
import DescriptionListEntry from './DescriptionListEntry';
|
||||
import { useFormatDateAndTime } from '../../../../../../hooks/useFormatDateAndTime';
|
||||
|
||||
export type InstancesModalProps = {
|
||||
instances: IInstance[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const InstancesModal = ({ instances = [], onClose }: InstancesModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const formatDateAndTime = useFormatDateAndTime();
|
||||
|
||||
return (
|
||||
<GenericModal onConfirm={onClose} confirmText={t('Close')} icon={null} title={t('Instances')} onClose={onClose}>
|
||||
<Accordion>
|
||||
{instances.map(({ address, broadcastAuth, currentStatus, instanceRecord }) => (
|
||||
<AccordionItem defaultExpanded title={address} key={address}>
|
||||
<DescriptionList>
|
||||
<DescriptionListEntry label={t('Address')}>{address}</DescriptionListEntry>
|
||||
<DescriptionListEntry label={t('Auth')}>{broadcastAuth ? 'true' : 'false'}</DescriptionListEntry>
|
||||
<DescriptionListEntry
|
||||
label={
|
||||
<>
|
||||
{t('Current_Status')} > {t('Connected')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{currentStatus.connected ? 'true' : 'false'}
|
||||
</DescriptionListEntry>
|
||||
<DescriptionListEntry
|
||||
label={
|
||||
<>
|
||||
{t('Current_Status')} > {t('Local')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{currentStatus.local ? 'true' : 'false'}
|
||||
</DescriptionListEntry>
|
||||
<DescriptionListEntry
|
||||
label={
|
||||
<>
|
||||
{t('Current_Status')} > {t('Last_Heartbeat_Time')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{currentStatus.lastHeartbeatTime}
|
||||
</DescriptionListEntry>
|
||||
<DescriptionListEntry label={<>{t('Instance_Record')} > ID</>}>{instanceRecord?._id}</DescriptionListEntry>
|
||||
<DescriptionListEntry
|
||||
label={
|
||||
<>
|
||||
{t('Instance_Record')} > {t('PID')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{instanceRecord?.pid}
|
||||
</DescriptionListEntry>
|
||||
<DescriptionListEntry
|
||||
label={
|
||||
<>
|
||||
{t('Instance_Record')} > {t('Created_at')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{formatDateAndTime(instanceRecord?._createdAt)}
|
||||
</DescriptionListEntry>
|
||||
</DescriptionList>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstancesModal;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './InstancesModal';
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { IStats } from '@rocket.chat/core-typings';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import MessagesRoomsCard from './MessagesRoomsCard';
|
||||
|
||||
export default {
|
||||
component: MessagesRoomsCard,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
args: {
|
||||
statistics: {
|
||||
totalChannels: 12,
|
||||
totalPrivateGroups: 23,
|
||||
totalDirect: 21,
|
||||
totalDiscussions: 32,
|
||||
totalLivechat: 31,
|
||||
totalMessages: 321,
|
||||
totalDirectMessages: 23,
|
||||
totalDiscussionsMessages: 32,
|
||||
totalLivechatMessages: 31,
|
||||
} as IStats,
|
||||
},
|
||||
} satisfies Meta<typeof MessagesRoomsCard>;
|
||||
|
||||
export const Example = {};
|
||||
export const Vertical = {};
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { IStats } from '@rocket.chat/core-typings';
|
||||
import { Card } from '@rocket.chat/fuselage';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import WorkspaceCardSection from '../components/WorkspaceCardSection';
|
||||
import WorkspaceCardSectionTitle from '../components/WorkspaceCardSectionTitle';
|
||||
import WorkspaceCardTextSeparator from '../components/WorkspaceCardTextSeparator';
|
||||
|
||||
export type MessagesRoomsCardProps = {
|
||||
statistics: IStats;
|
||||
};
|
||||
|
||||
const MessagesRoomsCard = ({ statistics }: MessagesRoomsCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card height='full'>
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Total_rooms')} variant='h4' />
|
||||
|
||||
<WorkspaceCardTextSeparator label={t('Channels')} icon='hash' value={statistics.totalChannels} />
|
||||
<WorkspaceCardTextSeparator label={t('Private_Groups')} icon='lock' value={statistics.totalPrivateGroups} />
|
||||
<WorkspaceCardTextSeparator label={t('Direct_Messages')} icon='balloon' value={statistics.totalDirect} />
|
||||
<WorkspaceCardTextSeparator label={t('Discussions')} icon='discussion' value={statistics.totalDiscussions} />
|
||||
<WorkspaceCardTextSeparator label={t('Omnichannel')} icon='headset' value={statistics.totalLivechat} />
|
||||
<WorkspaceCardTextSeparator label={t('Total')} value={statistics.totalRooms} />
|
||||
</WorkspaceCardSection>
|
||||
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Messages')} variant='h4' />
|
||||
<WorkspaceCardTextSeparator label={t('Stats_Total_Messages_Channel')} icon='hash' value={statistics.totalChannelMessages} />
|
||||
<WorkspaceCardTextSeparator
|
||||
label={t('Stats_Total_Messages_PrivateGroup')}
|
||||
icon='lock'
|
||||
value={statistics.totalPrivateGroupMessages}
|
||||
/>
|
||||
<WorkspaceCardTextSeparator label={t('Stats_Total_Messages_Direct')} icon='balloon' value={statistics.totalDirectMessages} />
|
||||
<WorkspaceCardTextSeparator
|
||||
label={t('Stats_Total_Messages_Discussions')}
|
||||
icon='discussion'
|
||||
value={statistics.totalDiscussionsMessages}
|
||||
/>
|
||||
<WorkspaceCardTextSeparator label={t('Stats_Total_Messages_Livechat')} icon='headset' value={statistics.totalLivechatMessages} />
|
||||
<WorkspaceCardTextSeparator label={t('Total')} value={statistics.totalMessages} />
|
||||
</WorkspaceCardSection>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MessagesRoomsCard);
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { IStats } from '@rocket.chat/core-typings';
|
||||
import { Button, Card, CardBody, CardControls, Margins } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { useRouter } from '@rocket.chat/ui-contexts';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useFormatMemorySize } from '../../../../hooks/useFormatMemorySize';
|
||||
import { useHasLicenseModule } from '../../../../hooks/useHasLicenseModule';
|
||||
import WorkspaceCardSection from '../components/WorkspaceCardSection';
|
||||
import WorkspaceCardSectionTitle from '../components/WorkspaceCardSectionTitle';
|
||||
import WorkspaceCardTextSeparator from '../components/WorkspaceCardTextSeparator';
|
||||
|
||||
export type UsersUploadsCardProps = {
|
||||
statistics: IStats;
|
||||
};
|
||||
|
||||
const UsersUploadsCard = ({ statistics }: UsersUploadsCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const formatMemorySize = useFormatMemorySize();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleEngagement = useStableCallback(() => {
|
||||
router.navigate('/admin/engagement');
|
||||
});
|
||||
|
||||
const { data: canViewEngagement = false } = useHasLicenseModule('engagement-dashboard');
|
||||
|
||||
return (
|
||||
<Card height='full'>
|
||||
<CardBody flexDirection='column'>
|
||||
<Margins blockEnd={24}>
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Users')} variant='h4' />
|
||||
|
||||
<WorkspaceCardTextSeparator label={t('Online')} status='online' value={statistics.onlineUsers} />
|
||||
<WorkspaceCardTextSeparator label={t('Busy')} status='busy' value={statistics.busyUsers} />
|
||||
<WorkspaceCardTextSeparator label={t('Away')} status='away' value={statistics.awayUsers} />
|
||||
<WorkspaceCardTextSeparator label={t('Offline')} status='offline' value={statistics.offlineUsers} />
|
||||
<WorkspaceCardTextSeparator label={t('Total')} value={statistics.totalUsers} />
|
||||
</WorkspaceCardSection>
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Types')} variant='h4' />
|
||||
|
||||
<WorkspaceCardTextSeparator label={t('Users_Connected')} value={statistics.totalConnectedUsers} />
|
||||
<WorkspaceCardTextSeparator label={t('Stats_Active_Users')} value={statistics.activeUsers} />
|
||||
<WorkspaceCardTextSeparator label={t('Stats_Active_Guests')} value={statistics.activeGuests} />
|
||||
<WorkspaceCardTextSeparator label={t('Stats_Non_Active_Users')} value={statistics.nonActiveUsers} />
|
||||
<WorkspaceCardTextSeparator label={t('Stats_App_Users')} value={statistics.appUsers} />
|
||||
</WorkspaceCardSection>
|
||||
|
||||
<WorkspaceCardSection>
|
||||
<WorkspaceCardSectionTitle title={t('Uploads')} variant='h4' />
|
||||
|
||||
<WorkspaceCardTextSeparator label={t('Stats_Total_Uploads')} value={statistics.uploadsTotal} />
|
||||
<WorkspaceCardTextSeparator label={t('Stats_Total_Uploads_Size')} value={formatMemorySize(statistics.uploadsTotalSize)} />
|
||||
</WorkspaceCardSection>
|
||||
</Margins>
|
||||
</CardBody>
|
||||
|
||||
<CardControls>
|
||||
<Button disabled={!canViewEngagement} medium onClick={handleEngagement}>
|
||||
{t('See_on_Engagement_Dashboard')}
|
||||
</Button>
|
||||
</CardControls>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UsersUploadsCard);
|
||||
@@ -0,0 +1,225 @@
|
||||
import type { IWorkspaceInfo } from '@rocket.chat/core-typings';
|
||||
import { Box, Card, CardBody, CardCol, CardControls, CardHeader, CardTitle, Icon } from '@rocket.chat/fuselage';
|
||||
import { useBreakpoints } from '@rocket.chat/fuselage-hooks';
|
||||
import type { SupportedVersions } from '@rocket.chat/server-cloud-communication';
|
||||
import { ExternalLink, useLicense, useLicenseName } from '@rocket.chat/ui-client';
|
||||
import type { LocationPathname } from '@rocket.chat/ui-contexts';
|
||||
import { useSetModal, useMediaUrl } from '@rocket.chat/ui-contexts';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import VersionCardActionButton from './components/VersionCardActionButton';
|
||||
import type { VersionActionItem } from './components/VersionCardActionItem';
|
||||
import VersionCardActionItem from './components/VersionCardActionItem';
|
||||
import { VersionCardSkeleton } from './components/VersionCardSkeleton';
|
||||
import { VersionTag } from './components/VersionTag';
|
||||
import { getVersionStatus } from './getVersionStatus';
|
||||
import RegisterWorkspaceModal from './modals/RegisterWorkspaceModal';
|
||||
import { useFormatDate } from '../../../../hooks/useFormatDate';
|
||||
import { useRegistrationStatus } from '../../../../hooks/useRegistrationStatus';
|
||||
import { links } from '../../../../lib/links';
|
||||
import { isOverLicenseLimits } from '../../../../lib/utils/isOverLicenseLimits';
|
||||
|
||||
const SUPPORT_EXTERNAL_LINK = links.go.versionSupport;
|
||||
const RELEASES_EXTERNAL_LINK = links.go.updateProduct;
|
||||
|
||||
export type VersionCardProps = {
|
||||
serverInfo: IWorkspaceInfo;
|
||||
};
|
||||
|
||||
const VersionCard = ({ serverInfo }: VersionCardProps) => {
|
||||
const breakpoints = useBreakpoints();
|
||||
const isExtraLargeOrBigger = breakpoints.includes('xl');
|
||||
|
||||
const getUrl = useMediaUrl();
|
||||
const cardBackground = {
|
||||
backgroundImage: `url(${getUrl('images/globe.png')})`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: isExtraLargeOrBigger ? 'right 20px center' : 'left 450px center',
|
||||
backgroundSize: 'auto',
|
||||
};
|
||||
|
||||
const setModal = useSetModal();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
const { data: licenseData, isPending, refetch: refetchLicense } = useLicense({ loadValues: true });
|
||||
const { isRegistered, canViewRegistrationStatus } = useRegistrationStatus();
|
||||
|
||||
const { license, limits } = licenseData || {};
|
||||
const isAirgapped = license?.information?.offline;
|
||||
const licenseName = useLicenseName();
|
||||
|
||||
const serverVersion = serverInfo.version;
|
||||
|
||||
const { versionStatus, versions } = useMemo(() => {
|
||||
const supportedVersions = serverInfo?.supportedVersions?.signed ? decodeBase64(serverInfo?.supportedVersions?.signed) : undefined;
|
||||
|
||||
if (!supportedVersions) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const versionStatus = getVersionStatus(serverVersion, supportedVersions?.versions);
|
||||
|
||||
return {
|
||||
versionStatus,
|
||||
versions: supportedVersions?.versions,
|
||||
};
|
||||
}, [serverInfo?.supportedVersions?.signed, serverVersion]);
|
||||
|
||||
const isOverLimits = limits && isOverLicenseLimits(limits);
|
||||
|
||||
const actionButton:
|
||||
| undefined
|
||||
| {
|
||||
path: LocationPathname;
|
||||
label: ReactNode;
|
||||
}
|
||||
| {
|
||||
action: () => void;
|
||||
label: ReactNode;
|
||||
} = useMemo(() => {
|
||||
if (canViewRegistrationStatus && !isRegistered) {
|
||||
return {
|
||||
action: () => {
|
||||
const handleModalClose = (): void => {
|
||||
setModal(null);
|
||||
refetchLicense();
|
||||
};
|
||||
setModal(<RegisterWorkspaceModal onClose={handleModalClose} onStatusChange={refetchLicense} />);
|
||||
},
|
||||
label: t('RegisterWorkspace_Button'),
|
||||
};
|
||||
}
|
||||
|
||||
if (versionStatus?.label === 'outdated') {
|
||||
return {
|
||||
action: () => {
|
||||
window.open(RELEASES_EXTERNAL_LINK, '_blank');
|
||||
},
|
||||
label: t('Update_version'),
|
||||
};
|
||||
}
|
||||
|
||||
if (isOverLimits) {
|
||||
return { path: '/admin/subscription', label: t('Manage_subscription') };
|
||||
}
|
||||
}, [canViewRegistrationStatus, isRegistered, versionStatus, isOverLimits, t, setModal, refetchLicense]);
|
||||
|
||||
const actionItems = useMemo(() => {
|
||||
return (
|
||||
[
|
||||
isOverLimits
|
||||
? {
|
||||
danger: true,
|
||||
icon: 'warning',
|
||||
label: t('Plan_limits_reached'),
|
||||
}
|
||||
: {
|
||||
icon: 'check',
|
||||
label: t('Operating_withing_plan_limits'),
|
||||
},
|
||||
(isAirgapped || !versions) && {
|
||||
icon: 'warning',
|
||||
label: (
|
||||
<Trans i18nKey='Check_support_availability'>
|
||||
Check
|
||||
<ExternalLink to={SUPPORT_EXTERNAL_LINK}>support</ExternalLink>
|
||||
availability
|
||||
</Trans>
|
||||
),
|
||||
},
|
||||
|
||||
versionStatus?.label !== 'outdated' &&
|
||||
versionStatus?.expiration && {
|
||||
icon: 'check',
|
||||
label: (
|
||||
<Trans i18nKey='Version_supported_until' values={{ date: formatDate(versionStatus?.expiration) }}>
|
||||
Version
|
||||
<ExternalLink to={SUPPORT_EXTERNAL_LINK}>supported</ExternalLink>
|
||||
until {formatDate(versionStatus?.expiration)}
|
||||
</Trans>
|
||||
),
|
||||
},
|
||||
versionStatus?.label === 'outdated' && {
|
||||
danger: true,
|
||||
icon: 'warning',
|
||||
label: (
|
||||
<Trans i18nKey='Version_not_supported'>
|
||||
Version
|
||||
<ExternalLink to={SUPPORT_EXTERNAL_LINK}>not supported</ExternalLink>
|
||||
</Trans>
|
||||
),
|
||||
},
|
||||
canViewRegistrationStatus &&
|
||||
(isRegistered
|
||||
? {
|
||||
icon: 'check',
|
||||
label: t('Workspace_registered'),
|
||||
}
|
||||
: {
|
||||
danger: true,
|
||||
icon: 'warning',
|
||||
label: t('Workspace_not_registered'),
|
||||
}),
|
||||
].filter(Boolean) as VersionActionItem[]
|
||||
).sort((a) => (a.danger ? -1 : 1));
|
||||
}, [
|
||||
isOverLimits,
|
||||
t,
|
||||
isAirgapped,
|
||||
versions,
|
||||
versionStatus?.label,
|
||||
versionStatus?.expiration,
|
||||
formatDate,
|
||||
canViewRegistrationStatus,
|
||||
isRegistered,
|
||||
]);
|
||||
|
||||
if (isPending && !licenseData) {
|
||||
return (
|
||||
<Card style={{ ...cardBackground }}>
|
||||
<VersionCardSkeleton />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={{ ...cardBackground }}>
|
||||
<CardCol>
|
||||
<CardHeader>
|
||||
<CardTitle variant='h3'>{t('Version_version', { version: serverVersion })}</CardTitle>
|
||||
{!isAirgapped && versions && <VersionTag versionStatus={versionStatus?.label} title={versionStatus.version} />}
|
||||
</CardHeader>
|
||||
|
||||
<Box color='secondary-info'>
|
||||
<Icon name='rocketchat' size={16} /> {licenseName.data}
|
||||
</Box>
|
||||
</CardCol>
|
||||
|
||||
<CardBody flexDirection='column'>
|
||||
{actionItems.length > 0 && actionItems.map((item, index) => <VersionCardActionItem key={index} {...item} />)}
|
||||
</CardBody>
|
||||
|
||||
{actionButton && (
|
||||
<CardControls>
|
||||
<VersionCardActionButton {...actionButton} />
|
||||
</CardControls>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionCard;
|
||||
|
||||
const decodeBase64 = (b64: string): SupportedVersions | undefined => {
|
||||
const [, bodyEncoded] = b64.split('.');
|
||||
if (!bodyEncoded) {
|
||||
return;
|
||||
}
|
||||
|
||||
return JSON.parse(atob(bodyEncoded));
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Button } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import type { LocationPathname } from '@rocket.chat/ui-contexts';
|
||||
import { useRouter } from '@rocket.chat/ui-contexts';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export type VersionCardActionButtonProps =
|
||||
| {
|
||||
path: LocationPathname;
|
||||
label: ReactNode;
|
||||
}
|
||||
| {
|
||||
action: () => void;
|
||||
label: ReactNode;
|
||||
};
|
||||
|
||||
const VersionCardActionButton = (item: VersionCardActionButtonProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleActionButton = useStableCallback(() => {
|
||||
if ('action' in item) {
|
||||
return item.action();
|
||||
}
|
||||
|
||||
router.navigate(item.path);
|
||||
});
|
||||
|
||||
return (
|
||||
<Button primary onClick={() => handleActionButton()}>
|
||||
{item.label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(VersionCardActionButton);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Box, FramedIcon } from '@rocket.chat/fuselage';
|
||||
import type { Keys } from '@rocket.chat/icons';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type VersionActionItem = {
|
||||
danger?: boolean;
|
||||
icon: Keys;
|
||||
label: ReactNode;
|
||||
};
|
||||
|
||||
export type VersionCardActionItemProps = VersionActionItem;
|
||||
|
||||
const VersionCardActionItem = ({ icon, label, danger }: VersionCardActionItemProps) => {
|
||||
return (
|
||||
<Box display='flex' alignItems='center' color={danger ? 'danger' : 'secondary-info'} fontScale='p2m'>
|
||||
<FramedIcon danger={danger} icon={icon} />
|
||||
<Box marginInlineStart={12}>{label}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionCardActionItem;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Box, Skeleton } from '@rocket.chat/fuselage';
|
||||
|
||||
export const VersionCardSkeleton = () => {
|
||||
return (
|
||||
<>
|
||||
<Skeleton height={28} width={160} />
|
||||
<Skeleton height={28} width={150} />
|
||||
<Box display='flex' width={160} height={28} alignItems='center'>
|
||||
<Skeleton height={38} width={28} marginInlineEnd={12} />
|
||||
<Skeleton height={20} width={120} />
|
||||
</Box>
|
||||
<Box display='flex' width={180} height={28} alignItems='center'>
|
||||
<Skeleton height={38} width={28} marginInlineEnd={12} />
|
||||
<Skeleton height={20} width={140} />
|
||||
</Box>
|
||||
<Box display='flex' width={160} height={28} alignItems='center'>
|
||||
<Skeleton height={38} width={28} marginInlineEnd={12} />
|
||||
<Skeleton height={20} width={120} />
|
||||
</Box>
|
||||
<Skeleton height={50} width={160} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Tag } from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type VersionStatus = 'outdated' | 'latest' | 'available_version' | undefined;
|
||||
|
||||
export type VersionTagProps = {
|
||||
versionStatus: VersionStatus;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export const VersionTag = ({ versionStatus, title }: VersionTagProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (versionStatus === 'outdated') {
|
||||
return (
|
||||
<Tag title={title} variant='danger'>
|
||||
{t('Outdated')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
if (versionStatus === 'latest') {
|
||||
return (
|
||||
<Tag title={title} variant='primary'>
|
||||
{t('Latest')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag title={title} variant='secondary'>
|
||||
{t('New_version_available')}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { getVersionStatus } from './getVersionStatus';
|
||||
|
||||
describe('if the server version from server and the highest version from cloud are the same', () => {
|
||||
describe('the expiration date is in the future', () => {
|
||||
it('should return as latest version', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('latest');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the expiration date is in the past', () => {
|
||||
it('should return as outdated version', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date('2020-01-01'),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('outdated');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the server version is not in the list of supported versions', () => {
|
||||
it('should return as outdated version', () => {
|
||||
const status = getVersionStatus('2.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('outdated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the server version is in the list of supported versions but is not the highest', () => {
|
||||
describe('the expiration date is in the future', () => {
|
||||
it('should return as available version', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '4.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
expect(status.label).toBe('available_version');
|
||||
});
|
||||
});
|
||||
describe('the expiration date is in the past', () => {
|
||||
it('should return as outdated version', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date('2020-01-01'),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '4.0.0',
|
||||
expiration: new Date(),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
expect(status.label).toBe('outdated');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the server version is not in the list of supported versions but is the highest', () => {
|
||||
it('should return as latest version', () => {
|
||||
const status = getVersionStatus('4.0.0', [
|
||||
{
|
||||
version: '2.0.0',
|
||||
expiration: new Date(),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('outdated');
|
||||
expect(status.version).toBe('4.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prerelease version', () => {
|
||||
it('should not suggest available version the highest version is a prerelease from a different tag', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '4.0.0-develop',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('latest');
|
||||
expect(status.version).toBe('3.0.0');
|
||||
});
|
||||
|
||||
it('should suggest available version the highest version is a prerelease from the same tag', () => {
|
||||
const status = getVersionStatus('6.5.0-rc.14', [
|
||||
{
|
||||
version: '6.5.0-rc.14',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '6.5.0-rc.15',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '4.0.0-develop',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('available_version');
|
||||
expect(status.version).toBe('6.5.0-rc.15');
|
||||
});
|
||||
|
||||
it('should suggest available version the highest version even if the current one is a prerelease', () => {
|
||||
const status = getVersionStatus('6.5.0-rc.14', [
|
||||
{
|
||||
version: '6.5.0-rc.14',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '6.5.0-rc.15',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '7.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('available_version');
|
||||
expect(status.version).toBe('7.0.0');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { SupportedVersions } from '@rocket.chat/server-cloud-communication';
|
||||
import semver from 'semver';
|
||||
|
||||
import type { VersionStatus } from './components/VersionTag';
|
||||
|
||||
export const getVersionStatus = (
|
||||
serverVersion: string,
|
||||
versions: SupportedVersions['versions'],
|
||||
): { label: VersionStatus; expiration: Date | undefined; version: string } => {
|
||||
const highestVersion = versions
|
||||
.sort((prev, current) => semver.compare(current.version, prev.version))
|
||||
.filter((v) => {
|
||||
const [prerelease] = semver.prerelease(v.version) || [];
|
||||
const [currentPrerelease] = semver.prerelease(serverVersion) || [];
|
||||
|
||||
return !prerelease || prerelease === currentPrerelease;
|
||||
})[0];
|
||||
|
||||
const currentVersionData =
|
||||
versions.find((v) => v.version === serverVersion) ||
|
||||
versions.find((v) =>
|
||||
semver.satisfies(String(semver.coerce(v.version)), `=${semver.major(serverVersion)}.${semver.minor(serverVersion)}.x`),
|
||||
);
|
||||
|
||||
const currentVersionIsExpired = currentVersionData?.expiration && new Date(currentVersionData.expiration) < new Date();
|
||||
|
||||
const isSupported = !currentVersionIsExpired && !!currentVersionData;
|
||||
|
||||
return {
|
||||
label: 'outdated',
|
||||
version: serverVersion,
|
||||
...(isSupported && semver.gte(currentVersionData?.version, highestVersion.version) && { label: 'latest' }),
|
||||
...(isSupported &&
|
||||
semver.gt(highestVersion.version, currentVersionData?.version) && { label: 'available_version', version: highestVersion.version }),
|
||||
expiration: currentVersionData?.expiration,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type featureBulletsType = {
|
||||
key: number;
|
||||
title: string;
|
||||
description: string;
|
||||
disconnect: string;
|
||||
};
|
||||
|
||||
const useFeatureBullets = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const featureBullets: featureBulletsType[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 1,
|
||||
title: t('RegisterWorkspace_Features_MobileNotifications_Title'),
|
||||
description: t('RegisterWorkspace_Features_MobileNotifications_Description'),
|
||||
disconnect: t('RegisterWorkspace_Features_MobileNotifications_Disconnect'),
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
title: t('RegisterWorkspace_Features_Marketplace_Title'),
|
||||
description: t('RegisterWorkspace_Features_Marketplace_Description'),
|
||||
disconnect: t('RegisterWorkspace_Features_Marketplace_Disconnect'),
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: t('RegisterWorkspace_Features_Omnichannel_Title'),
|
||||
description: t('RegisterWorkspace_Features_Omnichannel_Description'),
|
||||
disconnect: t('RegisterWorkspace_Features_Omnichannel_Disconnect'),
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return featureBullets;
|
||||
};
|
||||
|
||||
export default useFeatureBullets;
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTitle,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { ExternalLink } from '@rocket.chat/ui-client';
|
||||
import { useSetModal } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RegisterWorkspaceSetupModal from './RegisterWorkspaceSetupModal';
|
||||
import RegisterWorkspaceTokenModal from './RegisterWorkspaceTokenModal';
|
||||
import { links } from '../../../../../lib/links';
|
||||
import useFeatureBullets from '../hooks/useFeatureBullets';
|
||||
|
||||
export type RegisterWorkspaceModalProps = {
|
||||
onClose: () => void;
|
||||
onStatusChange?: () => void;
|
||||
};
|
||||
|
||||
const documentationLink = links.go.registerInfoCollected;
|
||||
|
||||
const RegisterWorkspaceModal = ({ onClose, onStatusChange, ...props }: RegisterWorkspaceModalProps) => {
|
||||
const setModal = useSetModal();
|
||||
const bulletFeatures = useFeatureBullets();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleTokenModal = (): void => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<RegisterWorkspaceTokenModal onClose={handleModalClose} onStatusChange={onStatusChange} />);
|
||||
};
|
||||
|
||||
const handleSetupModal = (): void => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<RegisterWorkspaceSetupModal onClose={handleModalClose} onStatusChange={onStatusChange} />);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTitle>{t('RegisterWorkspace_NotRegistered_Title')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box withRichContent>
|
||||
<span>{`${t('RegisterWorkspace_NotRegistered_Subtitle')}:`}</span>
|
||||
<ul>
|
||||
{bulletFeatures.map((features) => (
|
||||
<li key={features.key}>
|
||||
<strong>{features.title}</strong>
|
||||
<Box is='p' marginBlockStart={4}>
|
||||
{features.description}
|
||||
</Box>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Box is='p' fontSize='p2'>
|
||||
{t('RegisterWorkspace_Registered_Benefits')}
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Box is='div' display='flex' justifyContent='space-between' alignItems='center' width='full'>
|
||||
<ExternalLink to={documentationLink}>{t('Learn_more')}</ExternalLink>
|
||||
<ButtonGroup align='end'>
|
||||
<Button onClick={handleTokenModal}>{t('Use_token')}</Button>
|
||||
<Button primary onClick={handleSetupModal}>
|
||||
{t('RegisterWorkspace_Button')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceModal;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { validateEmail } from '@rocket.chat/tools';
|
||||
import { useSetModal } from '@rocket.chat/ui-contexts';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import RegisteredWorkspaceModal from '../RegisteredWorkspaceModal';
|
||||
import RegisterWorkspaceSetupStepOneModal from './RegisterWorkspaceSetupStepOneModal';
|
||||
import RegisterWorkspaceSetupStepTwoModal from './RegisterWorkspaceSetupStepTwoModal';
|
||||
|
||||
export type RegisterWorkspaceSetupModalProps = {
|
||||
onClose: () => void;
|
||||
onStatusChange?: () => void;
|
||||
};
|
||||
|
||||
const RegisterWorkspaceSetupModal = ({ onClose }: RegisterWorkspaceSetupModalProps) => {
|
||||
const setModal = useSetModal();
|
||||
|
||||
const [step, setStep] = useState(1);
|
||||
const [email, setEmail] = useState('');
|
||||
const [terms, setTerms] = useState(false);
|
||||
const [validInfo, setValidInfo] = useState(false);
|
||||
const [intentData, setIntentData] = useState({
|
||||
device_code: '',
|
||||
interval: 0,
|
||||
user_code: '',
|
||||
});
|
||||
|
||||
// reset validInfo when users go back to step 1
|
||||
useEffect(() => {
|
||||
setValidInfo(false);
|
||||
}, [step]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step === 1) {
|
||||
setValidInfo(validateEmail(email) && terms);
|
||||
}
|
||||
}, [email, terms]);
|
||||
|
||||
const onSuccess = () => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<RegisteredWorkspaceModal onClose={handleModalClose} />);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{step === 1 ? (
|
||||
<RegisterWorkspaceSetupStepOneModal
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
step={step}
|
||||
setStep={setStep}
|
||||
terms={terms}
|
||||
setTerms={setTerms}
|
||||
onClose={onClose}
|
||||
validInfo={validInfo}
|
||||
setIntentData={setIntentData}
|
||||
/>
|
||||
) : (
|
||||
<RegisterWorkspaceSetupStepTwoModal
|
||||
email={email}
|
||||
step={step}
|
||||
setStep={setStep}
|
||||
onClose={onClose}
|
||||
intentData={intentData}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceSetupModal;
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
Modal,
|
||||
Box,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldRow,
|
||||
TextInput,
|
||||
CheckBox,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTagline,
|
||||
ModalTitle,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { ExternalLink } from '@rocket.chat/ui-client';
|
||||
import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useId } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { links } from '../../../../../../lib/links';
|
||||
import WorkspaceRegistrationModal from '../RegisterWorkspaceModal';
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
step: number;
|
||||
setStep: (step: number) => void;
|
||||
terms: boolean;
|
||||
setTerms: (terms: boolean) => void;
|
||||
onClose: () => void;
|
||||
validInfo: boolean;
|
||||
setIntentData: (intentData: any) => void;
|
||||
};
|
||||
|
||||
const RegisterWorkspaceSetupStepOneModal = ({
|
||||
email,
|
||||
setEmail,
|
||||
step,
|
||||
setStep,
|
||||
terms,
|
||||
setTerms,
|
||||
onClose,
|
||||
validInfo,
|
||||
setIntentData,
|
||||
...props
|
||||
}: Props) => {
|
||||
const setModal = useSetModal();
|
||||
const { t } = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const createRegistrationIntent = useEndpoint('POST', '/v1/cloud.createRegistrationIntent');
|
||||
|
||||
const handleBack = (): void => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<WorkspaceRegistrationModal onClose={handleModalClose} />);
|
||||
};
|
||||
|
||||
const handleRegisterWorkspace = async () => {
|
||||
try {
|
||||
const { intentData } = await createRegistrationIntent({ resend: false, email });
|
||||
setIntentData(intentData);
|
||||
setStep(step + 1);
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
};
|
||||
|
||||
const emailField = useId();
|
||||
const termsField = useId();
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTagline>{t('RegisterWorkspace_Setup_Steps', { step, numberOfSteps: 2 })}</ModalTagline>
|
||||
<ModalTitle>{t('RegisterWorkspace_with_email')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box>
|
||||
<Box is='p' fontSize='p2' withRichContent>
|
||||
{t('RegisterWorkspace_Setup_Subtitle')}
|
||||
</Box>
|
||||
<Field paddingBlockStart={10}>
|
||||
<FieldLabel htmlFor={emailField}>{t('RegisterWorkspace_Setup_Label')}</FieldLabel>
|
||||
<FieldRow>
|
||||
<TextInput
|
||||
id={emailField}
|
||||
onChange={(e) => {
|
||||
setEmail((e.target as HTMLInputElement).value);
|
||||
}}
|
||||
/>
|
||||
</FieldRow>
|
||||
</Field>
|
||||
<Box marginBlock={16} fontSize='c1'>
|
||||
<Box is='p'>
|
||||
<strong>{t('RegisterWorkspace_Setup_Have_Account_Title')}</strong>
|
||||
</Box>
|
||||
<Box is='p'>{t('RegisterWorkspace_Setup_Have_Account_Subtitle')}</Box>
|
||||
<Box is='p' paddingBlockStart={16}>
|
||||
<strong>{t('RegisterWorkspace_Setup_No_Account_Title')}</strong>
|
||||
</Box>
|
||||
<Box is='p'>{t('RegisterWorkspace_Setup_No_Account_Subtitle')}</Box>
|
||||
</Box>
|
||||
<Field>
|
||||
<FieldRow justifyContent='initial'>
|
||||
<FieldLabel display='block' fontScale='c1' htmlFor={termsField}>
|
||||
<Trans i18nKey='RegisterWorkspace_Setup_Terms_Privacy'>
|
||||
I agree with <ExternalLink to={links.terms}>Terms and Conditions</ExternalLink> and{' '}
|
||||
<ExternalLink to={links.privacy}>Privacy Policy</ExternalLink>
|
||||
</Trans>
|
||||
</FieldLabel>
|
||||
<CheckBox id={termsField} checked={terms} onChange={() => setTerms(!terms)} />
|
||||
</FieldRow>
|
||||
</Field>
|
||||
</Box>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<ButtonGroup align='end'>
|
||||
<Button onClick={handleBack}>{t('Back')}</Button>
|
||||
<Button primary onClick={handleRegisterWorkspace} disabled={!validInfo}>
|
||||
{t('Next')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceSetupStepOneModal;
|
||||
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Modal,
|
||||
Box,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldRow,
|
||||
TextInput,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTagline,
|
||||
ModalTitle,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
step: number;
|
||||
setStep: (step: number) => void;
|
||||
onClose: () => void;
|
||||
intentData: {
|
||||
device_code: string;
|
||||
interval: number;
|
||||
user_code: string;
|
||||
};
|
||||
onSuccess: () => void;
|
||||
};
|
||||
|
||||
const setIntervalTime = (interval?: number): number => (interval ? interval * 1000 : 0);
|
||||
|
||||
const RegisterWorkspaceSetupStepTwoModal = ({ email, step, setStep, onClose, intentData, onSuccess, ...props }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const cloudConfirmationPoll = useEndpoint('GET', '/v1/cloud.confirmationPoll');
|
||||
const createRegistrationIntent = useEndpoint('POST', '/v1/cloud.createRegistrationIntent');
|
||||
|
||||
const handleBackFromConfirmation = (): void => setStep(step - 1);
|
||||
|
||||
const handleResendRegistrationEmail = async () => {
|
||||
try {
|
||||
await createRegistrationIntent({ resend: true, email });
|
||||
dispatchToastMessage({ type: 'success', message: t('Email_sent') });
|
||||
} catch (error: unknown) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
};
|
||||
|
||||
const getConfirmation = useCallback(async () => {
|
||||
try {
|
||||
const { pollData } = await cloudConfirmationPoll({
|
||||
deviceCode: intentData.device_code,
|
||||
});
|
||||
|
||||
if ('successful' in pollData && pollData.successful) {
|
||||
dispatchToastMessage({ type: 'success', message: t('Workspace_registered') });
|
||||
onSuccess();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
}
|
||||
}, [cloudConfirmationPoll, intentData.device_code, dispatchToastMessage, t, onSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
const pollInterval = setInterval(() => getConfirmation(), setIntervalTime(intentData.interval));
|
||||
|
||||
return (): void => clearInterval(pollInterval);
|
||||
}, [getConfirmation, intentData]);
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTagline>{t('RegisterWorkspace_Setup_Steps', { step, numberOfSteps: 2 })}</ModalTagline>
|
||||
<ModalTitle>{t('Awaiting_confirmation')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box fontSize='p2'>
|
||||
<Box>
|
||||
<Trans i18nKey='cloud.RegisterWorkspace_Setup_Email_Confirmation'>
|
||||
<Box is='p'>
|
||||
Email sent to{' '}
|
||||
<Box is='span' fontScale='p2b'>
|
||||
{email}{' '}
|
||||
</Box>
|
||||
with a confirmation link.
|
||||
</Box>
|
||||
</Trans>
|
||||
</Box>
|
||||
<Box is='p'>{t('RegisterWorkspace_Setup_Email_Verification')}</Box>
|
||||
<Field paddingBlockStart={10}>
|
||||
<FieldLabel>{t('Security_code')}</FieldLabel>
|
||||
<FieldRow>
|
||||
<TextInput defaultValue={intentData.user_code} disabled />
|
||||
</FieldRow>
|
||||
</Field>
|
||||
</Box>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Box is='div' display='flex' justifyContent='start' fontSize='c1' width='full'>
|
||||
<Trans i18nKey='RegisterWorkspace_Didnt_Receive_Email'>
|
||||
Didn't receive email?{' '}
|
||||
<Box is='a' paddingInline={4} onClick={handleResendRegistrationEmail}>
|
||||
Resend
|
||||
</Box>{' '}
|
||||
or{' '}
|
||||
<Box is='a' paddingInline={4} onClick={handleBackFromConfirmation}>
|
||||
change email
|
||||
</Box>
|
||||
</Trans>
|
||||
</Box>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceSetupStepTwoModal;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './RegisterWorkspaceSetupModal';
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldRow,
|
||||
FieldError,
|
||||
Modal,
|
||||
TextInput,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTitle,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import WorkspaceRegistrationModal from './RegisterWorkspaceModal';
|
||||
|
||||
export type RegisterWorkspaceTokenModalProps = {
|
||||
onClose: () => void;
|
||||
onStatusChange?: () => void;
|
||||
};
|
||||
|
||||
const RegisterWorkspaceTokenModal = ({ onClose, onStatusChange, ...props }: RegisterWorkspaceTokenModalProps) => {
|
||||
const setModal = useSetModal();
|
||||
const { t } = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const connectWorkspace = useEndpoint('POST', '/v1/cloud.connectWorkspace');
|
||||
|
||||
const [token, setToken] = useState('');
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const handleBackAction = (): void => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<WorkspaceRegistrationModal onClose={handleModalClose} />);
|
||||
};
|
||||
|
||||
const handleTokenChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setToken(event.target.value);
|
||||
};
|
||||
|
||||
const isToken = token.length > 0;
|
||||
|
||||
const handleConnectButtonClick = async (): Promise<void> => {
|
||||
setProcessing(true);
|
||||
setError(false);
|
||||
|
||||
try {
|
||||
await connectWorkspace({ token });
|
||||
|
||||
setModal(null);
|
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Connected') });
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
setError(true);
|
||||
} finally {
|
||||
await onStatusChange?.();
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTitle>{t('RegisterWorkspace_Token_Title')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box is='p'>
|
||||
<Trans i18nKey='RegisterWorkspace_Token_Step_One'>
|
||||
1. Go to:{' '}
|
||||
<Box is='span' fontWeight={600}>
|
||||
cloud.rocket.chat {'>'} Workspaces
|
||||
</Box>{' '}
|
||||
and click{' '}
|
||||
<Box is='span' fontWeight={600}>
|
||||
"Register self-managed"
|
||||
</Box>
|
||||
.
|
||||
</Trans>
|
||||
</Box>
|
||||
<Box is='p' fontSize='p2'>{`2. ${t('RegisterWorkspace_Token_Step_Two')}`}</Box>
|
||||
<Field paddingBlockStart={10}>
|
||||
<FieldLabel>{t('Registration_Token')}</FieldLabel>
|
||||
<FieldRow>
|
||||
<TextInput onChange={handleTokenChange} value={token} />
|
||||
</FieldRow>
|
||||
{error && <FieldError>{t('Token_Not_Recognized')}</FieldError>}
|
||||
</Field>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<ButtonGroup align='end'>
|
||||
<Button onClick={handleBackAction}>{t('Back')}</Button>
|
||||
<Button primary disabled={!isToken} loading={processing} onClick={handleConnectButtonClick}>
|
||||
{t('Next')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceTokenModal;
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTitle,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useSafely } from '@rocket.chat/fuselage-hooks';
|
||||
import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import useFeatureBullets from '../hooks/useFeatureBullets';
|
||||
|
||||
export type RegisteredWorkspaceModalProps = {
|
||||
onClose: () => void;
|
||||
onStatusChange?: () => void;
|
||||
};
|
||||
|
||||
const RegisteredWorkspaceModal = ({ onClose, onStatusChange, ...props }: RegisteredWorkspaceModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const setModal = useSetModal();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const bulletFeatures = useFeatureBullets();
|
||||
const [isSyncing, setSyncing] = useSafely(useState(false));
|
||||
|
||||
const syncWorkspace = useEndpoint('POST', '/v1/cloud.syncWorkspace');
|
||||
|
||||
const handleSyncAction = async () => {
|
||||
setSyncing(true);
|
||||
|
||||
try {
|
||||
const { success } = await syncWorkspace();
|
||||
|
||||
if (!success) {
|
||||
throw Error(t('RegisterWorkspace_Syncing_Error'));
|
||||
}
|
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('RegisterWorkspace_Syncing_Complete') });
|
||||
setModal(null);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : t('RegisterWorkspace_Syncing_Error');
|
||||
dispatchToastMessage({ type: 'error', message });
|
||||
} finally {
|
||||
onStatusChange?.();
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTitle>{t('Workspace_registered')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box withRichContent>
|
||||
<span>{`${t('RegisterWorkspace_Registered_Subtitle')}: `}</span>
|
||||
<ul>
|
||||
{bulletFeatures.map((item, index) => (
|
||||
<li key={index}>
|
||||
<strong>{item.title}</strong>
|
||||
<Box is='p' marginBlockStart={4}>
|
||||
{item.description}
|
||||
</Box>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Box>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<ButtonGroup align='end'>
|
||||
<Button icon='reload' onClick={handleSyncAction} loading={isSyncing}>
|
||||
{t('Sync')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisteredWorkspaceModal;
|
||||
71
apps/meteor/client/views/admin/workspace/WorkspacePage.tsx
Normal file
71
apps/meteor/client/views/admin/workspace/WorkspacePage.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { IWorkspaceInfo, IStats } from '@rocket.chat/core-typings';
|
||||
import { Box, Button, ButtonGroup, Callout, CardGrid } from '@rocket.chat/fuselage';
|
||||
import type { IInstance } from '@rocket.chat/rest-typings';
|
||||
import { Page, PageHeader, PageScrollableContentWithShadow } from '@rocket.chat/ui-client';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DeploymentCard from './DeploymentCard/DeploymentCard';
|
||||
import MessagesRoomsCard from './MessagesRoomsCard/MessagesRoomsCard';
|
||||
import UsersUploadsCard from './UsersUploadsCard/UsersUploadsCard';
|
||||
import VersionCard from './VersionCard/VersionCard';
|
||||
import { useIsEnterprise } from '../../../hooks/useIsEnterprise';
|
||||
|
||||
type WorkspaceStatusPageProps = {
|
||||
canViewStatistics: boolean;
|
||||
serverInfo: IWorkspaceInfo;
|
||||
statistics: IStats;
|
||||
statisticsIsLoading: boolean;
|
||||
instances: IInstance[];
|
||||
onClickRefreshButton: () => void;
|
||||
onClickDownloadInfo: () => void;
|
||||
};
|
||||
|
||||
const WorkspacePage = ({
|
||||
canViewStatistics,
|
||||
serverInfo,
|
||||
statistics,
|
||||
statisticsIsLoading,
|
||||
instances,
|
||||
onClickRefreshButton,
|
||||
onClickDownloadInfo,
|
||||
}: WorkspaceStatusPageProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = useIsEnterprise();
|
||||
|
||||
const warningMultipleInstances = !data?.isEnterprise && !statistics?.msEnabled && statistics?.instanceCount > 1;
|
||||
|
||||
return (
|
||||
<Page background='tint'>
|
||||
<PageHeader title={t('Workspace')}>
|
||||
{canViewStatistics && (
|
||||
<ButtonGroup>
|
||||
<Button onClick={onClickDownloadInfo}>{t('Download_Info')}</Button>
|
||||
<Button onClick={onClickRefreshButton} loading={statisticsIsLoading}>
|
||||
{t('Refresh')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</PageHeader>
|
||||
|
||||
<PageScrollableContentWithShadow padding={16}>
|
||||
<Box marginBlock='none' marginInline='auto' width='full' color='default'>
|
||||
{warningMultipleInstances && (
|
||||
<Callout type='warning' title={t('Multiple_monolith_instances_alert')} marginBlockEnd={16}></Callout>
|
||||
)}
|
||||
<Box marginBlockEnd={16}>
|
||||
<VersionCard serverInfo={serverInfo} />
|
||||
</Box>
|
||||
<CardGrid breakpoints={{ lg: 4, xs: 4, padding: 8 }}>
|
||||
<DeploymentCard serverInfo={serverInfo} statistics={statistics} instances={instances} />
|
||||
<UsersUploadsCard statistics={statistics} />
|
||||
<MessagesRoomsCard statistics={statistics} />
|
||||
</CardGrid>
|
||||
</Box>
|
||||
</PageScrollableContentWithShadow>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkspacePage);
|
||||
67
apps/meteor/client/views/admin/workspace/WorkspaceRoute.tsx
Normal file
67
apps/meteor/client/views/admin/workspace/WorkspaceRoute.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Callout, ButtonGroup, Button } from '@rocket.chat/fuselage';
|
||||
import { Page, PageHeader, PageScrollableContentWithShadow } from '@rocket.chat/ui-client';
|
||||
import { usePermission } from '@rocket.chat/ui-contexts';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import WorkspacePage from './WorkspacePage';
|
||||
import PageSkeleton from '../../../components/PageSkeleton';
|
||||
import { useWorkspaceInfo } from '../../../hooks/useWorkspaceInfo';
|
||||
import { downloadJsonAs } from '../../../lib/download';
|
||||
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
|
||||
|
||||
const WorkspaceRoute = () => {
|
||||
const { t } = useTranslation();
|
||||
const canViewStatistics = usePermission('view-statistics');
|
||||
|
||||
const [refreshStatistics, setRefreshStatistics] = useState(false);
|
||||
const [serverInfoQuery, instancesQuery, statisticsQuery] = useWorkspaceInfo({ refreshStatistics });
|
||||
|
||||
if (!canViewStatistics) {
|
||||
return <NotAuthorizedPage />;
|
||||
}
|
||||
|
||||
if (serverInfoQuery.isPending || instancesQuery.isPending || statisticsQuery.isPending) {
|
||||
return <PageSkeleton />;
|
||||
}
|
||||
|
||||
const handleClickRefreshButton = async () => {
|
||||
setRefreshStatistics(true);
|
||||
statisticsQuery.refetch();
|
||||
};
|
||||
|
||||
const handleClickDownloadInfo = (): void => {
|
||||
downloadJsonAs(statisticsQuery.data, 'statistics');
|
||||
};
|
||||
|
||||
if (serverInfoQuery.isError || instancesQuery.isError || statisticsQuery.isError) {
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader title={t('Workspace')}>
|
||||
<ButtonGroup>
|
||||
<Button icon='reload' primary type='button' onClick={handleClickRefreshButton} loading={statisticsQuery.isPending}>
|
||||
{t('Refresh')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</PageHeader>
|
||||
<PageScrollableContentWithShadow>
|
||||
<Callout type='danger'>{t('Error_loading_pages')}</Callout>
|
||||
</PageScrollableContentWithShadow>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkspacePage
|
||||
canViewStatistics={canViewStatistics}
|
||||
serverInfo={serverInfoQuery.data}
|
||||
statistics={statisticsQuery.data}
|
||||
statisticsIsLoading={statisticsQuery.isPending}
|
||||
instances={instancesQuery.data}
|
||||
onClickRefreshButton={handleClickRefreshButton}
|
||||
onClickDownloadInfo={handleClickDownloadInfo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkspaceRoute);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type WorkspaceCardSectionProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const WorkspaceCardSection = ({ children }: WorkspaceCardSectionProps) => <Box fontScale='p2'>{children}</Box>;
|
||||
|
||||
export default WorkspaceCardSection;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
|
||||
export type WorkspaceCardSectionTitleProps = {
|
||||
variant?: 'p2b' | 'h4';
|
||||
title: string;
|
||||
};
|
||||
|
||||
const WorkspaceCardSectionTitle = ({ variant = 'p2b', title }: WorkspaceCardSectionTitleProps) => (
|
||||
<Box fontScale={variant} marginBlockEnd={variant === 'h4' ? 20 : 0}>
|
||||
{title}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default WorkspaceCardSectionTitle;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Box, Icon, StatusBullet } from '@rocket.chat/fuselage';
|
||||
import type { Keys } from '@rocket.chat/icons';
|
||||
import { TextSeparator } from '@rocket.chat/ui-client';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
|
||||
export type WorkspaceCardTextSeparatorProps = {
|
||||
label: ReactNode;
|
||||
icon?: Keys;
|
||||
status?: ComponentProps<typeof StatusBullet>['status'];
|
||||
value: ReactNode;
|
||||
};
|
||||
const WorkspaceCardTextSeparator = ({ icon, label, value, status }: WorkspaceCardTextSeparatorProps) => (
|
||||
<TextSeparator
|
||||
label={
|
||||
<>
|
||||
{icon && <Icon name={icon} size='x16' marginInlineEnd={4} />}
|
||||
{status && (
|
||||
<Box minWidth='x16' display='inline-flex' flexDirection='row' alignItems='flex-end' justifyContent='center'>
|
||||
<StatusBullet status={status} />
|
||||
</Box>
|
||||
)}
|
||||
{label && label}
|
||||
</>
|
||||
}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
|
||||
export default WorkspaceCardTextSeparator;
|
||||
Reference in New Issue
Block a user