@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.
Files changed (163) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-G7GF4PWA.cjs → chunk-2IJMHBOK.cjs} +3 -3
  3. package/dist/{chunk-IC4IJJB2.cjs → chunk-3WXHPD6N.cjs} +55 -55
  4. package/dist/{chunk-UV45EPXN.cjs → chunk-4LTLEUY5.cjs} +5 -5
  5. package/dist/{chunk-BNHTBLAG.cjs → chunk-4O5N5CGK.cjs} +17 -17
  6. package/dist/{chunk-COWMXUHN.cjs → chunk-4SSJ3T3N.cjs} +3 -1
  7. package/dist/{chunk-IUDBG3HJ.cjs → chunk-5HMENXSR.cjs} +3 -3
  8. package/dist/{chunk-COJG2ATY.cjs → chunk-5OENPMRU.cjs} +43 -43
  9. package/dist/{chunk-IO6NN3SL.cjs → chunk-5TXE7BXT.cjs} +46 -46
  10. package/dist/{chunk-EIACEY2L.cjs → chunk-63ZV3UGG.cjs} +10 -10
  11. package/dist/{chunk-HTYJUMFB.cjs → chunk-6DCUGIGU.cjs} +2 -2
  12. package/dist/{chunk-GEZ3UXCA.cjs → chunk-6XU3RCK3.cjs} +18 -18
  13. package/dist/{chunk-3ZQESOPJ.cjs → chunk-7J5NSWJG.cjs} +87 -87
  14. package/dist/{chunk-MXY2OGEV.cjs → chunk-7MOBD44H.cjs} +28 -28
  15. package/dist/{chunk-U767P4JU.cjs → chunk-ACPOJE32.cjs} +9 -9
  16. package/dist/{chunk-CHGIACIP.cjs → chunk-BAA3YCLT.cjs} +72 -72
  17. package/dist/{chunk-VUHHLRAY.cjs → chunk-D2XGRWPW.cjs} +73 -73
  18. package/dist/{chunk-5RMBLOJ5.cjs → chunk-EIHMPQRQ.cjs} +9 -9
  19. package/dist/{chunk-3SJVF3HW.cjs → chunk-ETQNMR2K.cjs} +5 -5
  20. package/dist/{chunk-2W22LY3W.cjs → chunk-FBANHIAG.cjs} +9 -9
  21. package/dist/{chunk-P63CRPWI.cjs → chunk-FJXSKMQJ.cjs} +20 -20
  22. package/dist/{chunk-QOW2GP3V.cjs → chunk-GVHCQRUE.cjs} +21 -21
  23. package/dist/{chunk-KNN2GCMW.cjs → chunk-H3GBPYPS.cjs} +18 -18
  24. package/dist/{chunk-FW7RKWPY.cjs → chunk-JKBNK5U6.cjs} +3 -3
  25. package/dist/{chunk-W7OFAROY.cjs → chunk-KA3L6D5N.cjs} +3 -3
  26. package/dist/{chunk-USPUZO53.cjs → chunk-LQDAB5V5.cjs} +4 -4
  27. package/dist/{chunk-KHOB3TES.cjs → chunk-ONYANAMA.cjs} +4 -4
  28. package/dist/{chunk-D3H6AI7O.cjs → chunk-PRJZMZ3X.cjs} +1182 -746
  29. package/dist/chunk-QD727QFI.cjs +62 -0
  30. package/dist/{chunk-X3WIOMEE.cjs → chunk-RMI6O3AE.cjs} +32 -32
  31. package/dist/{chunk-MFDRCTM3.cjs → chunk-S5P6CF5R.cjs} +47 -47
  32. package/dist/{chunk-477P6AQH.cjs → chunk-TMPLHUUU.cjs} +6 -6
  33. package/dist/{chunk-UO2EJGRJ.cjs → chunk-UPQ4MAR7.cjs} +14 -14
  34. package/dist/{chunk-LFEURIZN.cjs → chunk-VMAOZA2V.cjs} +215 -215
  35. package/dist/{chunk-NMWLXHFH.cjs → chunk-YY3WP2YQ.cjs} +30 -30
  36. package/dist/{chunk-WFAZMASF.cjs → chunk-ZDPPNHA2.cjs} +3 -3
  37. package/dist/{cloud-tenants-O33HCUYB.cjs → cloud-tenants-5RNK4ZAT.cjs} +19 -19
  38. package/dist/components/case-studies/index.cjs +33 -33
  39. package/dist/components/chat/index.cjs +13 -13
  40. package/dist/components/contact/index.cjs +20 -20
  41. package/dist/components/docs/index.cjs +23 -23
  42. package/dist/components/embeds/index.cjs +20 -20
  43. package/dist/components/faq/index.cjs +21 -21
  44. package/dist/components/features/cloud-tenants/index.d.ts +7 -5
  45. package/dist/components/features/cloud-tenants/index.d.ts.map +1 -1
  46. package/dist/components/features/cloud-tenants/index.js +8 -1
  47. package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts +26 -0
  48. package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts.map +1 -0
  49. package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.js +176 -0
  50. package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts +4 -2
  51. package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts.map +1 -1
  52. package/dist/components/features/cloud-tenants/tenant-access-cell.js +3 -2
  53. package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts +12 -5
  54. package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts.map +1 -1
  55. package/dist/components/features/cloud-tenants/tenant-customer-cell.js +38 -7
  56. package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts +7 -0
  57. package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts.map +1 -0
  58. package/dist/components/features/cloud-tenants/tenant-licenses-cell.js +17 -0
  59. package/dist/components/features/cloud-tenants/tenant-presentation.d.ts +19 -0
  60. package/dist/components/features/cloud-tenants/tenant-presentation.d.ts.map +1 -1
  61. package/dist/components/features/cloud-tenants/tenant-presentation.js +24 -3
  62. package/dist/components/features/cloud-tenants/tenant-row.d.ts +35 -8
  63. package/dist/components/features/cloud-tenants/tenant-row.d.ts.map +1 -1
  64. package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts +9 -3
  65. package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts.map +1 -1
  66. package/dist/components/features/cloud-tenants/tenants-table-columns.js +5 -3
  67. package/dist/components/features/cloud-tenants/tenants-table-view.d.ts +27 -6
  68. package/dist/components/features/cloud-tenants/tenants-table-view.d.ts.map +1 -1
  69. package/dist/components/features/cloud-tenants/tenants-table-view.js +75 -17
  70. package/dist/components/features/devices/device-directory.d.ts +18 -0
  71. package/dist/components/features/devices/device-directory.d.ts.map +1 -0
  72. package/dist/components/features/devices/device-directory.js +35 -0
  73. package/dist/components/features/devices/devices-filter-toolbar.d.ts +3 -1
  74. package/dist/components/features/devices/devices-filter-toolbar.d.ts.map +1 -1
  75. package/dist/components/features/devices/devices-filter-toolbar.js +2 -1
  76. package/dist/components/features/devices/devices-panel-view.d.ts +3 -1
  77. package/dist/components/features/devices/devices-panel-view.d.ts.map +1 -1
  78. package/dist/components/features/devices/devices-panel-view.js +11 -2
  79. package/dist/components/features/devices/devices-table-columns.d.ts +17 -3
  80. package/dist/components/features/devices/devices-table-columns.d.ts.map +1 -1
  81. package/dist/components/features/devices/devices-table-columns.js +80 -10
  82. package/dist/components/features/devices/index.d.ts +1 -0
  83. package/dist/components/features/devices/index.d.ts.map +1 -1
  84. package/dist/components/features/devices/index.js +8 -0
  85. package/dist/components/features/devices/types.d.ts +14 -0
  86. package/dist/components/features/devices/types.d.ts.map +1 -1
  87. package/dist/components/features/devices/use-tag-filter-modal.d.ts +1 -0
  88. package/dist/components/features/devices/use-tag-filter-modal.d.ts.map +1 -1
  89. package/dist/components/features/devices/use-tag-filter-modal.js +13 -8
  90. package/dist/components/features/index.cjs +33 -19
  91. package/dist/components/help-center-pages/index.cjs +120 -120
  92. package/dist/components/help-center-pages/trust-center-page.d.ts +1 -1
  93. package/dist/components/index.cjs +96 -76
  94. package/dist/components/meeting-scheduler/index.cjs +82 -82
  95. package/dist/components/navigation/app-layout.js +2 -2
  96. package/dist/components/navigation/index.cjs +22 -22
  97. package/dist/components/onboarding-guides/index.cjs +22 -22
  98. package/dist/components/platform/tool-badge-mark.d.ts +1 -1
  99. package/dist/components/product-demo/index.cjs +30 -30
  100. package/dist/components/related-content/index.cjs +8 -8
  101. package/dist/components/tickets/index.cjs +24 -24
  102. package/dist/components/ui/data-attribution.d.ts +8 -3
  103. package/dist/components/ui/data-attribution.d.ts.map +1 -1
  104. package/dist/components/ui/data-attribution.js +17 -6
  105. package/dist/components/ui/date-picker.d.ts +2 -0
  106. package/dist/components/ui/date-picker.d.ts.map +1 -1
  107. package/dist/components/ui/date-picker.js +19 -6
  108. package/dist/components/ui/index.cjs +25 -19
  109. package/dist/components/ui/index.d.ts +1 -0
  110. package/dist/components/ui/index.d.ts.map +1 -1
  111. package/dist/components/ui/index.js +1 -0
  112. package/dist/{devices-2RGGMC2B.cjs → devices-44O5PZOW.cjs} +20 -20
  113. package/dist/{embeddable-chat-SJEEJI4N.cjs → embeddable-chat-W3CLMGC3.cjs} +90 -90
  114. package/dist/hooks/index.cjs +11 -7
  115. package/dist/hooks/ui/use-content-breakpoint.d.ts +8 -0
  116. package/dist/hooks/ui/use-content-breakpoint.d.ts.map +1 -1
  117. package/dist/hooks/ui/use-content-breakpoint.js +17 -3
  118. package/dist/hooks/ui/use-media-query.d.ts +1 -0
  119. package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
  120. package/dist/hooks/ui/use-media-query.js +3 -1
  121. package/dist/index.cjs +44 -24
  122. package/dist/{knowledge-5TJIXVMD.cjs → knowledge-UUU3WVO3.cjs} +20 -20
  123. package/dist/{logs-HGQOTRKZ.cjs → logs-DFHNSODA.cjs} +26 -26
  124. package/dist/{policies-GWFSLVSM.cjs → policies-RGFUGDBP.cjs} +23 -23
  125. package/dist/{remote-session-SG44CNOK.cjs → remote-session-U3H77HKR.cjs} +10 -10
  126. package/dist/{scripts-PGJNAZF4.cjs → scripts-KOVO5DHW.cjs} +26 -26
  127. package/dist/{software-update-KKLSYHDI.cjs → software-update-4EKHVV5W.cjs} +23 -23
  128. package/dist/{tickets-board-SQY2Y6QV.cjs → tickets-board-YIFOVMDX.cjs} +10 -10
  129. package/dist/types/data-sync.d.ts +4 -1
  130. package/dist/types/data-sync.d.ts.map +1 -1
  131. package/dist/types/data-sync.js +4 -1
  132. package/dist/types/index.cjs +4 -1
  133. package/dist/utils/presentation-for.d.ts +6 -0
  134. package/dist/utils/presentation-for.d.ts.map +1 -0
  135. package/dist/utils/presentation-for.js +7 -0
  136. package/package.json +1 -1
  137. package/src/components/features/cloud-tenants/index.ts +8 -3
  138. package/src/components/features/cloud-tenants/single-customer-tenants-table-view.tsx +234 -0
  139. package/src/components/features/cloud-tenants/tenant-access-cell.tsx +12 -5
  140. package/src/components/features/cloud-tenants/tenant-customer-cell.tsx +48 -12
  141. package/src/components/features/cloud-tenants/tenant-licenses-cell.tsx +28 -0
  142. package/src/components/features/cloud-tenants/tenant-presentation.ts +46 -4
  143. package/src/components/features/cloud-tenants/tenant-row.ts +38 -8
  144. package/src/components/features/cloud-tenants/tenants-table-columns.ts +8 -5
  145. package/src/components/features/cloud-tenants/tenants-table-view.tsx +123 -28
  146. package/src/components/features/devices/device-directory.ts +55 -0
  147. package/src/components/features/devices/devices-filter-toolbar.tsx +4 -1
  148. package/src/components/features/devices/devices-panel-view.tsx +12 -2
  149. package/src/components/features/devices/devices-table-columns.tsx +123 -8
  150. package/src/components/features/devices/index.ts +7 -0
  151. package/src/components/features/devices/types.ts +14 -0
  152. package/src/components/features/devices/use-tag-filter-modal.ts +32 -14
  153. package/src/components/help-center-pages/trust-center-page.tsx +1 -1
  154. package/src/components/navigation/app-layout.tsx +4 -4
  155. package/src/components/platform/tool-badge-mark.ts +1 -1
  156. package/src/components/ui/data-attribution.tsx +20 -8
  157. package/src/components/ui/date-picker.tsx +21 -3
  158. package/src/components/ui/index.ts +2 -0
  159. package/src/hooks/ui/use-content-breakpoint.ts +21 -2
  160. package/src/hooks/ui/use-media-query.ts +1 -0
  161. package/src/types/data-sync.ts +4 -1
  162. package/src/utils/presentation-for.ts +10 -0
  163. package/dist/chunk-UAI4VMEF.cjs +0 -46
@@ -1,17 +1,26 @@
1
- /** One connected directory tenant, as the tenants list shows it. */
2
- export interface TenantRow {
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 1699-8249: Tenant, Customers,
13
- * Access, open). Data-only on purpose: the live table and the route skeleton
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-[340px]', hideAt: 'md' },
20
- access: { id: 'access', header: 'Access', width: 'w-[200px] content-lg:w-[216px]' },
21
- open: { id: 'open', width: 'w-12 shrink-0 flex-none', hideAt: 'md', align: 'right' },
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 { type ColumnDef, DataTable, dataTableLoadingRows, type Row, useDataTable } from '../../ui/data-table';
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 type { TenantRow } from './tenant-row';
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: TenantRow) => row.id;
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): TenantRow => ({
34
+ const loadingTenant = (id: string): MultiCustomerTenantRow => ({
22
35
  id,
23
36
  name: '',
24
37
  provider: '',
25
- customer: { name: '' },
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
- export interface TenantsTableViewProps {
53
- rows: TenantRow[];
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: TenantRow) => string;
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, customer, access state and a way into each tenant's details. */
75
- export function TenantsTableView({
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
- }: TenantsTableViewProps) {
88
- const columns = useMemo<ColumnDef<TenantRow>[]>(
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<TenantRow> }) => (
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<TenantRow> }) => (
157
+ cell: ({ row }: { row: Row<MultiCustomerTenantRow> }) => (
103
158
  <TenantCustomerCell
104
- name={row.original.customer.name}
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<TenantRow> }) => (
116
- <TenantAccessCell accessState={row.original.accessState} lastReadAt={row.original.lastReadAt} now={now} />
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<TenantRow> }) => (
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
- [getHref, now],
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 table = useDataTable<TenantRow>({ data: loadingRows ?? rows, columns, getRowId, enableSorting: false });
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: `Nothing matches "${search}". Try a different name, domain or customer.`,
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="Search for Devices"
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 ?? getDeviceFilterColumns(deviceFilters).filter(column => !hideFilters?.includes(column.key));
170
+ filterColumns ??
171
+ getDeviceFilterColumns(deviceFilters, { directoryColumns }).filter(column => !hideFilters?.includes(column.key));
168
172
 
169
173
  const list = (
170
174
  <>
171
- <DevicesFilterToolbar {...toolbar} isLoading={loading} />
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={