@flamingo-stack/openframe-frontend-core 0.0.853 → 0.0.855
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/build/export-map.json +1 -1
- package/dist/{case-study-card-WP5Z234Q.cjs → case-study-card-6VAMNHLU.cjs} +8 -8
- package/dist/{chunk-2W22LY3W.cjs → chunk-2JCA7BRK.cjs} +19 -19
- package/dist/{chunk-53QPG7SJ.cjs → chunk-2MCPK6AX.cjs} +28 -28
- package/dist/{chunk-QOW2GP3V.cjs → chunk-2P72KAG5.cjs} +23 -23
- package/dist/{chunk-NMWLXHFH.cjs → chunk-3GJ75OUL.cjs} +183 -183
- package/dist/{chunk-P63CRPWI.cjs → chunk-4H3J57AN.cjs} +25 -25
- package/dist/{chunk-6GWRPW6B.cjs → chunk-5NK3FUHY.cjs} +4 -4
- package/dist/{chunk-DLTPSP5F.cjs → chunk-5TYG5JNB.cjs} +2 -2
- package/dist/{chunk-EIACEY2L.cjs → chunk-65NMKAPQ.cjs} +12 -12
- package/dist/{chunk-IGVAPIEW.cjs → chunk-6C4OR4MZ.cjs} +2 -2
- package/dist/{chunk-GG466XFQ.cjs → chunk-6DMH4GXN.cjs} +30 -5
- package/dist/{chunk-EF2FZ7GT.cjs → chunk-6VVQJU76.cjs} +54 -54
- package/dist/{chunk-G7GF4PWA.cjs → chunk-77GX573I.cjs} +9 -9
- package/dist/{chunk-7RZHSBNX.cjs → chunk-AOOHJLXS.cjs} +90 -90
- package/dist/{chunk-O3EKOXFZ.cjs → chunk-AYMY2VNB.cjs} +37 -43
- package/dist/{chunk-67CZ2Q5K.cjs → chunk-BSPIGZCC.cjs} +36 -36
- package/dist/{chunk-FW7RKWPY.cjs → chunk-D6GEMVHD.cjs} +3 -3
- package/dist/{chunk-KEV6F3QN.cjs → chunk-DPNFI5XM.cjs} +100 -100
- package/dist/{chunk-6V2M2X3M.cjs → chunk-DTZ6KASI.cjs} +6 -6
- package/dist/{chunk-SSKMGH5O.cjs → chunk-DXVGGWLG.cjs} +10 -10
- package/dist/{chunk-IO6NN3SL.cjs → chunk-EOUEUYCQ.cjs} +54 -54
- package/dist/{chunk-5RMBLOJ5.cjs → chunk-ESGE7CUX.cjs} +14 -14
- package/dist/{chunk-ETZFBB7O.cjs → chunk-EXQSIX2I.cjs} +4 -4
- package/dist/{chunk-IC4IJJB2.cjs → chunk-F33EAPD2.cjs} +124 -124
- package/dist/{chunk-COWMXUHN.cjs → chunk-GK577ZLJ.cjs} +6 -14
- package/dist/{chunk-E4MFQKPC.cjs → chunk-GZYRDLJW.cjs} +103 -103
- package/dist/{chunk-LFEURIZN.cjs → chunk-HML3JUXF.cjs} +324 -322
- package/dist/{chunk-BNHTBLAG.cjs → chunk-J3CFRGZ2.cjs} +17 -17
- package/dist/{chunk-USPUZO53.cjs → chunk-KZAQPRTA.cjs} +9 -11
- package/dist/{chunk-HTYJUMFB.cjs → chunk-LSLRJJFP.cjs} +2 -2
- package/dist/{chunk-TBGQMHCG.cjs → chunk-MINFN3IG.cjs} +8 -8
- package/dist/{chunk-KHOB3TES.cjs → chunk-MX52LZ6M.cjs} +6 -6
- package/dist/{chunk-OH5TXFRR.cjs → chunk-O4CPT3RK.cjs} +7 -7
- package/dist/{chunk-HF3V6WBJ.cjs → chunk-OTHQ2KQW.cjs} +3 -3
- package/dist/{chunk-IUDBG3HJ.cjs → chunk-PCQ2HKU3.cjs} +5 -5
- package/dist/{chunk-BNG6JOGZ.cjs → chunk-QPHXYE5P.cjs} +27 -27
- package/dist/{chunk-UO2EJGRJ.cjs → chunk-QZXCUY36.cjs} +53 -53
- package/dist/{chunk-COJG2ATY.cjs → chunk-RIJU4HZ2.cjs} +51 -55
- package/dist/{chunk-KKFDRCQF.cjs → chunk-RY7INTYU.cjs} +14 -1
- package/dist/{chunk-GI3MMBP3.cjs → chunk-SQ7YHWEP.cjs} +228 -15
- package/dist/{chunk-U767P4JU.cjs → chunk-STV6VJ3U.cjs} +21 -21
- package/dist/chunk-T54I5ZZY.cjs +62 -0
- package/dist/{chunk-UV45EPXN.cjs → chunk-TW5Y4D5I.cjs} +7 -7
- package/dist/{chunk-FX35L4Y6.cjs → chunk-XCQ5I27Z.cjs} +3 -3
- package/dist/{chunk-AZEO7HBB.cjs → chunk-Y44EF4JW.cjs} +2 -2
- package/dist/{chunk-W7OFAROY.cjs → chunk-YSXVRTGT.cjs} +9 -12
- package/dist/{chunk-GI6ECCWB.cjs → chunk-ZUGJLCYP.cjs} +1272 -856
- package/dist/{chunk-KNN2GCMW.cjs → chunk-ZYAVIRQV.cjs} +25 -25
- package/dist/{cloud-tenants-QUL2XPAS.cjs → cloud-tenants-2XFIDDGU.cjs} +28 -29
- package/dist/components/case-studies/index.cjs +44 -45
- package/dist/components/chat/chat-message-row.d.ts +1 -1
- package/dist/components/chat/chat-message-row.d.ts.map +1 -1
- package/dist/components/chat/chat-message-row.js +4 -4
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts +7 -13
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/use-cover-image-fallback.js +2 -8
- package/dist/components/chat/hooks/use-chat-attachment-image-gallery.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-attachment-image-gallery.js +8 -4
- package/dist/components/chat/index.cjs +22 -23
- package/dist/components/contact/index.cjs +29 -30
- package/dist/components/docs/index.cjs +32 -33
- package/dist/components/embeds/index.cjs +29 -30
- package/dist/components/faq/index.cjs +30 -31
- package/dist/components/features/cloud-tenants/index.d.ts +7 -5
- package/dist/components/features/cloud-tenants/index.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/index.js +8 -1
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts +26 -0
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts.map +1 -0
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.js +176 -0
- package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts +4 -2
- package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-access-cell.js +3 -2
- package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts +12 -5
- package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-customer-cell.js +38 -7
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts +7 -0
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts.map +1 -0
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.js +17 -0
- package/dist/components/features/cloud-tenants/tenant-presentation.d.ts +19 -0
- package/dist/components/features/cloud-tenants/tenant-presentation.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-presentation.js +24 -3
- package/dist/components/features/cloud-tenants/tenant-row.d.ts +35 -8
- package/dist/components/features/cloud-tenants/tenant-row.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts +9 -3
- package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-columns.js +5 -3
- package/dist/components/features/cloud-tenants/tenants-table-view.d.ts +27 -6
- package/dist/components/features/cloud-tenants/tenants-table-view.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-view.js +75 -17
- package/dist/components/features/devices/device-directory.d.ts +18 -0
- package/dist/components/features/devices/device-directory.d.ts.map +1 -0
- package/dist/components/features/devices/device-directory.js +35 -0
- package/dist/components/features/devices/devices-filter-toolbar.d.ts +3 -1
- package/dist/components/features/devices/devices-filter-toolbar.d.ts.map +1 -1
- package/dist/components/features/devices/devices-filter-toolbar.js +2 -1
- package/dist/components/features/devices/devices-panel-view.d.ts +3 -1
- package/dist/components/features/devices/devices-panel-view.d.ts.map +1 -1
- package/dist/components/features/devices/devices-panel-view.js +11 -2
- package/dist/components/features/devices/devices-table-columns.d.ts +17 -3
- package/dist/components/features/devices/devices-table-columns.d.ts.map +1 -1
- package/dist/components/features/devices/devices-table-columns.js +80 -10
- package/dist/components/features/devices/index.d.ts +1 -0
- package/dist/components/features/devices/index.d.ts.map +1 -1
- package/dist/components/features/devices/index.js +8 -0
- package/dist/components/features/devices/types.d.ts +14 -0
- package/dist/components/features/devices/types.d.ts.map +1 -1
- package/dist/components/features/devices/use-tag-filter-modal.d.ts +1 -0
- package/dist/components/features/devices/use-tag-filter-modal.d.ts.map +1 -1
- package/dist/components/features/devices/use-tag-filter-modal.js +13 -8
- package/dist/components/features/index.cjs +42 -29
- package/dist/components/help-center-pages/index.cjs +134 -135
- package/dist/components/index.cjs +126 -113
- package/dist/components/meeting-scheduler/index.cjs +102 -103
- package/dist/components/navigation/app-layout.js +2 -2
- package/dist/components/navigation/index.cjs +31 -32
- package/dist/components/onboarding-guides/index.cjs +31 -32
- package/dist/components/product-demo/index.cjs +39 -40
- package/dist/components/related-content/index.cjs +16 -17
- package/dist/components/tickets/index.cjs +35 -36
- package/dist/components/ui/date-picker.d.ts +2 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/date-picker.js +19 -6
- package/dist/components/ui/entity-image.d.ts +1 -2
- package/dist/components/ui/entity-image.d.ts.map +1 -1
- package/dist/components/ui/entity-image.js +4 -8
- package/dist/components/ui/index.cjs +28 -29
- package/dist/components/ui/markdown/base-components.d.ts +0 -13
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
- package/dist/components/ui/markdown/base-components.js +7 -7
- package/dist/components/ui/square-avatar.d.ts.map +1 -1
- package/dist/components/ui/square-avatar.js +3 -8
- package/dist/{devices-LNB4CYPP.cjs → devices-O3DYGIZB.cjs} +29 -30
- package/dist/embed-shims/index.cjs +3 -2
- package/dist/embed-shims/next-image.cjs +3 -2
- package/dist/embed-shims/next-image.d.ts +2 -0
- package/dist/embed-shims/next-image.d.ts.map +1 -1
- package/dist/embed-shims/next-image.js +14 -4
- package/dist/{embeddable-chat-SJEEJI4N.cjs → embeddable-chat-QD7NFD2H.cjs} +100 -101
- package/dist/hooks/index.cjs +27 -16
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/ui/use-content-breakpoint.d.ts +8 -0
- package/dist/hooks/ui/use-content-breakpoint.d.ts.map +1 -1
- package/dist/hooks/ui/use-content-breakpoint.js +17 -3
- package/dist/hooks/ui/use-media-query.d.ts +1 -0
- package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
- package/dist/hooks/ui/use-media-query.js +3 -1
- package/dist/hooks/use-authed-asset-src.d.ts +77 -2
- package/dist/hooks/use-authed-asset-src.d.ts.map +1 -1
- package/dist/hooks/use-authed-asset-src.js +185 -73
- package/dist/hooks/use-authed-image-src.d.ts +11 -4
- package/dist/hooks/use-authed-image-src.d.ts.map +1 -1
- package/dist/hooks/use-authed-image-src.js +6 -2
- package/dist/hooks/use-image-fallback.d.ts +16 -0
- package/dist/hooks/use-image-fallback.d.ts.map +1 -0
- package/dist/hooks/use-image-fallback.js +14 -0
- package/dist/index.cjs +58 -36
- package/dist/{knowledge-UTWKB6XG.cjs → knowledge-K7AFNZNO.cjs} +29 -30
- package/dist/{logs-PMXIWMBQ.cjs → logs-MZ3BR2FR.cjs} +35 -36
- package/dist/{policies-6DCMFDL7.cjs → policies-XVKQ2W7S.cjs} +32 -33
- package/dist/{remote-session-SG44CNOK.cjs → remote-session-VOZJXDM4.cjs} +13 -14
- package/dist/{scripts-2G7LXW7Z.cjs → scripts-IBPMBS6G.cjs} +35 -36
- package/dist/{software-update-MYLKHDHC.cjs → software-update-PXTANMAO.cjs} +32 -33
- package/dist/{tickets-board-SQY2Y6QV.cjs → tickets-board-POYZS7BW.cjs} +16 -17
- package/dist/utils/app-origins.d.ts +3 -6
- package/dist/utils/app-origins.d.ts.map +1 -1
- package/dist/utils/app-origins.js +8 -1
- package/dist/utils/embed-authed-fetch.d.ts +46 -26
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/embed-authed-fetch.js +24 -8
- package/dist/utils/index.cjs +4 -1
- package/dist/utils/presentation-for.d.ts +6 -0
- package/dist/utils/presentation-for.d.ts.map +1 -0
- package/dist/utils/presentation-for.js +7 -0
- package/dist/{video-file-player-XMOB234V.cjs → video-file-player-RIOT24NT.cjs} +3 -4
- package/package.json +1 -1
- package/src/components/chat/chat-message-row.tsx +8 -7
- package/src/components/chat/entity-cards/use-cover-image-fallback.ts +8 -28
- package/src/components/chat/hooks/use-chat-attachment-image-gallery.tsx +19 -8
- package/src/components/features/cloud-tenants/index.ts +8 -3
- package/src/components/features/cloud-tenants/single-customer-tenants-table-view.tsx +234 -0
- package/src/components/features/cloud-tenants/tenant-access-cell.tsx +12 -5
- package/src/components/features/cloud-tenants/tenant-customer-cell.tsx +48 -12
- package/src/components/features/cloud-tenants/tenant-licenses-cell.tsx +28 -0
- package/src/components/features/cloud-tenants/tenant-presentation.ts +46 -4
- package/src/components/features/cloud-tenants/tenant-row.ts +38 -8
- package/src/components/features/cloud-tenants/tenants-table-columns.ts +8 -5
- package/src/components/features/cloud-tenants/tenants-table-view.tsx +123 -28
- package/src/components/features/devices/device-directory.ts +55 -0
- package/src/components/features/devices/devices-filter-toolbar.tsx +4 -1
- package/src/components/features/devices/devices-panel-view.tsx +12 -2
- package/src/components/features/devices/devices-table-columns.tsx +123 -8
- package/src/components/features/devices/index.ts +7 -0
- package/src/components/features/devices/types.ts +14 -0
- package/src/components/features/devices/use-tag-filter-modal.ts +32 -14
- package/src/components/navigation/app-layout.tsx +4 -4
- package/src/components/ui/date-picker.tsx +21 -3
- package/src/components/ui/entity-image.tsx +4 -12
- package/src/components/ui/markdown/base-components.tsx +15 -20
- package/src/components/ui/square-avatar.tsx +5 -8
- package/src/embed-shims/next-image.tsx +34 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/ui/use-content-breakpoint.ts +21 -2
- package/src/hooks/ui/use-media-query.ts +1 -0
- package/src/hooks/use-authed-asset-src.ts +323 -107
- package/src/hooks/use-authed-image-src.ts +15 -5
- package/src/hooks/use-image-fallback.ts +31 -0
- package/src/utils/app-origins.ts +12 -7
- package/src/utils/embed-authed-fetch.ts +71 -32
- package/src/utils/presentation-for.ts +10 -0
- package/dist/chunk-UAI4VMEF.cjs +0 -46
- package/dist/chunk-YSDB6HGQ.cjs +0 -102
|
@@ -5,24 +5,37 @@ import { cn } from '../../../utils/cn';
|
|
|
5
5
|
import { openInNewTab } from '../../../utils/open-in-new-tab';
|
|
6
6
|
import { ArrowRightUpIcon } from '../../icons-v2-generated/arrows/arrow-right-up-icon';
|
|
7
7
|
import { Button } from '../../ui/button';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type ColumnDef,
|
|
10
|
+
type ColumnFiltersState,
|
|
11
|
+
DataTable,
|
|
12
|
+
type DataTableFilterOption,
|
|
13
|
+
dataTableLoadingRows,
|
|
14
|
+
type Row,
|
|
15
|
+
useDataTable,
|
|
16
|
+
} from '../../ui/data-table';
|
|
9
17
|
import { useLoadingScope } from '../../ui/loading-scope';
|
|
18
|
+
import {
|
|
19
|
+
SingleCustomerTenantsTableView,
|
|
20
|
+
type SingleCustomerTenantsTableViewProps,
|
|
21
|
+
} from './single-customer-tenants-table-view';
|
|
10
22
|
import { TenantAccessCell } from './tenant-access-cell';
|
|
11
23
|
import { TenantCell } from './tenant-cell';
|
|
12
24
|
import { TenantCustomerCell } from './tenant-customer-cell';
|
|
13
|
-
import
|
|
25
|
+
import { TenantLicensesCell } from './tenant-licenses-cell';
|
|
26
|
+
import type { MultiCustomerTenantRow } from './tenant-row';
|
|
14
27
|
import { TENANT_COLUMNS, type TenantsTableColumn } from './tenants-table-columns';
|
|
15
28
|
|
|
16
|
-
const getRowId = (row:
|
|
29
|
+
const getRowId = (row: MultiCustomerTenantRow) => row.id;
|
|
17
30
|
|
|
18
31
|
const columnMeta = ({ width, hideAt, align }: TenantsTableColumn) => ({ width, hideAt, align });
|
|
19
32
|
|
|
20
33
|
/** A tenant still being read: a row the table keys, every field a cell shows empty (each draws its loading form). */
|
|
21
|
-
const loadingTenant = (id: string):
|
|
34
|
+
const loadingTenant = (id: string): MultiCustomerTenantRow => ({
|
|
22
35
|
id,
|
|
23
36
|
name: '',
|
|
24
37
|
provider: '',
|
|
25
|
-
|
|
38
|
+
customers: [{ name: '' }],
|
|
26
39
|
accessState: '',
|
|
27
40
|
});
|
|
28
41
|
|
|
@@ -45,16 +58,35 @@ function OpenTenantButton({ href, name }: { href: string; name: string }) {
|
|
|
45
58
|
);
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
/** The columns whose header carries a funnel (Figma 3036-4860). */
|
|
62
|
+
export const TENANTS_TABLE_FILTER_COLUMNS = ['tenant', 'customer', 'licenses', 'access'] as const;
|
|
63
|
+
|
|
64
|
+
export type TenantsTableFilterColumn = (typeof TENANTS_TABLE_FILTER_COLUMNS)[number];
|
|
65
|
+
|
|
66
|
+
/** Each column's funnel: what it offers, what is ticked. The host narrows the rows; the view only draws. */
|
|
67
|
+
export interface TenantsTableFilters {
|
|
68
|
+
/** A column the host leaves out has no funnel; one with no options draws its bare label. */
|
|
69
|
+
options: Partial<Record<TenantsTableFilterColumn, DataTableFilterOption[]>>;
|
|
70
|
+
selections: Readonly<Record<TenantsTableFilterColumn, readonly string[]>>;
|
|
71
|
+
onChange: (next: Record<TenantsTableFilterColumn, string[]>) => void;
|
|
72
|
+
/** The options are in flight: the funnels draw, inert. */
|
|
73
|
+
pending?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
48
76
|
/**
|
|
49
77
|
* Click handler that opens `href` in a new tab. The row is itself the details
|
|
50
78
|
* link and an `<a>` cannot be nested in an `<a>`, so the button opens it by script.
|
|
51
79
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
interface MultiCustomerTenantsTableViewProps {
|
|
81
|
+
/** A directory per row that serves any number of customers, with its licences and the column funnels. */
|
|
82
|
+
variant: 'multi-customer';
|
|
83
|
+
rows: MultiCustomerTenantRow[];
|
|
54
84
|
/** Where a row leads: the row is a link to it, and its last cell opens it in a new tab. */
|
|
55
|
-
getHref: (row:
|
|
85
|
+
getHref: (row: MultiCustomerTenantRow) => string;
|
|
56
86
|
/** The search the rows answer; named by the "no match" message. */
|
|
57
87
|
search?: string;
|
|
88
|
+
/** The funnels of the column headers. Absent: none, as on the product page. */
|
|
89
|
+
filters?: TenantsTableFilters;
|
|
58
90
|
/** The rows on screen are the previous result of a search still loading: they dim. */
|
|
59
91
|
isPending?: boolean;
|
|
60
92
|
/** All matching tenants, loaded or not. Default: the rows given. */
|
|
@@ -68,14 +100,27 @@ export interface TenantsTableViewProps {
|
|
|
68
100
|
now?: Date;
|
|
69
101
|
}
|
|
70
102
|
|
|
103
|
+
/** The single-customer list unless `variant` is 'multi-customer', the one that takes funnels and needs its own rows. */
|
|
104
|
+
export type TenantsTableViewProps = SingleCustomerTenantsTableViewProps | MultiCustomerTenantsTableViewProps;
|
|
105
|
+
|
|
106
|
+
/** The tenants list, in the layout its `variant` names. */
|
|
107
|
+
export function TenantsTableView(props: TenantsTableViewProps) {
|
|
108
|
+
return props.variant === 'multi-customer' ? (
|
|
109
|
+
<MultiCustomerTenantsTableView {...props} />
|
|
110
|
+
) : (
|
|
111
|
+
<SingleCustomerTenantsTableView {...props} />
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
/** The rows a loading tenants table draws when the host states none (a typical page of tenants). */
|
|
72
116
|
const TENANTS_TABLE_SKELETON_ROWS = 20;
|
|
73
117
|
|
|
74
|
-
/** The tenants list: tenant,
|
|
75
|
-
|
|
118
|
+
/** The tenants list: tenant, customers, licences, access state and a way into each tenant's details. */
|
|
119
|
+
function MultiCustomerTenantsTableView({
|
|
76
120
|
rows,
|
|
77
121
|
getHref,
|
|
78
122
|
search = '',
|
|
123
|
+
filters,
|
|
79
124
|
isPending = false,
|
|
80
125
|
totalCount,
|
|
81
126
|
stickyHeaderOffset,
|
|
@@ -84,51 +129,75 @@ export function TenantsTableView({
|
|
|
84
129
|
isFetchingNextPage = false,
|
|
85
130
|
onLoadMore,
|
|
86
131
|
now,
|
|
87
|
-
}:
|
|
88
|
-
const
|
|
89
|
-
|
|
132
|
+
}: MultiCustomerTenantsTableViewProps) {
|
|
133
|
+
const filterOptions = filters?.options;
|
|
134
|
+
const filtersPending = filters?.pending ?? false;
|
|
135
|
+
const columns = useMemo<ColumnDef<MultiCustomerTenantRow>[]>(() => {
|
|
136
|
+
/**
|
|
137
|
+
* A column's funnel, while the host offers one. Declared with no options too: below lg only a filtered column keeps
|
|
138
|
+
* its header, and the column filter draws the bare label when there is nothing to choose.
|
|
139
|
+
*/
|
|
140
|
+
const funnel = (id: TenantsTableFilterColumn) => {
|
|
141
|
+
const options = filterOptions?.[id];
|
|
142
|
+
return options ? { filter: { options, pending: filtersPending } } : {};
|
|
143
|
+
};
|
|
144
|
+
return [
|
|
90
145
|
{
|
|
91
146
|
id: TENANT_COLUMNS.tenant.id,
|
|
92
147
|
header: TENANT_COLUMNS.tenant.header,
|
|
93
|
-
cell: ({ row }: { row: Row<
|
|
148
|
+
cell: ({ row }: { row: Row<MultiCustomerTenantRow> }) => (
|
|
94
149
|
<TenantCell provider={row.original.provider} name={row.original.name} domain={row.original.domain} />
|
|
95
150
|
),
|
|
96
151
|
enableSorting: false,
|
|
97
|
-
meta: columnMeta(TENANT_COLUMNS.tenant),
|
|
152
|
+
meta: { ...columnMeta(TENANT_COLUMNS.tenant), ...funnel('tenant') },
|
|
98
153
|
},
|
|
99
154
|
{
|
|
100
155
|
id: TENANT_COLUMNS.customer.id,
|
|
101
156
|
header: TENANT_COLUMNS.customer.header,
|
|
102
|
-
cell: ({ row }: { row: Row<
|
|
157
|
+
cell: ({ row }: { row: Row<MultiCustomerTenantRow> }) => (
|
|
103
158
|
<TenantCustomerCell
|
|
104
|
-
|
|
105
|
-
imageUrl={row.original.customer.imageUrl}
|
|
159
|
+
customers={row.original.customers}
|
|
106
160
|
userCount={row.original.userCount}
|
|
161
|
+
unattributedUserCount={row.original.unattributedUserCount}
|
|
162
|
+
neverRead={row.original.lastReadAt == null && row.original.userCount == null}
|
|
107
163
|
/>
|
|
108
164
|
),
|
|
109
165
|
enableSorting: false,
|
|
110
|
-
meta: columnMeta(TENANT_COLUMNS.customer),
|
|
166
|
+
meta: { ...columnMeta(TENANT_COLUMNS.customer), ...funnel('customer') },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: TENANT_COLUMNS.licenses.id,
|
|
170
|
+
header: TENANT_COLUMNS.licenses.header,
|
|
171
|
+
cell: ({ row }: { row: Row<MultiCustomerTenantRow> }) => (
|
|
172
|
+
<TenantLicensesCell licenses={row.original.licenses} />
|
|
173
|
+
),
|
|
174
|
+
enableSorting: false,
|
|
175
|
+
meta: { ...columnMeta(TENANT_COLUMNS.licenses), ...funnel('licenses') },
|
|
111
176
|
},
|
|
112
177
|
{
|
|
113
178
|
id: TENANT_COLUMNS.access.id,
|
|
114
179
|
header: TENANT_COLUMNS.access.header,
|
|
115
|
-
cell: ({ row }: { row: Row<
|
|
116
|
-
<TenantAccessCell
|
|
180
|
+
cell: ({ row }: { row: Row<MultiCustomerTenantRow> }) => (
|
|
181
|
+
<TenantAccessCell
|
|
182
|
+
accessState={row.original.accessState}
|
|
183
|
+
lastReadAt={row.original.lastReadAt}
|
|
184
|
+
syncPaused={row.original.syncPaused}
|
|
185
|
+
now={now}
|
|
186
|
+
/>
|
|
117
187
|
),
|
|
118
188
|
enableSorting: false,
|
|
119
|
-
meta: columnMeta(TENANT_COLUMNS.access),
|
|
189
|
+
meta: { ...columnMeta(TENANT_COLUMNS.access), ...funnel('access') },
|
|
120
190
|
},
|
|
121
191
|
{
|
|
122
192
|
id: TENANT_COLUMNS.open.id,
|
|
123
|
-
cell: ({ row }: { row: Row<
|
|
193
|
+
cell: ({ row }: { row: Row<MultiCustomerTenantRow> }) => (
|
|
124
194
|
<OpenTenantButton href={getHref(row.original)} name={row.original.name} />
|
|
125
195
|
),
|
|
126
196
|
enableSorting: false,
|
|
127
197
|
meta: columnMeta(TENANT_COLUMNS.open),
|
|
128
198
|
},
|
|
129
|
-
]
|
|
130
|
-
|
|
131
|
-
);
|
|
199
|
+
];
|
|
200
|
+
}, [getHref, now, filterOptions, filtersPending]);
|
|
132
201
|
|
|
133
202
|
// A page's loading scope: the table holds one page of placeholder rows, drawn with the real cells (`loadingCells`).
|
|
134
203
|
const loading = useLoadingScope();
|
|
@@ -136,7 +205,31 @@ export function TenantsTableView({
|
|
|
136
205
|
() => (loading ? dataTableLoadingRows(skeletonRows, loadingTenant) : null),
|
|
137
206
|
[loading, skeletonRows],
|
|
138
207
|
);
|
|
139
|
-
const
|
|
208
|
+
const selections = filters?.selections;
|
|
209
|
+
const onFiltersChange = filters?.onChange;
|
|
210
|
+
const columnFilters = useMemo<ColumnFiltersState>(
|
|
211
|
+
() =>
|
|
212
|
+
TENANTS_TABLE_FILTER_COLUMNS.flatMap(id =>
|
|
213
|
+
selections && selections[id].length > 0 ? [{ id, value: selections[id] }] : [],
|
|
214
|
+
),
|
|
215
|
+
[selections],
|
|
216
|
+
);
|
|
217
|
+
const table = useDataTable<MultiCustomerTenantRow>({
|
|
218
|
+
data: loadingRows ?? rows,
|
|
219
|
+
columns,
|
|
220
|
+
getRowId,
|
|
221
|
+
enableSorting: false,
|
|
222
|
+
state: { columnFilters },
|
|
223
|
+
onColumnFiltersChange: updater => {
|
|
224
|
+
const next = typeof updater === 'function' ? updater(columnFilters) : updater;
|
|
225
|
+
const picked = (id: TenantsTableFilterColumn) => {
|
|
226
|
+
const value = next.find(filter => filter.id === id)?.value;
|
|
227
|
+
return Array.isArray(value) ? (value as string[]) : [];
|
|
228
|
+
};
|
|
229
|
+
const selected = Object.fromEntries(TENANTS_TABLE_FILTER_COLUMNS.map(id => [id, picked(id)]));
|
|
230
|
+
onFiltersChange?.(selected as Record<TenantsTableFilterColumn, string[]>);
|
|
231
|
+
},
|
|
232
|
+
});
|
|
140
233
|
|
|
141
234
|
return (
|
|
142
235
|
// Dim, don't unmount, the stale rows while a deferred search refetches.
|
|
@@ -152,7 +245,9 @@ export function TenantsTableView({
|
|
|
152
245
|
skeletonRows={skeletonRows}
|
|
153
246
|
emptyState={{
|
|
154
247
|
title: 'No tenants match',
|
|
155
|
-
description:
|
|
248
|
+
description: search
|
|
249
|
+
? `Nothing matches "${search}". Try a different name, domain or customer.`
|
|
250
|
+
: 'Nothing matches the selected filters. Try adjusting them.',
|
|
156
251
|
}}
|
|
157
252
|
rowClassName="mb-[var(--spacing-system-xxs)]"
|
|
158
253
|
rowHref={getHref}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { presentationFor } from '../../../utils/presentation-for';
|
|
2
|
+
import type { DeviceRow } from './types';
|
|
3
|
+
|
|
4
|
+
/** The schema's `DirectoryUserLink`: who a device is tied to in a directory. */
|
|
5
|
+
export type DeviceDirectoryUserLink = 'PRIMARY' | 'SHARED' | 'NONE';
|
|
6
|
+
|
|
7
|
+
/** The schema's `DirectorySource`: a directory that knows a device. */
|
|
8
|
+
export type DeviceDirectorySource = 'MICROSOFT_365' | 'GOOGLE_WORKSPACE';
|
|
9
|
+
|
|
10
|
+
const USER_LINK_LABELS = {
|
|
11
|
+
PRIMARY: 'Primary user',
|
|
12
|
+
SHARED: 'Shared',
|
|
13
|
+
NONE: 'No user',
|
|
14
|
+
} satisfies Record<DeviceDirectoryUserLink, string>;
|
|
15
|
+
|
|
16
|
+
const SOURCE_LABELS = {
|
|
17
|
+
MICROSOFT_365: 'M365',
|
|
18
|
+
GOOGLE_WORKSPACE: 'GOOGLE',
|
|
19
|
+
} satisfies Record<DeviceDirectorySource, string>;
|
|
20
|
+
|
|
21
|
+
/** The USER funnel's option (Figma 1136:64807). A value this build does not know keeps its own name. */
|
|
22
|
+
export function directoryUserLinkLabel(link: string): string {
|
|
23
|
+
return presentationFor(USER_LINK_LABELS, link) ?? link;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The SOURCES tag: which directory knows the device. */
|
|
27
|
+
export function directorySourceLabel(source: string): string {
|
|
28
|
+
return presentationFor(SOURCE_LABELS, source) ?? source;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const EMPTY_USER = '—';
|
|
32
|
+
|
|
33
|
+
// The row carries the wire's string: a member compared to it is typed here, so a misspelt one fails to compile.
|
|
34
|
+
const PRIMARY_LINK: DeviceDirectoryUserLink = 'PRIMARY';
|
|
35
|
+
const SHARED_LINK: DeviceDirectoryUserLink = 'SHARED';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The USER cell's two lines (Figma 865:55733): the primary user over "Primary user", "Shared" over how many, or the
|
|
39
|
+
* empty mark when nobody is tied to the device.
|
|
40
|
+
*/
|
|
41
|
+
export function directoryUserLines(
|
|
42
|
+
device: Pick<DeviceRow, 'directoryUserLink' | 'directoryUserName' | 'directoryUserCount'>,
|
|
43
|
+
): {
|
|
44
|
+
name: string;
|
|
45
|
+
detail: string | null;
|
|
46
|
+
} {
|
|
47
|
+
if (device.directoryUserLink === PRIMARY_LINK) {
|
|
48
|
+
return { name: device.directoryUserName ?? EMPTY_USER, detail: USER_LINK_LABELS.PRIMARY };
|
|
49
|
+
}
|
|
50
|
+
if (device.directoryUserLink === SHARED_LINK) {
|
|
51
|
+
const count = device.directoryUserCount ?? 0;
|
|
52
|
+
return { name: USER_LINK_LABELS.SHARED, detail: `${count} recent ${count === 1 ? 'user' : 'users'}` };
|
|
53
|
+
}
|
|
54
|
+
return { name: EMPTY_USER, detail: null };
|
|
55
|
+
}
|
|
@@ -29,6 +29,8 @@ export interface DevicesFilterToolbarProps {
|
|
|
29
29
|
selectedTags: FilterModalProps['selectedTags'];
|
|
30
30
|
onTagsChange: FilterModalProps['onTagsChange'];
|
|
31
31
|
isLoading?: boolean;
|
|
32
|
+
/** The search field's prompt. Default "Search for Devices". */
|
|
33
|
+
searchPlaceholder?: string;
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* When true (default), pins the toolbar at the top with the same negative-margin
|
|
@@ -67,6 +69,7 @@ export function DevicesFilterToolbar({
|
|
|
67
69
|
selectedTags,
|
|
68
70
|
onTagsChange,
|
|
69
71
|
isLoading,
|
|
72
|
+
searchPlaceholder = 'Search for Devices',
|
|
70
73
|
sticky = true,
|
|
71
74
|
toolbarRef,
|
|
72
75
|
}: DevicesFilterToolbarProps) {
|
|
@@ -87,7 +90,7 @@ export function DevicesFilterToolbar({
|
|
|
87
90
|
onSearchChange={onSearchChange}
|
|
88
91
|
onTagRemove={onTagRemove}
|
|
89
92
|
onClearAll={onClearAll}
|
|
90
|
-
placeholder=
|
|
93
|
+
placeholder={searchPlaceholder}
|
|
91
94
|
addMorePlaceholder="Add More..."
|
|
92
95
|
/>
|
|
93
96
|
</div>
|
|
@@ -99,6 +99,8 @@ export interface DevicesPanelViewProps<T extends DeviceRow = DeviceRow> {
|
|
|
99
99
|
hideColumns?: string[];
|
|
100
100
|
/** Filter keys to drop from the funnels and the grid row; the column itself stays. */
|
|
101
101
|
hideFilters?: string[];
|
|
102
|
+
/** Adds the USER and SOURCES columns and funnels, for a tenant with directories. Default: neither. */
|
|
103
|
+
directoryColumns?: boolean;
|
|
102
104
|
/** Message shown when the list is empty. */
|
|
103
105
|
emptyMessage?: string;
|
|
104
106
|
/** Drawn instead of the toolbar and the list: the pristine, genuinely empty panel. */
|
|
@@ -151,6 +153,7 @@ export function DevicesPanelView<T extends DeviceRow = DeviceRow>({
|
|
|
151
153
|
actionsColumn,
|
|
152
154
|
hideColumns,
|
|
153
155
|
hideFilters,
|
|
156
|
+
directoryColumns = false,
|
|
154
157
|
emptyMessage = 'No devices found. Try adjusting your search or filters.',
|
|
155
158
|
emptyState,
|
|
156
159
|
noOrganizations = false,
|
|
@@ -164,11 +167,17 @@ export function DevicesPanelView<T extends DeviceRow = DeviceRow>({
|
|
|
164
167
|
const scoped = useLoadingScope();
|
|
165
168
|
const loading = scoped || loadingGiven;
|
|
166
169
|
const gridColumns =
|
|
167
|
-
filterColumns ??
|
|
170
|
+
filterColumns ??
|
|
171
|
+
getDeviceFilterColumns(deviceFilters, { directoryColumns }).filter(column => !hideFilters?.includes(column.key));
|
|
168
172
|
|
|
169
173
|
const list = (
|
|
170
174
|
<>
|
|
171
|
-
<DevicesFilterToolbar
|
|
175
|
+
<DevicesFilterToolbar
|
|
176
|
+
{...toolbar}
|
|
177
|
+
// `devices(search)` also matches the primary user's name once directories are in play (Figma 865:55733).
|
|
178
|
+
searchPlaceholder={directoryColumns ? 'Search for Devices and Users' : undefined}
|
|
179
|
+
isLoading={loading}
|
|
180
|
+
/>
|
|
172
181
|
{viewMode === 'table' ? (
|
|
173
182
|
<DevicesTableBody
|
|
174
183
|
devices={devices}
|
|
@@ -187,6 +196,7 @@ export function DevicesPanelView<T extends DeviceRow = DeviceRow>({
|
|
|
187
196
|
onColumnFiltersChange={onColumnFiltersChange}
|
|
188
197
|
actionsColumn={actionsColumn}
|
|
189
198
|
hideColumns={hideColumns}
|
|
199
|
+
directoryColumns={directoryColumns}
|
|
190
200
|
disableColumnFilters={hideFilters}
|
|
191
201
|
totalCount={loading ? undefined : totalCount}
|
|
192
202
|
footerSlot={
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from '../../ui';
|
|
24
24
|
import { DATA_TABLE_SKELETON_ROWS } from '../../ui/data-table/utils';
|
|
25
25
|
import { OSTypeBadge } from '../os-type-badge';
|
|
26
|
+
import { directorySourceLabel, directoryUserLines, directoryUserLinkLabel } from './device-directory';
|
|
26
27
|
import { getDeviceName } from './device-name';
|
|
27
28
|
import { getDeviceStatusConfig } from './device-status';
|
|
28
29
|
import { DEFAULT_VISIBLE_STATUSES } from './device-statuses';
|
|
@@ -129,6 +130,8 @@ export interface DevicesTableBodyProps<T extends DeviceRow = DeviceRow> {
|
|
|
129
130
|
actionsColumn?: ColumnDef<T>;
|
|
130
131
|
/** Column ids to drop from the base table columns (e.g. ['organization'] when scoped to a single org). */
|
|
131
132
|
hideColumns?: string[];
|
|
133
|
+
/** Adds the USER and SOURCES columns, for a host whose tenant has directories. Default: neither. */
|
|
134
|
+
directoryColumns?: boolean;
|
|
132
135
|
/** Column ids whose header filter is dropped while the column stays visible (e.g. ['status'] on the archive page). */
|
|
133
136
|
disableColumnFilters?: string[];
|
|
134
137
|
/** The facet query is still in flight, see `getDeviceTableColumns`. */
|
|
@@ -168,6 +171,7 @@ export function DevicesTableBody<T extends DeviceRow = DeviceRow>({
|
|
|
168
171
|
onColumnFiltersChange,
|
|
169
172
|
actionsColumn,
|
|
170
173
|
hideColumns,
|
|
174
|
+
directoryColumns = false,
|
|
171
175
|
disableColumnFilters,
|
|
172
176
|
filtersPending,
|
|
173
177
|
totalCount,
|
|
@@ -177,12 +181,16 @@ export function DevicesTableBody<T extends DeviceRow = DeviceRow>({
|
|
|
177
181
|
const columns = useMemo<ColumnDef<T>[]>(() => {
|
|
178
182
|
const hidden = new Set(hideColumns ?? []);
|
|
179
183
|
const unfiltered = new Set(disableColumnFilters ?? []);
|
|
180
|
-
const base = getDeviceTableColumns<T>(deviceFilters ?? null, filtersPending
|
|
184
|
+
const base = getDeviceTableColumns<T>(deviceFilters ?? null, filtersPending, {
|
|
185
|
+
directoryColumns,
|
|
186
|
+
// Without the customer column there is room for the sources from where the customer's column would have started.
|
|
187
|
+
roomyDirectory: hidden.has('organization'),
|
|
188
|
+
})
|
|
181
189
|
.filter(c => !c.id || !hidden.has(c.id))
|
|
182
190
|
.map(c => (c.id && unfiltered.has(c.id) ? { ...c, meta: { ...c.meta, filter: undefined } } : c));
|
|
183
191
|
const open = getDeviceOpenColumn<T>();
|
|
184
192
|
return actionsColumn ? [...base, actionsColumn, open] : [...base, open];
|
|
185
|
-
}, [deviceFilters, filtersPending, actionsColumn, hideColumns, disableColumnFilters]);
|
|
193
|
+
}, [deviceFilters, filtersPending, actionsColumn, hideColumns, directoryColumns, disableColumnFilters]);
|
|
186
194
|
|
|
187
195
|
// While loading the table holds one page of empty rows, drawn with the real cells: every part of a row draws its
|
|
188
196
|
// own loading form (`DataTable.Body`'s `loadingCells`), so a loading row is the loaded row's own layout.
|
|
@@ -257,11 +265,43 @@ function DeviceLastSeen({ lastSeen }: { lastSeen: string | undefined }) {
|
|
|
257
265
|
);
|
|
258
266
|
}
|
|
259
267
|
|
|
268
|
+
/** The name over its role, as the USER column reads it (Figma 865:55733); the empty mark when nobody is tied. */
|
|
269
|
+
function DirectoryUserCell({ device }: { device: DeviceRow }) {
|
|
270
|
+
const loading = useLoadingScope();
|
|
271
|
+
if (loading && !device.directoryUserLink) {
|
|
272
|
+
return <TextLineSkeleton type="h4" className="w-3/4" />;
|
|
273
|
+
}
|
|
274
|
+
const { name, detail } = directoryUserLines(device);
|
|
275
|
+
return (
|
|
276
|
+
<div className="flex min-w-0 flex-col justify-center">
|
|
277
|
+
<TruncateText>{name}</TruncateText>
|
|
278
|
+
{detail && (
|
|
279
|
+
<TruncateText variant="h6" tone="secondary">
|
|
280
|
+
{detail}
|
|
281
|
+
</TruncateText>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The directories that know the device, as tags (Figma 187:61663); nothing when none does. */
|
|
288
|
+
function DirectorySourcesCell({ sources }: { sources: readonly string[] | undefined }) {
|
|
289
|
+
return (
|
|
290
|
+
<div data-loading-optional className="flex min-w-0 flex-wrap gap-[var(--spacing-system-xxs)]">
|
|
291
|
+
{(sources ?? []).map(source => (
|
|
292
|
+
<Tag key={source} label={directorySourceLabel(source)} variant="grey" />
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
260
298
|
export interface DeviceFilterColumn {
|
|
261
299
|
key: string;
|
|
262
300
|
label: string;
|
|
263
301
|
filterable?: boolean;
|
|
264
302
|
filterOptions?: DeviceFilterOption[];
|
|
303
|
+
/** Filters in the filter window from `lg` until `xl` too: its column's header has no funnel there. */
|
|
304
|
+
inWindowLgToXl?: boolean;
|
|
265
305
|
}
|
|
266
306
|
|
|
267
307
|
/** A device filter dropdown entry: `meta.filter.options` plus the facet count. */
|
|
@@ -277,6 +317,8 @@ function buildDeviceFilterOptions(deviceFilters?: DeviceFilters | null): {
|
|
|
277
317
|
status: DeviceFilterOption[];
|
|
278
318
|
os: DeviceFilterOption[];
|
|
279
319
|
organization: DeviceFilterOption[];
|
|
320
|
+
user: DeviceFilterOption[];
|
|
321
|
+
sources: DeviceFilterOption[];
|
|
280
322
|
} {
|
|
281
323
|
// Show only DEFAULT_VISIBLE_STATUSES (DELETED and legacy ARCHIVED live on /devices/archive)
|
|
282
324
|
const status = (deviceFilters?.statuses ?? [])
|
|
@@ -294,19 +336,56 @@ function buildDeviceFilterOptions(deviceFilters?: DeviceFilters | null): {
|
|
|
294
336
|
})),
|
|
295
337
|
);
|
|
296
338
|
|
|
297
|
-
|
|
339
|
+
const user = (deviceFilters?.userLinks ?? []).map(link => ({
|
|
340
|
+
id: link.value,
|
|
341
|
+
label: directoryUserLinkLabel(link.value),
|
|
342
|
+
value: link.value,
|
|
343
|
+
count: link.count,
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
const sources = (deviceFilters?.directorySources ?? []).map(source => ({
|
|
347
|
+
id: source.value,
|
|
348
|
+
label: directorySourceLabel(source.value),
|
|
349
|
+
value: source.value,
|
|
350
|
+
count: source.count,
|
|
351
|
+
}));
|
|
352
|
+
|
|
353
|
+
return { status, os, organization, user, sources };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** The directory columns of a device list: the table and its filter window take the same ones. */
|
|
357
|
+
interface DeviceDirectoryColumnOptions {
|
|
358
|
+
directoryColumns?: boolean;
|
|
359
|
+
roomyDirectory?: boolean;
|
|
298
360
|
}
|
|
299
361
|
|
|
300
362
|
// Filter column metadata used by the external filter modal (useTagFilterModal).
|
|
301
363
|
// Kept separate from the table ColumnDef because DataTable doesn't carry the
|
|
302
364
|
// filter metadata that the external mobile filter modal needs.
|
|
303
|
-
|
|
365
|
+
// `roomyDirectory` is the table's own (`getDeviceTableColumns`): beside the customer column SOURCES shows only from
|
|
366
|
+
// `xl`, and the header has no funnel for it from `lg` until then, so the window carries it there.
|
|
367
|
+
export function getDeviceFilterColumns(
|
|
368
|
+
deviceFilters?: DeviceFilters | null,
|
|
369
|
+
{ directoryColumns = false, roomyDirectory = false }: DeviceDirectoryColumnOptions = {},
|
|
370
|
+
): DeviceFilterColumn[] {
|
|
304
371
|
const options = buildDeviceFilterOptions(deviceFilters);
|
|
305
372
|
return [
|
|
306
373
|
{
|
|
307
374
|
key: 'device',
|
|
308
375
|
label: 'DEVICE',
|
|
309
376
|
},
|
|
377
|
+
...(directoryColumns
|
|
378
|
+
? [
|
|
379
|
+
{ key: 'user', label: 'USER', filterable: true, filterOptions: options.user },
|
|
380
|
+
{
|
|
381
|
+
key: 'sources',
|
|
382
|
+
label: 'SOURCES',
|
|
383
|
+
filterable: true,
|
|
384
|
+
filterOptions: options.sources,
|
|
385
|
+
inWindowLgToXl: !roomyDirectory,
|
|
386
|
+
},
|
|
387
|
+
]
|
|
388
|
+
: []),
|
|
310
389
|
{ key: 'status', label: 'STATUS', filterable: true, filterOptions: options.status },
|
|
311
390
|
{ key: 'os', label: 'OS', filterable: true, filterOptions: options.os },
|
|
312
391
|
{ key: 'organization', label: 'CUSTOMER', filterable: true, filterOptions: options.organization },
|
|
@@ -318,17 +397,51 @@ export function getDeviceFilterColumns(deviceFilters?: DeviceFilters | null): De
|
|
|
318
397
|
* funnels drawn (inert) rather than growing them when it answers. Absent means
|
|
319
398
|
* these are the final options: an empty one then hides its funnel, which is what
|
|
320
399
|
* a list with no facet source of its own wants.
|
|
400
|
+
* @param options.directoryColumns Adds USER and SOURCES after DEVICE. Off by default: a picker or a tenant with no
|
|
401
|
+
* directories has neither.
|
|
402
|
+
* @param options.roomyDirectory No customer column beside them (one customer's list), so SOURCES shows from `lg`
|
|
403
|
+
* rather than `xl`.
|
|
321
404
|
*/
|
|
322
405
|
export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
323
406
|
deviceFilters?: DeviceFilters | null,
|
|
324
407
|
filtersPending?: boolean,
|
|
408
|
+
{ directoryColumns = false, roomyDirectory = false }: DeviceDirectoryColumnOptions = {},
|
|
325
409
|
): ColumnDef<T>[] {
|
|
326
410
|
const {
|
|
327
411
|
status: statusFilterOptions,
|
|
328
412
|
os: osFilterOptions,
|
|
329
413
|
organization: orgFilterOptions,
|
|
414
|
+
user: userFilterOptions,
|
|
415
|
+
sources: sourcesFilterOptions,
|
|
330
416
|
} = buildDeviceFilterOptions(deviceFilters);
|
|
331
417
|
|
|
418
|
+
const directory: ColumnDef<T>[] = directoryColumns
|
|
419
|
+
? [
|
|
420
|
+
{
|
|
421
|
+
id: 'user',
|
|
422
|
+
header: 'USER',
|
|
423
|
+
cell: ({ row }: { row: Row<T> }) => <DirectoryUserCell device={row.original} />,
|
|
424
|
+
enableSorting: false,
|
|
425
|
+
meta: {
|
|
426
|
+
width: 'min-w-0 content-lg:w-[15%]',
|
|
427
|
+
hideAt: 'lg',
|
|
428
|
+
filter: { options: userFilterOptions, pending: filtersPending },
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: 'sources',
|
|
433
|
+
header: 'SOURCES',
|
|
434
|
+
cell: ({ row }: { row: Row<T> }) => <DirectorySourcesCell sources={row.original.directorySources} />,
|
|
435
|
+
enableSorting: false,
|
|
436
|
+
meta: {
|
|
437
|
+
width: 'min-w-0 content-lg:w-[11%]',
|
|
438
|
+
hideAt: roomyDirectory ? 'lg' : 'xl',
|
|
439
|
+
filter: { options: sourcesFilterOptions, pending: filtersPending },
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
]
|
|
443
|
+
: [];
|
|
444
|
+
|
|
332
445
|
return [
|
|
333
446
|
{
|
|
334
447
|
accessorKey: 'device',
|
|
@@ -347,8 +460,10 @@ export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
|
347
460
|
</div>
|
|
348
461
|
);
|
|
349
462
|
},
|
|
350
|
-
|
|
463
|
+
// The name takes what the other columns leave: with two more of them, their shares below shrink to leave some.
|
|
464
|
+
meta: { width: directoryColumns ? 'flex-1 min-w-0' : 'flex-1 content-md:w-1/4' },
|
|
351
465
|
},
|
|
466
|
+
...directory,
|
|
352
467
|
{
|
|
353
468
|
accessorKey: 'status',
|
|
354
469
|
id: 'status',
|
|
@@ -366,7 +481,7 @@ export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
|
366
481
|
);
|
|
367
482
|
},
|
|
368
483
|
meta: {
|
|
369
|
-
width: 'w-auto shrink-0 content-md:w-1/5',
|
|
484
|
+
width: directoryColumns ? 'w-auto shrink-0 content-md:w-[14%]' : 'w-auto shrink-0 content-md:w-1/5',
|
|
370
485
|
// Declared unconditionally, empty options and all. `meta.filter` is what
|
|
371
486
|
// keeps a header cell visible below `lg`, so dropping it until the
|
|
372
487
|
// options query resolves made every header on a tablet disappear on
|
|
@@ -383,7 +498,7 @@ export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
|
383
498
|
<OSTypeBadge osType={row.original.osType} iconSize="w-4 h-4 content-md:w-6 content-md:h-6" />
|
|
384
499
|
),
|
|
385
500
|
meta: {
|
|
386
|
-
width: 'w-[200px] content-md:w-1/6',
|
|
501
|
+
width: directoryColumns ? 'w-[200px] content-md:w-[10%]' : 'w-[200px] content-md:w-1/6',
|
|
387
502
|
hideAt: 'md',
|
|
388
503
|
filter: { options: osFilterOptions, pending: filtersPending },
|
|
389
504
|
},
|
|
@@ -394,7 +509,7 @@ export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
|
|
|
394
509
|
header: 'CUSTOMER',
|
|
395
510
|
cell: ({ row }: { row: Row<T> }) => <OrganizationCell device={row.original} />,
|
|
396
511
|
meta: {
|
|
397
|
-
width: 'w-1/6',
|
|
512
|
+
width: directoryColumns ? 'w-[14%]' : 'w-1/6',
|
|
398
513
|
hideAt: 'lg',
|
|
399
514
|
filter: { options: orgFilterOptions, placement: 'bottom-end', pending: filtersPending },
|
|
400
515
|
},
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
export {
|
|
4
|
+
type DeviceDirectorySource,
|
|
5
|
+
type DeviceDirectoryUserLink,
|
|
6
|
+
directorySourceLabel,
|
|
7
|
+
directoryUserLines,
|
|
8
|
+
directoryUserLinkLabel,
|
|
9
|
+
} from './device-directory';
|
|
3
10
|
export { formatLastOnline } from './device-last-online';
|
|
4
11
|
export { type DeviceNameSource, getDeviceName, matchesDeviceName } from './device-name';
|
|
5
12
|
export {
|
|
@@ -34,6 +34,14 @@ export interface DeviceRow extends DeviceNameSource {
|
|
|
34
34
|
organizationImageUrl?: string | null;
|
|
35
35
|
organizationImageHash?: string | null;
|
|
36
36
|
tags?: DeviceTag[];
|
|
37
|
+
/** Who the device is tied to in a directory: PRIMARY, SHARED or NONE. The USER column reads it. */
|
|
38
|
+
directoryUserLink?: string;
|
|
39
|
+
/** The primary user's name; only PRIMARY has one. */
|
|
40
|
+
directoryUserName?: string | null;
|
|
41
|
+
/** How many users are tied to it: "N recent users" when SHARED. */
|
|
42
|
+
directoryUserCount?: number;
|
|
43
|
+
/** The directories that know the device besides the agent. The SOURCES column reads it. */
|
|
44
|
+
directorySources?: readonly string[];
|
|
37
45
|
/** The viewer's unread notifications about this device. Absent or 0: no indicator. */
|
|
38
46
|
unreadNotificationCount?: number;
|
|
39
47
|
}
|
|
@@ -63,6 +71,10 @@ export interface DeviceFilters {
|
|
|
63
71
|
organizationIds: DeviceFilterTag[];
|
|
64
72
|
tagKeys: TagFilterOption[];
|
|
65
73
|
filteredCount: number;
|
|
74
|
+
/** Behind the USER funnel; only hosts that draw the directory columns read it. */
|
|
75
|
+
userLinks?: DeviceFilterValue[];
|
|
76
|
+
/** Behind the SOURCES funnel; only hosts that draw the directory columns read it. */
|
|
77
|
+
directorySources?: DeviceFilterValue[];
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
/** What narrows a device list, in the backend's field names. */
|
|
@@ -73,4 +85,6 @@ export interface DeviceFilterInput {
|
|
|
73
85
|
organizationIds?: string[];
|
|
74
86
|
tagKeys?: string[];
|
|
75
87
|
tagValues?: string[];
|
|
88
|
+
userLinks?: string[];
|
|
89
|
+
directorySources?: string[];
|
|
76
90
|
}
|