Skip to content

Commit 3f52d04

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

File tree

11 files changed

+160
-21
lines changed

11 files changed

+160
-21
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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
console.log(data);
46+
47+
return (
48+
<Box h='full' w='full' marginInline='auto' color='default' pbs={24}>
49+
{isLoading && <AccordionLoading />}
50+
{isError && (
51+
<Box maxWidth='x600' alignSelf='center'>
52+
{t('App_not_found')}
53+
</Box>
54+
)}
55+
{isSuccess && data.clusterStatus && data.clusterStatus.length > 0 && (
56+
<CustomScrollbars>
57+
<GenericTable w='full'>
58+
<GenericTableHeader>
59+
<GenericTableHeaderCell key='instanceId'>{t('Workspace_instance')}</GenericTableHeaderCell>
60+
<GenericTableHeaderCell key='status'>{t('Status')}</GenericTableHeaderCell>
61+
<GenericTableHeaderCell key='actions' width={64} />
62+
</GenericTableHeader>
63+
<GenericTableBody>
64+
{data?.clusterStatus?.map((instance) => (
65+
<GenericTableRow key={instance.instanceId}>
66+
<GenericTableCell>{instance.instanceId}</GenericTableCell>
67+
<GenericTableCell>
68+
<Box justifyContent='flex-start' display='flex'>
69+
<Tag medium color={getStatusColor(instance.status)}>
70+
{t(`App_status_${instance.status}`)}
71+
</Tag>
72+
</Box>
73+
</GenericTableCell>
74+
<GenericTableCell>
75+
<Menu options={{ viewLogs: { label: t('Logs'), action: handleSelectLogs, type: 'option' } }}></Menu>
76+
</GenericTableCell>
77+
</GenericTableRow>
78+
))}
79+
</GenericTableBody>
80+
</GenericTable>
81+
</CustomScrollbars>
82+
)}
83+
{isSuccess && (!data.clusterStatus || data.clusterStatus.length === 0) && (
84+
<CustomScrollbars>
85+
<GenericNoResults />
86+
</CustomScrollbars>
87+
)}
88+
</Box>
89+
);
90+
};
91+
92+
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 & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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';
44
import type { TranslationKey } from '@rocket.chat/ui-contexts';
55
import { useRouteParameter, usePermission, useSetModal } from '@rocket.chat/ui-contexts';
@@ -33,7 +33,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro
3333
const setModal = useSetModal();
3434
const isAdminUser = usePermission('manage-apps');
3535
const context = useRouteParameter('context');
36-
36+
console.log(app);
3737
const { price, purchaseType, pricingPlans } = app;
3838

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

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

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')) {
104+
if (status.type === 'danger') {
111105
return 'secondary-danger';
112106
}
113107

114108
return undefined;
115109
};
116110

111+
const getStatusFontColor = (status: appStatusSpanResponseProps) => {
112+
if (status.type === 'warning') {
113+
return Palette.statusColor['status-font-on-warning'].toString();
114+
}
115+
116+
return Palette.text['font-default'].toString();
117+
};
118+
117119
const handleAppRequestsNumber = (status: appStatusSpanResponseProps) => {
118120
if ((status.label === 'request' || status.label === 'requests') && !installed && isAppRequestsPage) {
119121
let numberOfRequests = 0;
@@ -164,7 +166,10 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro
164166
{statuses?.map((status, index) => (
165167
<Margins inlineEnd={index !== statuses.length - 1 ? 8 : undefined} key={index}>
166168
<Tag data-qa-type='app-status-tag' variant={getStatusVariant(status)} title={status.tooltipText ? status.tooltipText : ''}>
167-
{handleAppRequestsNumber(status)} {t(status.label)}
169+
<Box display='flex' color={getStatusFontColor(status)} alignItems='center'>
170+
{status.icon && <Icon name={status.icon} size={16} mie={2} />}
171+
{handleAppRequestsNumber(status)} {t(status.label)}
172+
</Box>
168173
</Tag>
169174
</Margins>
170175
))}

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,21 @@ type appButtonResponseProps = {
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

@@ -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/gazzodown/src/elements/PlainSpan.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const PlainSpan = ({ text }: PlainSpanProps): ReactElement => {
3737
if (markRegex) {
3838
const chunks = text.split(markRegex());
3939
const head = chunks.shift() ?? '';
40+
console.log(markRegex(), chunks, head);
4041

4142
return (
4243
<>

0 commit comments

Comments
 (0)