Skip to content

Commit 9f3d47b

Browse files
feat: new App instances page & mixed app status
1 parent 92848ce commit 9f3d47b

File tree

10 files changed

+160
-26
lines changed

10 files changed

+160
-26
lines changed

apps/meteor/client/apps/orchestrator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class AppClientOrchestrator {
4747
}
4848

4949
public async getInstalledApps(): Promise<App[]> {
50-
const result = await sdk.rest.get<'/apps/installed'>('/apps/installed');
50+
const result = await sdk.rest.get<'/apps/installed'>('/apps/installed', { includeClusterStatus: 'true' });
5151

5252
if ('apps' in result) {
5353
// TODO: chapter day: multiple results are returned, but we only need one

apps/meteor/client/providers/AppsProvider/storeQueryFunction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function storeQueryFunction(
3030
...app,
3131
...(installedApp && {
3232
private: installedApp.private,
33+
clusterStatus: installedApp.clusterStatus,
3334
installed: true,
3435
status: installedApp.status,
3536
version: installedApp.version,

apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import AppDetailsPageTabs from './AppDetailsPageTabs';
1313
import { handleAPIError } from '../helpers/handleAPIError';
1414
import { useAppInfo } from '../hooks/useAppInfo';
1515
import AppDetails from './tabs/AppDetails';
16+
import AppInstances from './tabs/AppInstances';
1617
import AppLogs from './tabs/AppLogs';
1718
import AppReleases from './tabs/AppReleases';
1819
import AppRequests from './tabs/AppRequests/AppRequests';
@@ -117,6 +118,7 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
117118
</FormProvider>
118119
)}
119120
{tab === 'logs' && <AppLogs id={id} />}
121+
{tab === 'instances' && <AppInstances id={id} />}
120122
</>
121123
)}
122124
</Box>

apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPageTabs.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const AppDetailsPageTabs = ({ context, installed, isSecurityVisible, settings, t
1919

2020
const router = useRouter();
2121

22-
const handleTabClick = (tab: 'details' | 'security' | 'releases' | 'settings' | 'logs' | 'requests') => {
22+
const handleTabClick = (tab: 'details' | 'security' | 'releases' | 'settings' | 'logs' | 'requests' | 'instances') => {
2323
router.navigate(
2424
{
2525
name: 'marketplace',
@@ -59,6 +59,11 @@ const AppDetailsPageTabs = ({ context, installed, isSecurityVisible, settings, t
5959
{t('Logs')}
6060
</Tabs.Item>
6161
)}
62+
{Boolean(installed) && isAdminUser && isAdminUser && (
63+
<Tabs.Item onClick={() => handleTabClick('instances')} selected={tab === 'instances'}>
64+
{t('Instances')}
65+
</Tabs.Item>
66+
)}
6267
</Tabs>
6368
);
6469
};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { AppStatus } from '@rocket.chat/apps';
2+
import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus';
3+
import { Box, Menu, Palette, Tag } from '@rocket.chat/fuselage';
4+
import { useRouter } from '@rocket.chat/ui-contexts';
5+
import type { ReactElement } from 'react';
6+
import { useTranslation } from 'react-i18next';
7+
8+
import { CustomScrollbars } from '../../../../../components/CustomScrollbars';
9+
import GenericNoResults from '../../../../../components/GenericNoResults';
10+
import {
11+
GenericTable,
12+
GenericTableBody,
13+
GenericTableCell,
14+
GenericTableHeader,
15+
GenericTableHeaderCell,
16+
GenericTableRow,
17+
} from '../../../../../components/GenericTable';
18+
import AccordionLoading from '../../../components/AccordionLoading';
19+
import { useAppInstances } from '../../../hooks/useAppInstances';
20+
21+
const AppInstances = ({ id }: { id: string }): ReactElement => {
22+
const { t } = useTranslation();
23+
const { data, isSuccess, isError, isLoading } = useAppInstances({ appId: id });
24+
25+
const getStatusColor = (status: AppStatus) => {
26+
if (AppStatusUtils.isDisabled(status) || AppStatusUtils.isError(status)) {
27+
return Palette.text['font-danger'].toString();
28+
}
29+
30+
return Palette.text['font-default'].toString();
31+
};
32+
33+
const router = useRouter();
34+
35+
const handleSelectLogs = () => {
36+
router.navigate(
37+
{
38+
name: 'marketplace',
39+
params: { ...router.getRouteParameters(), tab: 'logs' },
40+
},
41+
{ replace: true },
42+
);
43+
};
44+
45+
return (
46+
<Box h='full' w='full' marginInline='auto' color='default' pbs={24}>
47+
{isLoading && <AccordionLoading />}
48+
{isError && (
49+
<Box maxWidth='x600' alignSelf='center'>
50+
{t('App_not_found')}
51+
</Box>
52+
)}
53+
{isSuccess && data.clusterStatus && data.clusterStatus.length > 0 && (
54+
<CustomScrollbars>
55+
<GenericTable w='full'>
56+
<GenericTableHeader>
57+
<GenericTableHeaderCell key='instanceId'>{t('Workspace_instance')}</GenericTableHeaderCell>
58+
<GenericTableHeaderCell key='status'>{t('Status')}</GenericTableHeaderCell>
59+
<GenericTableHeaderCell key='actions' width={64} />
60+
</GenericTableHeader>
61+
<GenericTableBody>
62+
{data?.clusterStatus?.map((instance) => (
63+
<GenericTableRow key={instance.instanceId}>
64+
<GenericTableCell>{instance.instanceId}</GenericTableCell>
65+
<GenericTableCell>
66+
<Box justifyContent='flex-start' display='flex'>
67+
<Tag medium color={getStatusColor(instance.status)}>
68+
{t(`App_status_${instance.status}`)}
69+
</Tag>
70+
</Box>
71+
</GenericTableCell>
72+
<GenericTableCell>
73+
<Menu options={{ viewLogs: { label: t('Logs'), action: handleSelectLogs, type: 'option' } }}></Menu>
74+
</GenericTableCell>
75+
</GenericTableRow>
76+
))}
77+
</GenericTableBody>
78+
</GenericTable>
79+
</CustomScrollbars>
80+
)}
81+
{isSuccess && (!data.clusterStatus || data.clusterStatus.length === 0) && (
82+
<CustomScrollbars>
83+
<GenericNoResults />
84+
</CustomScrollbars>
85+
)}
86+
</Box>
87+
);
88+
};
89+
90+
export default AppInstances;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './AppInstances';

apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppStatus/AppStatus.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { App } from '@rocket.chat/core-typings';
2-
import { Box, Button, Tag, Margins } from '@rocket.chat/fuselage';
2+
import { Box, Button, Tag, Margins, Icon, Palette } from '@rocket.chat/fuselage';
33
import { useSafely } from '@rocket.chat/fuselage-hooks';
4-
import type { TranslationKey } from '@rocket.chat/ui-contexts';
54
import { useRouteParameter, usePermission, useSetModal } from '@rocket.chat/ui-contexts';
65
import type { ReactElement } from 'react';
76
import { useCallback, useState, memo } from 'react';
@@ -33,7 +32,6 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro
3332
const setModal = useSetModal();
3433
const isAdminUser = usePermission('manage-apps');
3534
const context = useRouteParameter('context');
36-
3735
const { price, purchaseType, pricingPlans } = app;
3836

3937
const button = appButtonProps({ ...app, isAdminUser, endUserRequested });
@@ -96,24 +94,26 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro
9694
appInstallationHandler();
9795
}, [button?.action, appAddon, appInstallationHandler, cancelAction, isAdminUser, setLoading, setModal, workspaceHasAddon]);
9896

99-
// @TODO we should refactor this to not use the label to determine the variant
10097
const getStatusVariant = (status: appStatusSpanResponseProps) => {
101-
if (isAppRequestsPage && totalUnseenRequests && (status.label === 'request' || status.label === 'requests')) {
98+
if (isAppRequestsPage && totalUnseenRequests && status.type === 'primary') {
10299
return 'primary';
103100
}
104101

105-
if (isAppRequestsPage && status.label === 'Requested') {
106-
return undefined;
107-
}
108-
109-
// includes() here because the label can be 'Disabled' or 'Disabled*'
110-
if (status.label.includes('Disabled')) {
102+
if (status.type === 'danger') {
111103
return 'secondary-danger';
112104
}
113105

114106
return undefined;
115107
};
116108

109+
const getStatusFontColor = (status: appStatusSpanResponseProps) => {
110+
if (status.type === 'warning') {
111+
return Palette.statusColor['status-font-on-warning'].toString();
112+
}
113+
114+
return Palette.text['font-default'].toString();
115+
};
116+
117117
const handleAppRequestsNumber = (status: appStatusSpanResponseProps) => {
118118
if ((status.label === 'request' || status.label === 'requests') && !installed && isAppRequestsPage) {
119119
let numberOfRequests = 0;
@@ -152,7 +152,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro
152152
onClick={handleAcquireApp}
153153
mie={8}
154154
>
155-
{t(button.label.replace(' ', '_') as TranslationKey)}
155+
{t(button.label)}
156156
</Button>
157157

158158
{shouldShowPriceDisplay && !installed && (
@@ -164,7 +164,10 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro
164164
{statuses?.map((status, index) => (
165165
<Margins inlineEnd={index !== statuses.length - 1 ? 8 : undefined} key={index}>
166166
<Tag data-qa-type='app-status-tag' variant={getStatusVariant(status)} title={status.tooltipText ? status.tooltipText : ''}>
167-
{handleAppRequestsNumber(status)} {t(status.label)}
167+
<Box display='flex' color={getStatusFontColor(status)} alignItems='center'>
168+
{status.icon && <Icon name={status.icon} size={16} mie={2} />}
169+
{handleAppRequestsNumber(status)} {t(status.label)}
170+
</Box>
168171
</Tag>
169172
</Margins>
170173
))}

apps/meteor/client/views/marketplace/helpers.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,25 @@ export type Actions = 'update' | 'install' | 'purchase' | 'request';
1313
type appButtonResponseProps = {
1414
action: Actions;
1515
icon?: 'reload' | 'warning';
16-
label: 'Update' | 'Install' | 'Subscribe' | 'See Pricing' | 'Try now' | 'Buy' | 'Request' | 'Requested';
16+
label: 'Update' | 'Install' | 'Subscribe' | 'See_Pricing' | 'Try_now' | 'Buy' | 'Request' | 'Requested';
1717
};
1818

1919
export type appStatusSpanResponseProps = {
20-
type?: 'failed' | 'warning';
20+
type?: 'primary' | 'failed' | 'warning' | 'danger';
2121
icon?: 'warning' | 'checkmark-circled' | 'check';
2222
label:
23-
| 'Config Needed'
23+
| 'Config_needed'
2424
| 'Failed'
2525
| 'Disabled'
2626
| 'Disabled*'
27-
| 'Trial period'
27+
| 'Trial_period'
2828
| 'Enabled'
2929
| 'Enabled*'
3030
| 'Incompatible'
3131
| 'request'
3232
| 'requests'
33-
| 'Requested';
33+
| 'Requested'
34+
| 'Mixed_status';
3435
tooltipText?: string;
3536
};
3637

@@ -126,13 +127,13 @@ export const appButtonProps = ({
126127
if (isTierBased) {
127128
return {
128129
action: 'purchase',
129-
label: 'See Pricing',
130+
label: 'See_Pricing',
130131
};
131132
}
132133

133134
return {
134135
action: 'purchase',
135-
label: 'Try now',
136+
label: 'Try_now',
136137
};
137138
}
138139

@@ -173,13 +174,24 @@ export const appIncompatibleStatusProps = (): appStatusSpanResponseProps => ({
173174
});
174175

175176
export const appStatusSpanProps = (
176-
{ installed, status, subscriptionInfo, appRequestStats, migrated }: App,
177+
{ installed, status, subscriptionInfo, appRequestStats, migrated, clusterStatus }: App,
177178
isEnterprise?: boolean,
178179
context?: string,
179180
isAppDetailsPage?: boolean,
180181
): appStatusSpanResponseProps | undefined => {
181182
const isEnabled = status && appEnabledStatuses.includes(status);
182183

184+
const isMixedStatus = clusterStatus && !clusterStatus.every((item) => item.status === clusterStatus?.[0].status);
185+
186+
if (isMixedStatus) {
187+
return {
188+
type: 'warning',
189+
icon: 'warning',
190+
label: 'Mixed_status',
191+
tooltipText: t('Mixed_status_tooltip'),
192+
};
193+
}
194+
183195
if (installed) {
184196
if (isEnabled) {
185197
return migrated && !isEnterprise
@@ -196,9 +208,10 @@ export const appStatusSpanProps = (
196208
? {
197209
label: 'Disabled*',
198210
tooltipText: t('Grandfathered_app'),
211+
type: 'danger',
199212
}
200213
: {
201-
type: 'warning',
214+
type: 'danger',
202215
label: 'Disabled',
203216
};
204217
}
@@ -208,15 +221,15 @@ export const appStatusSpanProps = (
208221
return {
209222
type: 'failed',
210223
icon: 'warning',
211-
label: status === AppStatus.INVALID_SETTINGS_DISABLED ? 'Config Needed' : 'Failed',
224+
label: status === AppStatus.INVALID_SETTINGS_DISABLED ? 'Config_needed' : 'Failed',
212225
};
213226
}
214227

215228
const isOnTrialPeriod = subscriptionInfo && subscriptionInfo.status === 'trialing';
216229
if (isOnTrialPeriod) {
217230
return {
218231
icon: 'checkmark-circled',
219-
label: 'Trial period',
232+
label: 'Trial_period',
220233
};
221234
}
222235

@@ -230,6 +243,7 @@ export const appStatusSpanProps = (
230243
if (appRequestStats.totalUnseen) {
231244
return {
232245
label: appRequestStats.totalUnseen > 1 ? 'requests' : 'request',
246+
type: 'primary',
233247
};
234248
}
235249

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { OperationResult } from '@rocket.chat/rest-typings';
2+
import { useEndpoint } from '@rocket.chat/ui-contexts';
3+
import type { UseQueryResult } from '@tanstack/react-query';
4+
import { useQuery } from '@tanstack/react-query';
5+
6+
export const useAppInstances = ({ appId }: { appId: string }): UseQueryResult<OperationResult<'GET', '/apps/:id/status'>> => {
7+
const status = useEndpoint('GET', '/apps/:id/status', { id: appId });
8+
9+
return useQuery({
10+
queryKey: ['marketplace', 'apps', appId],
11+
queryFn: () => status(),
12+
});
13+
};

packages/i18n/src/locales/en.i18n.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,7 @@
11151115
"Configure_Outgoing_Mail_SMTP": "Configure Outgoing Mail (SMTP)",
11161116
"Configure_video_conference": "Configure conference call",
11171117
"Configure_video_conference_to_make_it_available_on_this_workspace": "Configure video conference to make it available on this workspace",
1118+
"Config_needed": "Config needed",
11181119
"Confirm": "Confirm",
11191120
"Confirm_New_Password_Placeholder": "Please re-enter new password...",
11201121
"Confirm_configuration_update": "Confirm configuration update",
@@ -3381,6 +3382,8 @@
33813382
"Minimum": "Minimum",
33823383
"Minimum_balance": "Minimum balance",
33833384
"Missing_configuration": "Missing configuration",
3385+
"Mixed_status": "Mixed status",
3386+
"Mixed_status_tooltip": "Multiple app statuses across different workspace instances",
33843387
"Mobex_sms_gateway_address": "Mobex SMS Gateway Address",
33853388
"Mobex_sms_gateway_address_desc": "IP or Host of your Mobex service with specified port. E.g. `http://192.168.1.1:1401` or `https://www.example.com:1401`",
33863389
"Mobex_sms_gateway_from_number": "From",
@@ -5134,6 +5137,7 @@
51345137
"Translations": "Translations",
51355138
"Travel_and_Places": "Travel & Places",
51365139
"Trial_active": "Trial active",
5140+
"Trial_period: ": "Trial period",
51375141
"Trigger": "Trigger",
51385142
"Trigger_Words": "Trigger Words",
51395143
"Trigger_removed": "Trigger removed",
@@ -5660,6 +5664,7 @@
56605664
"Workspace": "Workspace",
56615665
"Workspace_and_user_preferences": "Workspace and user preferences",
56625666
"Workspace_exceeded_MAC_limit_disclaimer": "The workspace has exceeded the monthly limit of active contacts. Talk to your workspace admin to address this issue.",
5667+
"Workspace_instance": "Workspace instance",
56635668
"Workspace_not_connected": "Workspace not connected",
56645669
"Workspace_not_registered": "Workspace not registered",
56655670
"Workspace_now_using_device_management": "Workspace now using device management",

0 commit comments

Comments
 (0)