@flamingo-stack/openframe-frontend-core 0.0.852 → 0.0.854
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/{chunk-G7GF4PWA.cjs → chunk-2IJMHBOK.cjs} +3 -3
- package/dist/{chunk-IC4IJJB2.cjs → chunk-3WXHPD6N.cjs} +55 -55
- package/dist/{chunk-UV45EPXN.cjs → chunk-4LTLEUY5.cjs} +5 -5
- package/dist/{chunk-BNHTBLAG.cjs → chunk-4O5N5CGK.cjs} +17 -17
- package/dist/{chunk-COWMXUHN.cjs → chunk-4SSJ3T3N.cjs} +3 -1
- package/dist/{chunk-IUDBG3HJ.cjs → chunk-5HMENXSR.cjs} +3 -3
- package/dist/{chunk-COJG2ATY.cjs → chunk-5OENPMRU.cjs} +43 -43
- package/dist/{chunk-IO6NN3SL.cjs → chunk-5TXE7BXT.cjs} +46 -46
- package/dist/{chunk-EIACEY2L.cjs → chunk-63ZV3UGG.cjs} +10 -10
- package/dist/{chunk-HTYJUMFB.cjs → chunk-6DCUGIGU.cjs} +2 -2
- package/dist/{chunk-GEZ3UXCA.cjs → chunk-6XU3RCK3.cjs} +18 -18
- package/dist/{chunk-3ZQESOPJ.cjs → chunk-7J5NSWJG.cjs} +87 -87
- package/dist/{chunk-MXY2OGEV.cjs → chunk-7MOBD44H.cjs} +28 -28
- package/dist/{chunk-U767P4JU.cjs → chunk-ACPOJE32.cjs} +9 -9
- package/dist/{chunk-CHGIACIP.cjs → chunk-BAA3YCLT.cjs} +72 -72
- package/dist/{chunk-VUHHLRAY.cjs → chunk-D2XGRWPW.cjs} +73 -73
- package/dist/{chunk-5RMBLOJ5.cjs → chunk-EIHMPQRQ.cjs} +9 -9
- package/dist/{chunk-3SJVF3HW.cjs → chunk-ETQNMR2K.cjs} +5 -5
- package/dist/{chunk-2W22LY3W.cjs → chunk-FBANHIAG.cjs} +9 -9
- package/dist/{chunk-P63CRPWI.cjs → chunk-FJXSKMQJ.cjs} +20 -20
- package/dist/{chunk-QOW2GP3V.cjs → chunk-GVHCQRUE.cjs} +21 -21
- package/dist/{chunk-KNN2GCMW.cjs → chunk-H3GBPYPS.cjs} +18 -18
- package/dist/{chunk-FW7RKWPY.cjs → chunk-JKBNK5U6.cjs} +3 -3
- package/dist/{chunk-W7OFAROY.cjs → chunk-KA3L6D5N.cjs} +3 -3
- package/dist/{chunk-USPUZO53.cjs → chunk-LQDAB5V5.cjs} +4 -4
- package/dist/{chunk-KHOB3TES.cjs → chunk-ONYANAMA.cjs} +4 -4
- package/dist/{chunk-D3H6AI7O.cjs → chunk-PRJZMZ3X.cjs} +1182 -746
- package/dist/chunk-QD727QFI.cjs +62 -0
- package/dist/{chunk-X3WIOMEE.cjs → chunk-RMI6O3AE.cjs} +32 -32
- package/dist/{chunk-MFDRCTM3.cjs → chunk-S5P6CF5R.cjs} +47 -47
- package/dist/{chunk-477P6AQH.cjs → chunk-TMPLHUUU.cjs} +6 -6
- package/dist/{chunk-UO2EJGRJ.cjs → chunk-UPQ4MAR7.cjs} +14 -14
- package/dist/{chunk-LFEURIZN.cjs → chunk-VMAOZA2V.cjs} +215 -215
- package/dist/{chunk-NMWLXHFH.cjs → chunk-YY3WP2YQ.cjs} +30 -30
- package/dist/{chunk-WFAZMASF.cjs → chunk-ZDPPNHA2.cjs} +3 -3
- package/dist/{cloud-tenants-O33HCUYB.cjs → cloud-tenants-5RNK4ZAT.cjs} +19 -19
- package/dist/components/case-studies/index.cjs +33 -33
- package/dist/components/chat/index.cjs +13 -13
- package/dist/components/contact/index.cjs +20 -20
- package/dist/components/docs/index.cjs +23 -23
- package/dist/components/embeds/index.cjs +20 -20
- package/dist/components/faq/index.cjs +21 -21
- 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 +33 -19
- package/dist/components/help-center-pages/index.cjs +120 -120
- package/dist/components/help-center-pages/trust-center-page.d.ts +1 -1
- package/dist/components/index.cjs +96 -76
- package/dist/components/meeting-scheduler/index.cjs +82 -82
- package/dist/components/navigation/app-layout.js +2 -2
- package/dist/components/navigation/index.cjs +22 -22
- package/dist/components/onboarding-guides/index.cjs +22 -22
- package/dist/components/platform/tool-badge-mark.d.ts +1 -1
- package/dist/components/product-demo/index.cjs +30 -30
- package/dist/components/related-content/index.cjs +8 -8
- package/dist/components/tickets/index.cjs +24 -24
- package/dist/components/ui/data-attribution.d.ts +8 -3
- package/dist/components/ui/data-attribution.d.ts.map +1 -1
- package/dist/components/ui/data-attribution.js +17 -6
- 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/index.cjs +25 -19
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/{devices-2RGGMC2B.cjs → devices-44O5PZOW.cjs} +20 -20
- package/dist/{embeddable-chat-SJEEJI4N.cjs → embeddable-chat-W3CLMGC3.cjs} +90 -90
- package/dist/hooks/index.cjs +11 -7
- 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/index.cjs +44 -24
- package/dist/{knowledge-5TJIXVMD.cjs → knowledge-UUU3WVO3.cjs} +20 -20
- package/dist/{logs-HGQOTRKZ.cjs → logs-DFHNSODA.cjs} +26 -26
- package/dist/{policies-GWFSLVSM.cjs → policies-RGFUGDBP.cjs} +23 -23
- package/dist/{remote-session-SG44CNOK.cjs → remote-session-U3H77HKR.cjs} +10 -10
- package/dist/{scripts-PGJNAZF4.cjs → scripts-KOVO5DHW.cjs} +26 -26
- package/dist/{software-update-KKLSYHDI.cjs → software-update-4EKHVV5W.cjs} +23 -23
- package/dist/{tickets-board-SQY2Y6QV.cjs → tickets-board-YIFOVMDX.cjs} +10 -10
- package/dist/types/data-sync.d.ts +4 -1
- package/dist/types/data-sync.d.ts.map +1 -1
- package/dist/types/data-sync.js +4 -1
- package/dist/types/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/package.json +1 -1
- 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/help-center-pages/trust-center-page.tsx +1 -1
- package/src/components/navigation/app-layout.tsx +4 -4
- package/src/components/platform/tool-badge-mark.ts +1 -1
- package/src/components/ui/data-attribution.tsx +20 -8
- package/src/components/ui/date-picker.tsx +21 -3
- package/src/components/ui/index.ts +2 -0
- package/src/hooks/ui/use-content-breakpoint.ts +21 -2
- package/src/hooks/ui/use-media-query.ts +1 -0
- package/src/types/data-sync.ts +4 -1
- package/src/utils/presentation-for.ts +10 -0
- package/dist/chunk-UAI4VMEF.cjs +0 -46
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export interface
|
|
1
|
+
/** A customer a tenant's directory serves. */
|
|
2
|
+
export interface TenantCustomer {
|
|
3
|
+
name: string;
|
|
4
|
+
/** A URL the browser can load as is. */
|
|
5
|
+
imageUrl?: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** A directory's seats over its paid SKUs. */
|
|
9
|
+
export interface TenantLicenseSummary {
|
|
10
|
+
assigned: number;
|
|
11
|
+
/** Absent when the provider does not report purchased seats (Google Workspace). */
|
|
12
|
+
purchased?: number | null;
|
|
13
|
+
/** Purchased minus assigned; negative when the provider counts suspended seats as assigned. */
|
|
14
|
+
unused?: number | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TenantRowBase {
|
|
3
18
|
id: string;
|
|
4
19
|
/** The tenant's own name in its directory. */
|
|
5
20
|
name: string;
|
|
6
21
|
/** The directory provider (`MICROSOFT_365`, `GOOGLE_WORKSPACE`); an unknown value keeps its raw name. */
|
|
7
22
|
provider: string;
|
|
8
23
|
domain?: string | null;
|
|
9
|
-
/** The customer the tenant belongs to. */
|
|
10
|
-
customer: {
|
|
11
|
-
name: string;
|
|
12
|
-
/** A URL the browser can load as is. */
|
|
13
|
-
imageUrl?: string | null;
|
|
14
|
-
};
|
|
15
24
|
/** Users read from the directory; absent before the first read. */
|
|
16
25
|
userCount?: number | null;
|
|
17
26
|
/** What the last access probe found (`WRITE_ENABLED`, `DISCONNECTED`, ...). */
|
|
@@ -19,3 +28,24 @@ export interface TenantRow {
|
|
|
19
28
|
/** When the directory was last read; absent when it never was. */
|
|
20
29
|
lastReadAt?: string | null;
|
|
21
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A tenant of the single-customer list, `TenantsTableView`'s default variant, which accepts no other row: a
|
|
34
|
+
* multi-customer host that forgets `variant` does not compile. Deleted with that variant.
|
|
35
|
+
*/
|
|
36
|
+
export interface TenantRow extends TenantRowBase {
|
|
37
|
+
/** The customer the tenant belongs to. */
|
|
38
|
+
customer: TenantCustomer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A tenant of the multi-customer list (`variant="multi-customer"`): a directory serving any number of customers. */
|
|
42
|
+
export interface MultiCustomerTenantRow extends TenantRowBase {
|
|
43
|
+
/** The customers the directory serves, in the order the host shows them; empty while it serves none. */
|
|
44
|
+
customers: readonly TenantCustomer[];
|
|
45
|
+
/** Of the users read, the users no customer's rule claims; absent before the first read. */
|
|
46
|
+
unattributedUserCount?: number | null;
|
|
47
|
+
/** A person paused the sync: the row says so instead of when it was last read. */
|
|
48
|
+
syncPaused?: boolean;
|
|
49
|
+
/** Absent until the licence numbers have been read. */
|
|
50
|
+
licenses?: TenantLicenseSummary | null;
|
|
51
|
+
}
|
|
@@ -9,22 +9,25 @@ export interface TenantsTableColumn {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Column layout of the tenants list (Figma
|
|
13
|
-
*
|
|
12
|
+
* Column layout of the tenants list (Figma 3025-3536: Tenant, Customers, Licences, Access, open). A tablet drops the
|
|
13
|
+
* licence cells and keeps the funnel (3032-4579); a phone keeps the tenant, the access tag and open (3033-9343).
|
|
14
|
+
* Data-only on purpose: the live table and the route skeleton
|
|
14
15
|
* read the SAME widths, so nothing shifts when the rows arrive, and a skeleton
|
|
15
16
|
* never imports the table's cell renderers.
|
|
16
17
|
*/
|
|
17
18
|
export const TENANT_COLUMNS = {
|
|
18
19
|
tenant: { id: 'tenant', header: 'Tenant', width: 'flex-1 min-w-0' },
|
|
19
|
-
customer: { id: 'customer', header: 'Customers', width: 'w-[220px] content-lg:w-[
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
customer: { id: 'customer', header: 'Customers', width: 'w-[220px] content-lg:w-[280px]', hideAt: 'md' },
|
|
21
|
+
licenses: { id: 'licenses', header: 'Licences', width: 'w-[150px]', hideAt: 'lg' },
|
|
22
|
+
access: { id: 'access', header: 'Access', width: 'flex-none content-md:w-[200px] content-lg:w-[216px]' },
|
|
23
|
+
open: { id: 'open', width: 'w-12 shrink-0 flex-none', align: 'right' },
|
|
22
24
|
} satisfies Record<string, TenantsTableColumn>;
|
|
23
25
|
|
|
24
26
|
/** Render order of the standalone list. */
|
|
25
27
|
export const TENANTS_TABLE_COLUMNS: readonly TenantsTableColumn[] = [
|
|
26
28
|
TENANT_COLUMNS.tenant,
|
|
27
29
|
TENANT_COLUMNS.customer,
|
|
30
|
+
TENANT_COLUMNS.licenses,
|
|
28
31
|
TENANT_COLUMNS.access,
|
|
29
32
|
TENANT_COLUMNS.open,
|
|
30
33
|
];
|
|
@@ -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={
|