@gntik-ai/templates 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2420 -0
- package/dist/index.js +14520 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -3
- package/src/accept-invitation/Page.test.tsx +40 -0
- package/src/accept-invitation/Page.tsx +153 -0
- package/src/accept-invitation/data.ts +22 -0
- package/src/accept-invitation/template.meta.ts +12 -0
- package/src/assistant-chat/Page.test.tsx +51 -0
- package/src/assistant-chat/Page.tsx +273 -0
- package/src/assistant-chat/data.ts +71 -0
- package/src/assistant-chat/template.meta.ts +12 -0
- package/src/audit-log/Page.test.tsx +58 -0
- package/src/audit-log/Page.tsx +284 -0
- package/src/audit-log/data.ts +133 -0
- package/src/audit-log/template.meta.ts +12 -0
- package/src/board/Lane.tsx +117 -0
- package/src/board/Page.test.tsx +68 -0
- package/src/board/Page.tsx +178 -0
- package/src/board/data.ts +127 -0
- package/src/board/template.meta.ts +12 -0
- package/src/board/useBoard.ts +146 -0
- package/src/changelog/Page.test.tsx +31 -0
- package/src/changelog/Page.tsx +174 -0
- package/src/changelog/data.ts +105 -0
- package/src/changelog/template.meta.ts +12 -0
- package/src/code-editor/Page.test.tsx +83 -0
- package/src/code-editor/Page.tsx +207 -0
- package/src/code-editor/data.ts +130 -0
- package/src/code-editor/template.meta.ts +12 -0
- package/src/contact/Page.test.tsx +33 -0
- package/src/contact/Page.tsx +272 -0
- package/src/contact/PublicFrame.tsx +84 -0
- package/src/contact/data.ts +129 -0
- package/src/contact/template.meta.ts +12 -0
- package/src/create-wizard/Overlay.test.tsx +68 -0
- package/src/create-wizard/Page.test.tsx +57 -0
- package/src/create-wizard/Page.tsx +246 -0
- package/src/create-wizard/data.ts +139 -0
- package/src/create-wizard/examples/Overlay.tsx +60 -0
- package/src/create-wizard/template.meta.ts +12 -0
- package/src/create-workspace/Page.test.tsx +41 -0
- package/src/create-workspace/Page.tsx +227 -0
- package/src/create-workspace/data.ts +61 -0
- package/src/create-workspace/template.meta.ts +12 -0
- package/src/data-explorer/Page.test.tsx +45 -0
- package/src/data-explorer/Page.tsx +247 -0
- package/src/data-explorer/data.ts +94 -0
- package/src/data-explorer/template.meta.ts +12 -0
- package/src/docs-article/ArticleFeedback.tsx +75 -0
- package/src/docs-article/Page.test.tsx +34 -0
- package/src/docs-article/Page.tsx +200 -0
- package/src/docs-article/data.ts +65 -0
- package/src/docs-article/template.meta.ts +12 -0
- package/src/evaluations/Page.test.tsx +40 -0
- package/src/evaluations/Page.tsx +238 -0
- package/src/evaluations/data.ts +194 -0
- package/src/evaluations/template.meta.ts +12 -0
- package/src/forgot-password/Page.test.tsx +199 -0
- package/src/forgot-password/Page.tsx +66 -0
- package/src/forgot-password/data.ts +5 -0
- package/src/forgot-password/examples/UsernameOrEmail.tsx +25 -0
- package/src/forgot-password/template.meta.ts +12 -0
- package/src/getting-started/Page.test.tsx +27 -0
- package/src/getting-started/Page.tsx +125 -0
- package/src/getting-started/data.ts +67 -0
- package/src/getting-started/template.meta.ts +12 -0
- package/src/help-home/Page.test.tsx +33 -0
- package/src/help-home/Page.tsx +159 -0
- package/src/help-home/data.ts +151 -0
- package/src/help-home/template.meta.ts +12 -0
- package/src/home-dashboard/Page.test.tsx +57 -0
- package/src/home-dashboard/Page.tsx +152 -0
- package/src/home-dashboard/data.ts +66 -0
- package/src/home-dashboard/template.meta.ts +12 -0
- package/src/incident-board/Page.test.tsx +77 -0
- package/src/incident-board/Page.tsx +263 -0
- package/src/incident-board/data.ts +220 -0
- package/src/incident-board/template.meta.ts +12 -0
- package/src/incident-console/Page.test.tsx +49 -0
- package/src/incident-console/Page.tsx +274 -0
- package/src/incident-console/data.ts +176 -0
- package/src/incident-console/template.meta.ts +12 -0
- package/src/index.ts +250 -0
- package/src/keyboard-shortcuts/Page.test.tsx +45 -0
- package/src/keyboard-shortcuts/Page.tsx +94 -0
- package/src/keyboard-shortcuts/ShortcutSheet.tsx +80 -0
- package/src/keyboard-shortcuts/data.ts +84 -0
- package/src/keyboard-shortcuts/template.meta.ts +12 -0
- package/src/landing/Page.test.tsx +27 -0
- package/src/landing/Page.tsx +110 -0
- package/src/landing/data.ts +17 -0
- package/src/landing/template.meta.ts +11 -0
- package/src/live-event-stream/Page.test.tsx +251 -0
- package/src/live-event-stream/Page.tsx +307 -0
- package/src/live-event-stream/data.ts +44 -0
- package/src/live-event-stream/examples/Connecting.tsx +5 -0
- package/src/live-event-stream/examples/Error.tsx +5 -0
- package/src/live-event-stream/examples/LiveEmpty.tsx +5 -0
- package/src/live-event-stream/examples/LiveWithEvents.tsx +6 -0
- package/src/live-event-stream/examples/Stopped.tsx +6 -0
- package/src/live-event-stream/template.meta.ts +74 -0
- package/src/messaging/Page.test.tsx +34 -0
- package/src/messaging/Page.tsx +236 -0
- package/src/messaging/data.ts +118 -0
- package/src/messaging/template.meta.ts +12 -0
- package/src/meta.ts +24 -0
- package/src/mfa/Page.test.tsx +22 -0
- package/src/mfa/Page.tsx +45 -0
- package/src/mfa/data.ts +6 -0
- package/src/mfa/template.meta.ts +12 -0
- package/src/model-providers/Page.test.tsx +58 -0
- package/src/model-providers/Page.tsx +198 -0
- package/src/model-providers/data.ts +101 -0
- package/src/model-providers/template.meta.ts +12 -0
- package/src/notification-inbox/Page.test.tsx +49 -0
- package/src/notification-inbox/Page.tsx +237 -0
- package/src/notification-inbox/data.ts +129 -0
- package/src/notification-inbox/template.meta.ts +12 -0
- package/src/pending-activation/Page.test.tsx +390 -0
- package/src/pending-activation/Page.tsx +145 -0
- package/src/pending-activation/data.ts +25 -0
- package/src/pending-activation/template.meta.ts +12 -0
- package/src/pricing/ComparisonTable.tsx +75 -0
- package/src/pricing/Page.test.tsx +29 -0
- package/src/pricing/Page.tsx +83 -0
- package/src/pricing/PublicFrame.tsx +84 -0
- package/src/pricing/data.ts +104 -0
- package/src/pricing/template.meta.ts +12 -0
- package/src/prompt-playground/Page.test.tsx +72 -0
- package/src/prompt-playground/Page.tsx +254 -0
- package/src/prompt-playground/data.ts +68 -0
- package/src/prompt-playground/template.meta.ts +12 -0
- package/src/public-hub/Page.test.tsx +176 -0
- package/src/public-hub/Page.tsx +125 -0
- package/src/public-hub/Source.test.ts +54 -0
- package/src/public-hub/data.ts +24 -0
- package/src/public-hub/examples/Dark.tsx +11 -0
- package/src/public-hub/examples/Full.tsx +6 -0
- package/src/public-hub/examples/HighContrast.tsx +11 -0
- package/src/public-hub/examples/Light.tsx +11 -0
- package/src/public-hub/examples/Loading.tsx +6 -0
- package/src/public-hub/examples/Minimal.tsx +5 -0
- package/src/public-hub/examples/MinimalLoading.tsx +5 -0
- package/src/public-hub/template.meta.ts +60 -0
- package/src/reset-password/Page.test.tsx +27 -0
- package/src/reset-password/Page.tsx +46 -0
- package/src/reset-password/data.ts +5 -0
- package/src/reset-password/template.meta.ts +12 -0
- package/src/resource-detail/Page.test.tsx +260 -0
- package/src/resource-detail/Page.tsx +262 -0
- package/src/resource-detail/data.ts +55 -0
- package/src/resource-detail/examples/postgres-table-detail.tsx +28 -0
- package/src/resource-detail/examples/realtime-configuration.tsx +26 -0
- package/src/resource-detail/examples/runtime-with-transition.tsx +30 -0
- package/src/resource-detail/examples/runtime-without-transition.tsx +33 -0
- package/src/resource-detail/examples/shell.ts +12 -0
- package/src/resource-detail/template.meta.ts +21 -0
- package/src/resource-form/Page.test.tsx +40 -0
- package/src/resource-form/Page.tsx +186 -0
- package/src/resource-form/data.ts +63 -0
- package/src/resource-form/template.meta.ts +12 -0
- package/src/resource-gallery/Page.test.tsx +42 -0
- package/src/resource-gallery/Page.tsx +125 -0
- package/src/resource-gallery/data.ts +44 -0
- package/src/resource-gallery/template.meta.ts +20 -0
- package/src/resource-index/Page.test.tsx +120 -0
- package/src/resource-index/Page.tsx +216 -0
- package/src/resource-index/data.ts +84 -0
- package/src/resource-index/template.meta.ts +21 -0
- package/src/resource-tree/Page.test.tsx +31 -0
- package/src/resource-tree/Page.tsx +232 -0
- package/src/resource-tree/data.ts +180 -0
- package/src/resource-tree/template.meta.ts +12 -0
- package/src/run-trace/Page.test.tsx +56 -0
- package/src/run-trace/Page.tsx +242 -0
- package/src/run-trace/data.ts +171 -0
- package/src/run-trace/template.meta.ts +12 -0
- package/src/scorecard/Page.test.tsx +38 -0
- package/src/scorecard/Page.tsx +160 -0
- package/src/scorecard/TargetsTable.tsx +92 -0
- package/src/scorecard/data.ts +118 -0
- package/src/scorecard/template.meta.ts +12 -0
- package/src/search-results/Page.test.tsx +43 -0
- package/src/search-results/Page.tsx +243 -0
- package/src/search-results/data.ts +130 -0
- package/src/search-results/template.meta.ts +12 -0
- package/src/settings-api-keys/Page.test.tsx +39 -0
- package/src/settings-api-keys/Page.tsx +171 -0
- package/src/settings-api-keys/data.ts +58 -0
- package/src/settings-api-keys/template.meta.ts +12 -0
- package/src/settings-appearance/Page.test.tsx +44 -0
- package/src/settings-appearance/Page.tsx +92 -0
- package/src/settings-appearance/data.ts +21 -0
- package/src/settings-appearance/template.meta.ts +11 -0
- package/src/settings-billing/Page.test.tsx +31 -0
- package/src/settings-billing/Page.tsx +71 -0
- package/src/settings-billing/data.ts +15 -0
- package/src/settings-billing/template.meta.ts +19 -0
- package/src/settings-integrations/Page.test.tsx +31 -0
- package/src/settings-integrations/Page.tsx +185 -0
- package/src/settings-integrations/data.ts +116 -0
- package/src/settings-integrations/template.meta.ts +11 -0
- package/src/settings-members/Page.test.tsx +32 -0
- package/src/settings-members/Page.tsx +140 -0
- package/src/settings-members/data.ts +28 -0
- package/src/settings-members/template.meta.ts +12 -0
- package/src/settings-notifications/Page.test.tsx +31 -0
- package/src/settings-notifications/Page.tsx +71 -0
- package/src/settings-notifications/data.ts +12 -0
- package/src/settings-notifications/template.meta.ts +11 -0
- package/src/settings-profile/Baseline.test.tsx +9 -0
- package/src/settings-profile/Editable.test.tsx +21 -0
- package/src/settings-profile/Page.test.tsx +41 -0
- package/src/settings-profile/Page.tsx +133 -0
- package/src/settings-profile/ReadOnly.test.tsx +143 -0
- package/src/settings-profile/ReadOnlyProfile.tsx +85 -0
- package/src/settings-profile/SettingsFrame.test.tsx +55 -0
- package/src/settings-profile/SettingsFrame.tsx +88 -0
- package/src/settings-profile/__tests__/dom.ts +8 -0
- package/src/settings-profile/data.ts +41 -0
- package/src/settings-profile/examples/ReadOnly.tsx +31 -0
- package/src/settings-profile/fixtures/editable.snap +1 -0
- package/src/settings-profile/settings-nav.ts +98 -0
- package/src/settings-profile/template.meta.ts +21 -0
- package/src/settings-security/MfaSetupDialog.tsx +96 -0
- package/src/settings-security/Page.test.tsx +44 -0
- package/src/settings-security/Page.tsx +158 -0
- package/src/settings-security/data.ts +11 -0
- package/src/settings-security/template.meta.ts +11 -0
- package/src/settings-workspace/Page.test.tsx +44 -0
- package/src/settings-workspace/Page.tsx +120 -0
- package/src/settings-workspace/data.ts +44 -0
- package/src/settings-workspace/template.meta.ts +19 -0
- package/src/shared/ConsoleShell.test.tsx +81 -0
- package/src/shared/ConsoleShell.tsx +98 -0
- package/src/shell-sidebar/Page.test.tsx +21 -0
- package/src/shell-sidebar/Page.tsx +85 -0
- package/src/shell-sidebar/data.ts +24 -0
- package/src/shell-sidebar/template.meta.ts +12 -0
- package/src/shell-topnav/Page.test.tsx +25 -0
- package/src/shell-topnav/Page.tsx +94 -0
- package/src/shell-topnav/data.ts +25 -0
- package/src/shell-topnav/template.meta.ts +12 -0
- package/src/sign-in/BrandPanel.tsx +34 -0
- package/src/sign-in/Page.test.tsx +41 -0
- package/src/sign-in/Page.tsx +73 -0
- package/src/sign-in/data.ts +11 -0
- package/src/sign-in/template.meta.ts +12 -0
- package/src/sign-in-card/Page.test.tsx +182 -0
- package/src/sign-in-card/Page.tsx +82 -0
- package/src/sign-in-card/data.ts +8 -0
- package/src/sign-in-card/examples/Username.tsx +28 -0
- package/src/sign-in-card/template.meta.ts +12 -0
- package/src/sign-up/Page.test.tsx +33 -0
- package/src/sign-up/Page.tsx +70 -0
- package/src/sign-up/data.ts +15 -0
- package/src/sign-up/template.meta.ts +12 -0
- package/src/sso-login/Page.test.tsx +41 -0
- package/src/sso-login/Page.tsx +151 -0
- package/src/sso-login/data.ts +10 -0
- package/src/sso-login/template.meta.ts +12 -0
- package/src/status-403/Page.test.tsx +23 -0
- package/src/status-403/Page.tsx +92 -0
- package/src/status-403/data.ts +13 -0
- package/src/status-403/template.meta.ts +12 -0
- package/src/status-404/Page.test.tsx +23 -0
- package/src/status-404/Page.tsx +60 -0
- package/src/status-404/PageHeading.test.tsx +52 -0
- package/src/status-404/data.ts +12 -0
- package/src/status-404/examples/PageHeader.tsx +28 -0
- package/src/status-404/template.meta.ts +12 -0
- package/src/status-500/Page.test.tsx +24 -0
- package/src/status-500/Page.tsx +70 -0
- package/src/status-500/data.ts +20 -0
- package/src/status-500/template.meta.ts +12 -0
- package/src/status-maintenance/Page.test.tsx +24 -0
- package/src/status-maintenance/Page.tsx +84 -0
- package/src/status-maintenance/data.ts +14 -0
- package/src/status-maintenance/template.meta.ts +12 -0
- package/src/styles.css +11 -0
- package/src/test/a11y.ts +18 -0
- package/src/test/setup.ts +36 -0
- package/src/upgrade-required/Page.test.tsx +28 -0
- package/src/upgrade-required/Page.tsx +79 -0
- package/src/upgrade-required/data.ts +19 -0
- package/src/upgrade-required/template.meta.ts +12 -0
- package/src/usage-analytics/Page.test.tsx +83 -0
- package/src/usage-analytics/Page.tsx +184 -0
- package/src/usage-analytics/data.ts +161 -0
- package/src/usage-analytics/template.meta.ts +21 -0
- package/src/verify-email/Page.test.tsx +32 -0
- package/src/verify-email/Page.tsx +118 -0
- package/src/verify-email/data.ts +7 -0
- package/src/verify-email/template.meta.ts +12 -0
- package/src/workflow-builder/Page.test.tsx +97 -0
- package/src/workflow-builder/Page.tsx +163 -0
- package/src/workflow-builder/data.ts +13 -0
- package/src/workflow-builder/template.meta.ts +12 -0
- package/README.md +0 -3
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BulkActionBar,
|
|
3
|
+
DataTable,
|
|
4
|
+
FilterBar,
|
|
5
|
+
NoResultsEmpty,
|
|
6
|
+
PageHeader,
|
|
7
|
+
type ActiveFilter,
|
|
8
|
+
type DataTableColumn,
|
|
9
|
+
type FilterField,
|
|
10
|
+
type SavedView,
|
|
11
|
+
} from '@gntik-ai/blocks';
|
|
12
|
+
import { Archive, Download, Plus, Trash2 } from '@gntik-ai/icons';
|
|
13
|
+
import { Link, Page, Stack, type BreadcrumbItem } from '@gntik-ai/ui';
|
|
14
|
+
import { useMemo, useState } from 'react';
|
|
15
|
+
import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
|
|
16
|
+
import {
|
|
17
|
+
filterResources,
|
|
18
|
+
resourceBreadcrumbs,
|
|
19
|
+
resourceColumns,
|
|
20
|
+
resourceFilterFields,
|
|
21
|
+
resourceRows,
|
|
22
|
+
resourceSavedViews,
|
|
23
|
+
type ResourceRow,
|
|
24
|
+
} from './data';
|
|
25
|
+
|
|
26
|
+
export type ResourceBulkAction = 'export' | 'archive' | 'delete';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `T` is the row type (default: the deployment fixture rows). Columns decide what is shown,
|
|
30
|
+
* sorted and filtered: a filter field reads the column with the same id.
|
|
31
|
+
*/
|
|
32
|
+
export interface ResourceIndexProps<T extends object = ResourceRow> {
|
|
33
|
+
title: string;
|
|
34
|
+
description: string;
|
|
35
|
+
breadcrumbs: BreadcrumbItem[];
|
|
36
|
+
/** Initial rows; the page keeps its own copy so the demo bulk actions take effect. */
|
|
37
|
+
rows: readonly T[];
|
|
38
|
+
columns: ReadonlyArray<DataTableColumn<T>>;
|
|
39
|
+
/** FilterBar fields; each `id` matches a column id (or a row property). */
|
|
40
|
+
filterFields: readonly FilterField[];
|
|
41
|
+
savedViews: readonly SavedView[];
|
|
42
|
+
/** Stable row id (defaults to `row.id`). */
|
|
43
|
+
getRowId: (row: T) => string;
|
|
44
|
+
/** Detail page of a row: the name cell becomes a kit Link (routed through LinkProvider). */
|
|
45
|
+
getRowHref: (row: T) => string;
|
|
46
|
+
/** Called when the name cell is activated (with `getRowHref`, on top of the navigation). */
|
|
47
|
+
onRowClick: (row: T) => void;
|
|
48
|
+
/** Column whose cell links to the row (defaults to the first column). */
|
|
49
|
+
linkColumnId: string;
|
|
50
|
+
/** Noun for counts and empty states, singular and plural. */
|
|
51
|
+
noun: [singular: string, plural: string];
|
|
52
|
+
createLabel: string;
|
|
53
|
+
onCreate: () => void;
|
|
54
|
+
onOpen: (row: T) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Called for every bulk action. Without it, Archive pauses and Delete removes the rows locally
|
|
57
|
+
* (the fixture behaviour); with it the product decides and passes fresh `rows`.
|
|
58
|
+
*/
|
|
59
|
+
onBulkAction: (action: ResourceBulkAction, ids: string[]) => void;
|
|
60
|
+
/** Local Archive without `onBulkAction`; defaults to `status: 'paused'` on rows that have a status. */
|
|
61
|
+
archiveRow: (row: T) => T;
|
|
62
|
+
loading: boolean;
|
|
63
|
+
shell: Omit<ConsoleShellProps, 'children'>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const defaultRowId = (row: object): string => {
|
|
67
|
+
const id = (row as { id?: unknown }).id;
|
|
68
|
+
return typeof id === 'string' || typeof id === 'number' ? String(id) : '';
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const defaultArchive = <T extends object>(row: T): T => ('status' in row ? { ...row, status: 'paused' } : row);
|
|
72
|
+
|
|
73
|
+
/** Resource index: header, filter bar, selectable sortable table with pagination, bulk action bar and a no-results state. */
|
|
74
|
+
export default function ResourceIndexPage<T extends object = ResourceRow>({
|
|
75
|
+
title = 'Deployments',
|
|
76
|
+
description = 'Every service deployed in this workspace, across regions.',
|
|
77
|
+
breadcrumbs = resourceBreadcrumbs,
|
|
78
|
+
rows: initialRows = resourceRows as unknown as readonly T[],
|
|
79
|
+
columns = resourceColumns as unknown as ReadonlyArray<DataTableColumn<T>>,
|
|
80
|
+
filterFields = resourceFilterFields,
|
|
81
|
+
savedViews = resourceSavedViews,
|
|
82
|
+
getRowId = defaultRowId,
|
|
83
|
+
getRowHref,
|
|
84
|
+
onRowClick,
|
|
85
|
+
linkColumnId,
|
|
86
|
+
noun = ['deployment', 'deployments'],
|
|
87
|
+
createLabel = 'New deployment',
|
|
88
|
+
onCreate,
|
|
89
|
+
onOpen,
|
|
90
|
+
onBulkAction,
|
|
91
|
+
archiveRow = defaultArchive,
|
|
92
|
+
loading = false,
|
|
93
|
+
shell,
|
|
94
|
+
}: Partial<ResourceIndexProps<T>>) {
|
|
95
|
+
const [rows, setRows] = useState<readonly T[]>(initialRows);
|
|
96
|
+
const [query, setQuery] = useState('');
|
|
97
|
+
const [filters, setFilters] = useState<ActiveFilter[]>([]);
|
|
98
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
99
|
+
const visible = filterResources(rows, query, filters, columns);
|
|
100
|
+
const visibleSelected = selected.filter((id) => visible.some((r) => getRowId(r) === id));
|
|
101
|
+
const tableColumns = useMemo(
|
|
102
|
+
() => linkColumns(columns, linkColumnId ?? columns[0]?.id, getRowHref, onRowClick),
|
|
103
|
+
[columns, linkColumnId, getRowHref, onRowClick],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const runBulk = (action: ResourceBulkAction, ids: string[] = visibleSelected) => {
|
|
107
|
+
if (onBulkAction) onBulkAction(action, ids);
|
|
108
|
+
else if (action === 'delete') setRows((prev) => prev.filter((r) => !ids.includes(getRowId(r))));
|
|
109
|
+
else if (action === 'archive') setRows((prev) => prev.map((r) => (ids.includes(getRowId(r)) ? archiveRow(r) : r)));
|
|
110
|
+
if (action !== 'export') setSelected((prev) => prev.filter((id) => !ids.includes(id)));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<ConsoleShell currentHref="/deployments" breadcrumbs={breadcrumbs} {...shell}>
|
|
115
|
+
<Page
|
|
116
|
+
width="wide"
|
|
117
|
+
header={
|
|
118
|
+
<PageHeader
|
|
119
|
+
breadcrumbs={null}
|
|
120
|
+
title={title}
|
|
121
|
+
description={description}
|
|
122
|
+
status=""
|
|
123
|
+
meta={[{ label: 'Total', value: `${rows.length} ${rows.length === 1 ? noun[0] : noun[1]}` }]}
|
|
124
|
+
tabs={null}
|
|
125
|
+
actions={[{ label: createLabel, icon: Plus, variant: 'primary', onClick: onCreate }]}
|
|
126
|
+
/>
|
|
127
|
+
}
|
|
128
|
+
actionBarLabel="Bulk actions"
|
|
129
|
+
actionBar={
|
|
130
|
+
visibleSelected.length > 0 ? (
|
|
131
|
+
<BulkActionBar
|
|
132
|
+
selectedCount={visibleSelected.length}
|
|
133
|
+
totalCount={visible.length}
|
|
134
|
+
noun={noun}
|
|
135
|
+
onClear={() => setSelected([])}
|
|
136
|
+
actions={[
|
|
137
|
+
{ label: 'Export', icon: Download, onClick: () => runBulk('export') },
|
|
138
|
+
{ label: 'Archive', icon: Archive, onClick: () => runBulk('archive') },
|
|
139
|
+
{ label: 'Delete', icon: Trash2, variant: 'destructive', onClick: () => runBulk('delete') },
|
|
140
|
+
]}
|
|
141
|
+
/>
|
|
142
|
+
) : undefined
|
|
143
|
+
}
|
|
144
|
+
>
|
|
145
|
+
<Stack gap={4}>
|
|
146
|
+
<FilterBar
|
|
147
|
+
fields={filterFields}
|
|
148
|
+
query={query}
|
|
149
|
+
onQueryChange={setQuery}
|
|
150
|
+
filters={filters}
|
|
151
|
+
onFiltersChange={setFilters}
|
|
152
|
+
defaultFilters={[]}
|
|
153
|
+
views={savedViews}
|
|
154
|
+
placeholder={`Search ${noun[1]} by name or id…`}
|
|
155
|
+
resultCount={visible.length}
|
|
156
|
+
/>
|
|
157
|
+
<DataTable<T>
|
|
158
|
+
rows={visible}
|
|
159
|
+
columns={tableColumns}
|
|
160
|
+
getRowId={getRowId}
|
|
161
|
+
caption={title}
|
|
162
|
+
loading={loading}
|
|
163
|
+
selectedIds={visibleSelected}
|
|
164
|
+
onSelectionChange={setSelected}
|
|
165
|
+
// The BulkActionBar above already announces the count.
|
|
166
|
+
selectionSummary={false}
|
|
167
|
+
rowActions={(row) => [
|
|
168
|
+
{ label: 'Open', onSelect: () => onOpen?.(row) },
|
|
169
|
+
'separator',
|
|
170
|
+
{ label: 'Delete', icon: Trash2, destructive: true, onSelect: () => runBulk('delete', [getRowId(row)]) },
|
|
171
|
+
]}
|
|
172
|
+
emptyState={
|
|
173
|
+
<NoResultsEmpty
|
|
174
|
+
entity={noun[1]}
|
|
175
|
+
query={query}
|
|
176
|
+
filterCount={filters.length}
|
|
177
|
+
onClearFilters={() => setFilters([])}
|
|
178
|
+
onClearSearch={() => setQuery('')}
|
|
179
|
+
titleAs="h2"
|
|
180
|
+
/>
|
|
181
|
+
}
|
|
182
|
+
/>
|
|
183
|
+
</Stack>
|
|
184
|
+
</Page>
|
|
185
|
+
</ConsoleShell>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Wraps the link column's cell in a kit Link (href) or a link-styled button (click only). */
|
|
190
|
+
function linkColumns<T>(
|
|
191
|
+
columns: ReadonlyArray<DataTableColumn<T>>,
|
|
192
|
+
columnId: string | undefined,
|
|
193
|
+
getRowHref: ((row: T) => string) | undefined,
|
|
194
|
+
onRowClick: ((row: T) => void) | undefined,
|
|
195
|
+
): ReadonlyArray<DataTableColumn<T>> {
|
|
196
|
+
if (!getRowHref && !onRowClick) return columns;
|
|
197
|
+
return columns.map((col) => {
|
|
198
|
+
if (col.id !== columnId) return col;
|
|
199
|
+
const content = (row: T) => (col.cell ? col.cell(row) : String(col.accessor?.(row) ?? ''));
|
|
200
|
+
return {
|
|
201
|
+
...col,
|
|
202
|
+
cell: (row: T) => {
|
|
203
|
+
const onClick = onRowClick ? () => onRowClick(row) : undefined;
|
|
204
|
+
return getRowHref ? (
|
|
205
|
+
<Link href={getRowHref(row)} tone="inherit" underline="hover" onClick={onClick}>
|
|
206
|
+
{content(row)}
|
|
207
|
+
</Link>
|
|
208
|
+
) : (
|
|
209
|
+
<Link render={<button type="button" />} tone="inherit" underline="hover" onClick={onClick}>
|
|
210
|
+
{content(row)}
|
|
211
|
+
</Link>
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEPLOYMENT_COLUMNS,
|
|
3
|
+
DEPLOYMENT_ROWS,
|
|
4
|
+
type ActiveFilter,
|
|
5
|
+
type DeploymentRow,
|
|
6
|
+
type FilterField,
|
|
7
|
+
type SavedView,
|
|
8
|
+
} from '@gntik-ai/blocks';
|
|
9
|
+
import type { BreadcrumbItem } from '@gntik-ai/ui';
|
|
10
|
+
|
|
11
|
+
export type ResourceRow = DeploymentRow;
|
|
12
|
+
|
|
13
|
+
export const resourceRows: ResourceRow[] = DEPLOYMENT_ROWS;
|
|
14
|
+
export const resourceColumns = DEPLOYMENT_COLUMNS;
|
|
15
|
+
|
|
16
|
+
export const resourceBreadcrumbs: BreadcrumbItem[] = [
|
|
17
|
+
{ label: 'Overview', href: '/overview' },
|
|
18
|
+
{ label: 'Deployments' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const resourceFilterFields: FilterField[] = [
|
|
22
|
+
{ id: 'status', label: 'Status', options: ['Running', 'Queued', 'Paused', 'Degraded', 'Failed'] },
|
|
23
|
+
{ id: 'region', label: 'Region', options: ['eu-west-1', 'us-east-1', 'ap-south-1', 'eu-central-1'] },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const resourceSavedViews: SavedView[] = [
|
|
27
|
+
{ value: 'all', label: 'All deployments', filters: [] },
|
|
28
|
+
{
|
|
29
|
+
value: 'attention',
|
|
30
|
+
label: 'Needs attention',
|
|
31
|
+
filters: [
|
|
32
|
+
{ field: 'status', value: 'Degraded' },
|
|
33
|
+
{ field: 'status', value: 'Failed' },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
value: 'eu',
|
|
38
|
+
label: 'EU regions',
|
|
39
|
+
filters: [
|
|
40
|
+
{ field: 'region', value: 'eu-west-1' },
|
|
41
|
+
{ field: 'region', value: 'eu-central-1' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
type Column<T> = { id: string; accessor?: (row: T) => unknown };
|
|
47
|
+
|
|
48
|
+
const fieldValue = <T extends object>(row: T, field: string, columns?: ReadonlyArray<Column<T>>): unknown => {
|
|
49
|
+
const accessor = columns?.find((c) => c.id === field)?.accessor;
|
|
50
|
+
return accessor ? accessor(row) : (row as Record<string, unknown>)[field];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const text = (v: unknown) => (typeof v === 'string' || typeof v === 'number' ? String(v).toLowerCase() : undefined);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Search by name or id (or, without them, every column value); filters on one field are OR, across fields AND, matched case-insensitively.
|
|
57
|
+
* A filter field reads the column with the same id (its `accessor`), else the row property of that
|
|
58
|
+
* name, so statuses come from the columns.
|
|
59
|
+
*/
|
|
60
|
+
export function filterResources<T extends object = ResourceRow>(
|
|
61
|
+
rows: readonly T[],
|
|
62
|
+
query: string,
|
|
63
|
+
filters: readonly ActiveFilter[],
|
|
64
|
+
columns?: ReadonlyArray<Column<T>>,
|
|
65
|
+
): T[] {
|
|
66
|
+
const q = query.trim().toLowerCase();
|
|
67
|
+
const byField = new Map<string, string[]>();
|
|
68
|
+
for (const f of filters) byField.set(f.field, [...(byField.get(f.field) ?? []), f.value.toLowerCase()]);
|
|
69
|
+
return rows.filter((row) => {
|
|
70
|
+
if (q) {
|
|
71
|
+
const r = row as Record<string, unknown>;
|
|
72
|
+
let haystack = [r.name, r.id].map(text).filter((v) => v !== undefined);
|
|
73
|
+
// Rows without a name/id: search every column value instead.
|
|
74
|
+
if (haystack.length === 0 && columns)
|
|
75
|
+
haystack = columns.map((c) => text(c.accessor?.(row))).filter((v) => v !== undefined);
|
|
76
|
+
if (!haystack.some((v) => v.includes(q))) return false;
|
|
77
|
+
}
|
|
78
|
+
for (const [field, values] of byField) {
|
|
79
|
+
const value = text(fieldValue(row, field, columns));
|
|
80
|
+
if (value !== undefined && !values.includes(value)) return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TemplateMeta } from '../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: TemplateMeta = {
|
|
4
|
+
name: 'Resource index',
|
|
5
|
+
family: 'Resources',
|
|
6
|
+
priority: 'P1',
|
|
7
|
+
status: 'beta',
|
|
8
|
+
description:
|
|
9
|
+
'List page for any resource: header with the create action, filter bar with saved views, a selectable sortable table with pagination, a sticky bulk action bar and a no-results state.',
|
|
10
|
+
layout: 'SidebarLayout + Page',
|
|
11
|
+
blocks: [
|
|
12
|
+
'app-sidebar',
|
|
13
|
+
'app-topbar',
|
|
14
|
+
'page-header',
|
|
15
|
+
'filter-bar',
|
|
16
|
+
'data-table',
|
|
17
|
+
'pagination-footer',
|
|
18
|
+
'bulk-action-bar',
|
|
19
|
+
'empty-states',
|
|
20
|
+
],
|
|
21
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { render, screen, within } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../test/a11y';
|
|
4
|
+
import ResourceTreePage from './Page';
|
|
5
|
+
import { groupRows, treeResources } from './data';
|
|
6
|
+
|
|
7
|
+
describe('ResourceTreePage', () => {
|
|
8
|
+
it('renders the tree and one table per group', { timeout: 15000 }, async () => {
|
|
9
|
+
render(<ResourceTreePage />);
|
|
10
|
+
expect(screen.getByRole('main')).toBeInTheDocument();
|
|
11
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Resources' })).toBeInTheDocument();
|
|
12
|
+
expect(screen.getByRole('tree', { name: 'Teams and projects' })).toBeInTheDocument();
|
|
13
|
+
const groups = groupRows(treeResources, 'kind');
|
|
14
|
+
for (const g of groups) expect(screen.getByRole('table', { name: `${g.label} resources` })).toBeInTheDocument();
|
|
15
|
+
expect(screen.getAllByRole('button', { name: /\+2 more/ }).length).toBeGreaterThan(0);
|
|
16
|
+
await expectNoAxeViolations();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('filters by a tree node and regroups', { timeout: 15000 }, async () => {
|
|
20
|
+
render(<ResourceTreePage />);
|
|
21
|
+
await userEvent.click(within(screen.getByRole('tree')).getByText('compliance'));
|
|
22
|
+
expect(screen.getByText('compliance · 2 resources')).toBeInTheDocument();
|
|
23
|
+
expect(screen.queryByRole('table', { name: 'Services resources' })).not.toBeInTheDocument();
|
|
24
|
+
await userEvent.click(screen.getByRole('combobox', { name: 'Group by' }));
|
|
25
|
+
await userEvent.click(await screen.findByRole('option', { name: 'Region' }));
|
|
26
|
+
const table = screen.getByRole('table', { name: 'ap-south-1 resources' });
|
|
27
|
+
expect(within(table).getAllByRole('row')).toHaveLength(3);
|
|
28
|
+
await userEvent.click(screen.getByRole('button', { name: 'All resources' }));
|
|
29
|
+
expect(screen.getByText(`All resources · ${treeResources.length} resources`)).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { DataTable, PageHeader, SectionHeader, type DataTableColumn } from '@gntik-ai/blocks';
|
|
2
|
+
import { Plus } from '@gntik-ai/icons';
|
|
3
|
+
import {
|
|
4
|
+
Badge,
|
|
5
|
+
Button,
|
|
6
|
+
Card,
|
|
7
|
+
CardBody,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
EmptyState,
|
|
11
|
+
HStack,
|
|
12
|
+
OverflowList,
|
|
13
|
+
Page,
|
|
14
|
+
SimpleSelect,
|
|
15
|
+
Stack,
|
|
16
|
+
StatusTag,
|
|
17
|
+
Text,
|
|
18
|
+
Timestamp,
|
|
19
|
+
TreeList,
|
|
20
|
+
type BreadcrumbItem,
|
|
21
|
+
type StatusDefinition,
|
|
22
|
+
} from '@gntik-ai/ui';
|
|
23
|
+
import { useId, useState } from 'react';
|
|
24
|
+
import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
|
|
25
|
+
import {
|
|
26
|
+
buildResourceTree,
|
|
27
|
+
groupByLabels,
|
|
28
|
+
groupRows,
|
|
29
|
+
kindIcons,
|
|
30
|
+
resourceTreeBreadcrumbs,
|
|
31
|
+
rowsUnder,
|
|
32
|
+
treeResources,
|
|
33
|
+
type ResourceGroupBy,
|
|
34
|
+
type TreeResource,
|
|
35
|
+
} from './data';
|
|
36
|
+
|
|
37
|
+
export interface ResourceTreeProps {
|
|
38
|
+
resources: TreeResource[];
|
|
39
|
+
defaultGroupBy: ResourceGroupBy;
|
|
40
|
+
/** Initially selected tree node (`team:<name>` or `project:<name>`); none = every resource. */
|
|
41
|
+
defaultNodeId: string | null;
|
|
42
|
+
onOpen: (resource: TreeResource) => void;
|
|
43
|
+
onCreate: () => void;
|
|
44
|
+
breadcrumbs: BreadcrumbItem[];
|
|
45
|
+
currentHref: string;
|
|
46
|
+
shell: Omit<ConsoleShellProps, 'children'>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const STATUSES: Record<string, StatusDefinition> = {
|
|
50
|
+
healthy: { label: 'Healthy', tone: 'success' },
|
|
51
|
+
degraded: { label: 'Degraded', tone: 'warning' },
|
|
52
|
+
stopped: { label: 'Stopped', tone: 'neutral' },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const GROUP_ITEMS = (Object.keys(groupByLabels) as ResourceGroupBy[]).map((value) => ({
|
|
56
|
+
value,
|
|
57
|
+
label: groupByLabels[value],
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
function columns(onOpen?: (r: TreeResource) => void): DataTableColumn<TreeResource>[] {
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
id: 'name',
|
|
64
|
+
header: 'Name',
|
|
65
|
+
accessor: (r) => r.name,
|
|
66
|
+
sortable: true,
|
|
67
|
+
cell: (r) => {
|
|
68
|
+
const Icon = kindIcons[r.kind];
|
|
69
|
+
return (
|
|
70
|
+
<span className="inline-flex items-center gap-2">
|
|
71
|
+
<Icon size={14} aria-hidden className="shrink-0 text-muted-foreground" />
|
|
72
|
+
{onOpen ? (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={() => onOpen(r)}
|
|
76
|
+
className="font-medium text-foreground underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring"
|
|
77
|
+
>
|
|
78
|
+
{r.name}
|
|
79
|
+
</button>
|
|
80
|
+
) : (
|
|
81
|
+
<span className="font-medium text-foreground">{r.name}</span>
|
|
82
|
+
)}
|
|
83
|
+
</span>
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{ id: 'project', header: 'Project', accessor: (r) => r.project, sortable: true, variant: 'muted' },
|
|
88
|
+
{ id: 'region', header: 'Region', accessor: (r) => r.region, variant: 'mono' },
|
|
89
|
+
{
|
|
90
|
+
id: 'status',
|
|
91
|
+
header: 'Status',
|
|
92
|
+
accessor: (r) => r.status,
|
|
93
|
+
cell: (r) => <StatusTag status={r.status} statuses={STATUSES} />,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'tags',
|
|
97
|
+
header: 'Tags',
|
|
98
|
+
cell: (r) => (
|
|
99
|
+
<OverflowList
|
|
100
|
+
label={`${r.name} tags`}
|
|
101
|
+
items={r.tags}
|
|
102
|
+
max={2}
|
|
103
|
+
renderItem={(t) => (
|
|
104
|
+
<Badge variant="outline" size="sm">
|
|
105
|
+
{t}
|
|
106
|
+
</Badge>
|
|
107
|
+
)}
|
|
108
|
+
renderOverflowItem={(t) => t}
|
|
109
|
+
/>
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'updated',
|
|
114
|
+
header: 'Updated',
|
|
115
|
+
accessor: (r) => r.updatedAt,
|
|
116
|
+
sortable: true,
|
|
117
|
+
cell: (r) => <Timestamp value={r.updatedAt} className="text-muted-foreground" />,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Resource index browsed by a team → project TreeList, with grouped DataTables and tag overflow. */
|
|
123
|
+
export default function ResourceTreePage(props: Partial<ResourceTreeProps>) {
|
|
124
|
+
const {
|
|
125
|
+
resources = treeResources,
|
|
126
|
+
defaultGroupBy = 'kind',
|
|
127
|
+
defaultNodeId = null,
|
|
128
|
+
onOpen,
|
|
129
|
+
onCreate,
|
|
130
|
+
breadcrumbs = resourceTreeBreadcrumbs,
|
|
131
|
+
currentHref = '/resources',
|
|
132
|
+
shell,
|
|
133
|
+
} = props;
|
|
134
|
+
const [nodeId, setNodeId] = useState<string | null>(defaultNodeId);
|
|
135
|
+
const [groupBy, setGroupBy] = useState<ResourceGroupBy>(defaultGroupBy);
|
|
136
|
+
const ids = useId();
|
|
137
|
+
const tree = buildResourceTree(resources);
|
|
138
|
+
const rows = rowsUnder(resources, nodeId);
|
|
139
|
+
const groups = groupRows(rows, groupBy);
|
|
140
|
+
const scope = nodeId ? nodeId.split(':')[1] : 'All resources';
|
|
141
|
+
const cols = columns(onOpen);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
|
|
145
|
+
<Page
|
|
146
|
+
width="wide"
|
|
147
|
+
header={
|
|
148
|
+
<PageHeader
|
|
149
|
+
breadcrumbs={null}
|
|
150
|
+
title="Resources"
|
|
151
|
+
description="Services, databases, buckets and queues by team and project."
|
|
152
|
+
status=""
|
|
153
|
+
meta={[]}
|
|
154
|
+
tabs={null}
|
|
155
|
+
actions={[{ label: 'New resource', icon: Plus, onClick: onCreate }]}
|
|
156
|
+
/>
|
|
157
|
+
}
|
|
158
|
+
>
|
|
159
|
+
<div className="grid gap-6 lg:grid-cols-[17rem_minmax(0,1fr)]">
|
|
160
|
+
<Card className="self-start">
|
|
161
|
+
<CardHeader divided>
|
|
162
|
+
<CardTitle as="h2">Teams</CardTitle>
|
|
163
|
+
</CardHeader>
|
|
164
|
+
<CardBody className="p-2">
|
|
165
|
+
<Button
|
|
166
|
+
variant="ghost"
|
|
167
|
+
size="sm"
|
|
168
|
+
className="mb-1 w-full justify-start"
|
|
169
|
+
aria-pressed={nodeId === null}
|
|
170
|
+
onClick={() => setNodeId(null)}
|
|
171
|
+
>
|
|
172
|
+
All resources
|
|
173
|
+
</Button>
|
|
174
|
+
<TreeList
|
|
175
|
+
aria-label="Teams and projects"
|
|
176
|
+
nodes={tree}
|
|
177
|
+
defaultExpanded={tree.map((n) => n.id)}
|
|
178
|
+
selectionMode="single"
|
|
179
|
+
selected={nodeId ? [nodeId] : []}
|
|
180
|
+
onSelectedChange={(next) => setNodeId(next[0] ?? null)}
|
|
181
|
+
/>
|
|
182
|
+
</CardBody>
|
|
183
|
+
</Card>
|
|
184
|
+
<Stack gap={6} className="min-w-0">
|
|
185
|
+
<HStack gap={3} wrap justify="between">
|
|
186
|
+
<Text id={`${ids}-scope`} as="span" variant="label" aria-live="polite">
|
|
187
|
+
{scope} · {rows.length === 1 ? '1 resource' : `${rows.length} resources`}
|
|
188
|
+
</Text>
|
|
189
|
+
<HStack gap={2}>
|
|
190
|
+
<Text as="span" variant="supporting" tone="muted">
|
|
191
|
+
Group by
|
|
192
|
+
</Text>
|
|
193
|
+
<SimpleSelect
|
|
194
|
+
aria-label="Group by"
|
|
195
|
+
size="sm"
|
|
196
|
+
items={GROUP_ITEMS}
|
|
197
|
+
value={groupBy}
|
|
198
|
+
onValueChange={(v) => v && setGroupBy(v)}
|
|
199
|
+
className="w-32"
|
|
200
|
+
/>
|
|
201
|
+
</HStack>
|
|
202
|
+
</HStack>
|
|
203
|
+
{groups.length === 0 ? (
|
|
204
|
+
<EmptyState
|
|
205
|
+
size="sm"
|
|
206
|
+
titleAs="h2"
|
|
207
|
+
title="No resources here"
|
|
208
|
+
description="Pick another team or project."
|
|
209
|
+
/>
|
|
210
|
+
) : (
|
|
211
|
+
groups.map((g) => (
|
|
212
|
+
<section key={g.key} aria-labelledby={`${ids}-${g.key}`}>
|
|
213
|
+
<SectionHeader headingId={`${ids}-${g.key}`} title={g.label} count={g.rows.length} as="h2" />
|
|
214
|
+
<DataTable<TreeResource>
|
|
215
|
+
rows={g.rows}
|
|
216
|
+
columns={cols}
|
|
217
|
+
caption={`${g.label} resources`}
|
|
218
|
+
getRowLabel={(r) => r.name}
|
|
219
|
+
paginated={false}
|
|
220
|
+
showDensityToggle={false}
|
|
221
|
+
showColumnMenu={false}
|
|
222
|
+
defaultSort={{ columnId: 'name', direction: 'ascending' }}
|
|
223
|
+
/>
|
|
224
|
+
</section>
|
|
225
|
+
))
|
|
226
|
+
)}
|
|
227
|
+
</Stack>
|
|
228
|
+
</div>
|
|
229
|
+
</Page>
|
|
230
|
+
</ConsoleShell>
|
|
231
|
+
);
|
|
232
|
+
}
|