@flamingo-stack/openframe-frontend-core 0.0.853 → 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-BNG6JOGZ.cjs → chunk-6XU3RCK3.cjs} +18 -18
- package/dist/{chunk-KEV6F3QN.cjs → chunk-7J5NSWJG.cjs} +87 -87
- package/dist/{chunk-53QPG7SJ.cjs → chunk-7MOBD44H.cjs} +28 -28
- package/dist/{chunk-U767P4JU.cjs → chunk-ACPOJE32.cjs} +9 -9
- package/dist/{chunk-E4MFQKPC.cjs → chunk-BAA3YCLT.cjs} +72 -72
- package/dist/{chunk-7RZHSBNX.cjs → chunk-D2XGRWPW.cjs} +73 -73
- package/dist/{chunk-5RMBLOJ5.cjs → chunk-EIHMPQRQ.cjs} +9 -9
- package/dist/{chunk-TBGQMHCG.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-GI6ECCWB.cjs → chunk-PRJZMZ3X.cjs} +1155 -739
- package/dist/chunk-QD727QFI.cjs +62 -0
- package/dist/{chunk-67CZ2Q5K.cjs → chunk-RMI6O3AE.cjs} +32 -32
- package/dist/{chunk-EF2FZ7GT.cjs → chunk-S5P6CF5R.cjs} +47 -47
- package/dist/{chunk-6V2M2X3M.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-HF3V6WBJ.cjs → chunk-ZDPPNHA2.cjs} +3 -3
- package/dist/{cloud-tenants-QUL2XPAS.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/index.cjs +90 -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/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/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 +19 -19
- package/dist/{devices-LNB4CYPP.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 +42 -24
- package/dist/{knowledge-UTWKB6XG.cjs → knowledge-UUU3WVO3.cjs} +20 -20
- package/dist/{logs-PMXIWMBQ.cjs → logs-DFHNSODA.cjs} +26 -26
- package/dist/{policies-6DCMFDL7.cjs → policies-RGFUGDBP.cjs} +23 -23
- package/dist/{remote-session-SG44CNOK.cjs → remote-session-U3H77HKR.cjs} +10 -10
- package/dist/{scripts-2G7LXW7Z.cjs → scripts-KOVO5DHW.cjs} +26 -26
- package/dist/{software-update-MYLKHDHC.cjs → software-update-4EKHVV5W.cjs} +23 -23
- package/dist/{tickets-board-SQY2Y6QV.cjs → tickets-board-YIFOVMDX.cjs} +10 -10
- 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/navigation/app-layout.tsx +4 -4
- package/src/components/ui/date-picker.tsx +21 -3
- package/src/hooks/ui/use-content-breakpoint.ts +21 -2
- package/src/hooks/ui/use-media-query.ts +1 -0
- package/src/utils/presentation-for.ts +10 -0
- package/dist/chunk-UAI4VMEF.cjs +0 -46
|
@@ -10,9 +10,9 @@ var _chunkFDZOE5TLcjs = require('./chunk-FDZOE5TL.cjs');
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
var
|
|
14
|
-
require('./chunk-
|
|
15
|
-
require('./chunk-
|
|
13
|
+
var _chunkFJXSKMQJcjs = require('./chunk-FJXSKMQJ.cjs');
|
|
14
|
+
require('./chunk-5HMENXSR.cjs');
|
|
15
|
+
require('./chunk-ACPOJE32.cjs');
|
|
16
16
|
require('./chunk-KXFIYO4O.cjs');
|
|
17
17
|
require('./chunk-7HH4Y27U.cjs');
|
|
18
18
|
require('./chunk-QKCF77NZ.cjs');
|
|
@@ -1807,7 +1807,7 @@ require('./chunk-XIBCMGBW.cjs');
|
|
|
1807
1807
|
require('./chunk-IGVAPIEW.cjs');
|
|
1808
1808
|
require('./chunk-WWUSFOJ7.cjs');
|
|
1809
1809
|
require('./chunk-SKI6NKOE.cjs');
|
|
1810
|
-
require('./chunk-
|
|
1810
|
+
require('./chunk-KA3L6D5N.cjs');
|
|
1811
1811
|
require('./chunk-SSKMGH5O.cjs');
|
|
1812
1812
|
require('./chunk-S5C3OZCO.cjs');
|
|
1813
1813
|
require('./chunk-32ASZMTV.cjs');
|
|
@@ -1816,7 +1816,7 @@ require('./chunk-4IYNDV3G.cjs');
|
|
|
1816
1816
|
require('./chunk-6VOOPMP5.cjs');
|
|
1817
1817
|
require('./chunk-ORO54HDF.cjs');
|
|
1818
1818
|
require('./chunk-RUHKTWGB.cjs');
|
|
1819
|
-
require('./chunk-
|
|
1819
|
+
require('./chunk-4SSJ3T3N.cjs');
|
|
1820
1820
|
require('./chunk-YSDB6HGQ.cjs');
|
|
1821
1821
|
require('./chunk-FX35L4Y6.cjs');
|
|
1822
1822
|
require('./chunk-KKFDRCQF.cjs');
|
|
@@ -1842,7 +1842,7 @@ function buildTicketsBoardFixture(cast) {
|
|
|
1842
1842
|
const dana = assignee(cast.person("dana"));
|
|
1843
1843
|
const alex = assignee(cast.person("alex"));
|
|
1844
1844
|
return [
|
|
1845
|
-
|
|
1845
|
+
_chunkFJXSKMQJcjs.columnFromTicketStatus.call(void 0, "ACTIVE", [
|
|
1846
1846
|
ticket("tk-1051", "Printer offline at the front desk", "ACTIVE", {
|
|
1847
1847
|
ticketNumber: "1051",
|
|
1848
1848
|
agent: "fae",
|
|
@@ -1860,7 +1860,7 @@ function buildTicketsBoardFixture(cast) {
|
|
|
1860
1860
|
tags: ["onboarding"]
|
|
1861
1861
|
})
|
|
1862
1862
|
]),
|
|
1863
|
-
|
|
1863
|
+
_chunkFJXSKMQJcjs.columnFromTicketStatus.call(void 0, "TECH_REQUIRED", [
|
|
1864
1864
|
ticket("tk-1049", "Acrobat Pro for Maya", "TECH_REQUIRED", {
|
|
1865
1865
|
ticketNumber: "1049",
|
|
1866
1866
|
agent: "fae",
|
|
@@ -1884,7 +1884,7 @@ function buildTicketsBoardFixture(cast) {
|
|
|
1884
1884
|
escalatedByUser: true
|
|
1885
1885
|
})
|
|
1886
1886
|
]),
|
|
1887
|
-
|
|
1887
|
+
_chunkFJXSKMQJcjs.columnFromTicketStatus.call(void 0, "ON_HOLD", [
|
|
1888
1888
|
ticket("tk-1046", "Replace the conference room display", "ON_HOLD", {
|
|
1889
1889
|
ticketNumber: "1046",
|
|
1890
1890
|
organizationName: _chunkFDZOE5TLcjs.DEMO_ORGANIZATIONS.acme.name,
|
|
@@ -1893,7 +1893,7 @@ function buildTicketsBoardFixture(cast) {
|
|
|
1893
1893
|
activity: { kind: "waiting-external" }
|
|
1894
1894
|
})
|
|
1895
1895
|
]),
|
|
1896
|
-
|
|
1896
|
+
_chunkFJXSKMQJcjs.columnFromTicketStatus.call(void 0,
|
|
1897
1897
|
"RESOLVED",
|
|
1898
1898
|
[
|
|
1899
1899
|
ticket("tk-1048", "Slow laptop", "RESOLVED", {
|
|
@@ -1929,7 +1929,7 @@ function TicketsBoardScreen({ compact = false }) {
|
|
|
1929
1929
|
const cast = _chunkLHV2R6QUcjs.useProductDemoCast.call(void 0, );
|
|
1930
1930
|
const board = _react.useMemo.call(void 0, () => buildTicketsBoardFixture(cast), [cast]);
|
|
1931
1931
|
const columns = compact ? board.filter((column) => column.id !== "ON_HOLD") : board;
|
|
1932
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "h-full bg-ods-bg p-[var(--spacing-system-m)]", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
1932
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "h-full bg-ods-bg p-[var(--spacing-system-m)]", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkFJXSKMQJcjs.Board, { columns, onChange: noop, initialCollapsed: compact ? void 0 : FOLDED }) });
|
|
1933
1933
|
}
|
|
1934
1934
|
|
|
1935
1935
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry of a value → presentation table for a value off the wire, `undefined` for one the table does not know.
|
|
3
|
+
* Own keys only: `in` or a bare index would answer `toString` or `constructor` with the prototype's function.
|
|
4
|
+
*/
|
|
5
|
+
export declare function presentationFor<K extends string, V>(map: Readonly<Record<K, V>>, key: string | null | undefined): V | undefined;
|
|
6
|
+
//# sourceMappingURL=presentation-for.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presentation-for.d.ts","sourceRoot":"","sources":["../../src/utils/presentation-for.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EACjD,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAC3B,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7B,CAAC,GAAG,SAAS,CAEf"}
|
package/package.json
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
export { TenantsTableView } from './tenants-table-view';
|
|
4
|
-
export type { TenantsTableViewProps } from './tenants-table-view';
|
|
3
|
+
export { TENANTS_TABLE_FILTER_COLUMNS, TenantsTableView } from './tenants-table-view';
|
|
4
|
+
export type { TenantsTableFilterColumn, TenantsTableFilters, TenantsTableViewProps } from './tenants-table-view';
|
|
5
5
|
export { TenantCell } from './tenant-cell';
|
|
6
6
|
export type { TenantCellProps } from './tenant-cell';
|
|
7
7
|
export { TenantCustomerCell } from './tenant-customer-cell';
|
|
8
8
|
export type { TenantCustomerCellProps } from './tenant-customer-cell';
|
|
9
|
+
export { TenantLicensesCell } from './tenant-licenses-cell';
|
|
10
|
+
export type { TenantLicensesCellProps } from './tenant-licenses-cell';
|
|
9
11
|
export { TenantAccessCell } from './tenant-access-cell';
|
|
10
12
|
export type { TenantAccessCellProps } from './tenant-access-cell';
|
|
11
13
|
export { TENANT_COLUMNS, TENANTS_TABLE_COLUMNS } from './tenants-table-columns';
|
|
12
14
|
export type { TenantsTableColumn } from './tenants-table-columns';
|
|
13
|
-
export type { TenantRow } from './tenant-row';
|
|
15
|
+
export type { MultiCustomerTenantRow, TenantCustomer, TenantLicenseSummary, TenantRow } from './tenant-row';
|
|
14
16
|
export {
|
|
15
17
|
accessStateTag,
|
|
16
18
|
formatLastRead,
|
|
17
19
|
lastReadAt,
|
|
20
|
+
licenseSummaryLines,
|
|
18
21
|
providerMark,
|
|
19
22
|
TENANT_EMPTY_VALUE,
|
|
23
|
+
unattributedLabel,
|
|
20
24
|
usersCountLabel,
|
|
21
25
|
} from './tenant-presentation';
|
|
22
26
|
export type {
|
|
23
27
|
ProviderLogo,
|
|
24
28
|
ProviderMark,
|
|
29
|
+
TenantLicenseLines,
|
|
25
30
|
TenantStatusTag,
|
|
26
31
|
TenantAccessState,
|
|
27
32
|
TenantProvider,
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Lib main's tenants list, one customer per tenant: `TenantsTableView`'s default variant, the list a host renders while
|
|
4
|
+
// tenant-management-v2 is off. Its markup is lib main's to the class; it is deleted with that variant.
|
|
5
|
+
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { cn } from '../../../utils/cn';
|
|
8
|
+
import { openInNewTab } from '../../../utils/open-in-new-tab';
|
|
9
|
+
import { ArrowRightUpIcon } from '../../icons-v2-generated/arrows/arrow-right-up-icon';
|
|
10
|
+
import { Button } from '../../ui/button';
|
|
11
|
+
import { type ColumnDef, DataTable, dataTableLoadingRows, type Row, useDataTable } from '../../ui/data-table';
|
|
12
|
+
import { useLoadingScope } from '../../ui/loading-scope';
|
|
13
|
+
import { SquareAvatar } from '../../ui/square-avatar';
|
|
14
|
+
import { Tag } from '../../ui/tag';
|
|
15
|
+
import { TruncateText } from '../../ui/truncate-text';
|
|
16
|
+
import { TenantCell } from './tenant-cell';
|
|
17
|
+
import { accessStateTag, formatLastRead, usersCountLabel } from './tenant-presentation';
|
|
18
|
+
import type { TenantRow } from './tenant-row';
|
|
19
|
+
import type { TenantsTableColumn } from './tenants-table-columns';
|
|
20
|
+
|
|
21
|
+
/** Column layout of the single-customer list (Figma 1699-8249: Tenant, Customers, Access, open). */
|
|
22
|
+
const SINGLE_CUSTOMER_TENANT_COLUMNS = {
|
|
23
|
+
tenant: { id: 'tenant', header: 'Tenant', width: 'flex-1 min-w-0' },
|
|
24
|
+
customer: { id: 'customer', header: 'Customers', width: 'w-[220px] content-lg:w-[340px]', hideAt: 'md' },
|
|
25
|
+
access: { id: 'access', header: 'Access', width: 'w-[200px] content-lg:w-[216px]' },
|
|
26
|
+
open: { id: 'open', width: 'w-12 shrink-0 flex-none', hideAt: 'md', align: 'right' },
|
|
27
|
+
} satisfies Record<string, TenantsTableColumn>;
|
|
28
|
+
|
|
29
|
+
const getRowId = (row: TenantRow) => row.id;
|
|
30
|
+
|
|
31
|
+
const columnMeta = ({ width, hideAt, align }: TenantsTableColumn) => ({ width, hideAt, align });
|
|
32
|
+
|
|
33
|
+
/** A tenant still being read: a row the table keys, every field a cell shows empty (each draws its loading form). */
|
|
34
|
+
const loadingTenant = (id: string): TenantRow => ({
|
|
35
|
+
id,
|
|
36
|
+
name: '',
|
|
37
|
+
provider: '',
|
|
38
|
+
customer: { name: '' },
|
|
39
|
+
accessState: '',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** CUSTOMERS column: customer logo + name over the synced user count. */
|
|
43
|
+
function SingleCustomerTenantCustomerCell({
|
|
44
|
+
name,
|
|
45
|
+
imageUrl,
|
|
46
|
+
userCount,
|
|
47
|
+
}: {
|
|
48
|
+
name: string;
|
|
49
|
+
imageUrl?: string | null;
|
|
50
|
+
userCount?: number | null;
|
|
51
|
+
}) {
|
|
52
|
+
// While the tenant is read (a loading scope) the user count is a bar too (its label is never empty).
|
|
53
|
+
const loading = useLoadingScope();
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex min-w-0 flex-col justify-center">
|
|
56
|
+
<div className="flex min-w-0 items-center gap-[var(--spacing-system-xxs)]">
|
|
57
|
+
<SquareAvatar src={imageUrl ?? undefined} alt={name} fallback={name} size="xs" variant="square" />
|
|
58
|
+
<TruncateText>{name}</TruncateText>
|
|
59
|
+
</div>
|
|
60
|
+
<TruncateText variant="h6" tone="secondary" loading={loading}>
|
|
61
|
+
{usersCountLabel(userCount)}
|
|
62
|
+
</TruncateText>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** ACCESS column: the access tag over "Last read: ...". */
|
|
68
|
+
function SingleCustomerTenantAccessCell({
|
|
69
|
+
accessState,
|
|
70
|
+
lastReadAt,
|
|
71
|
+
now,
|
|
72
|
+
}: {
|
|
73
|
+
accessState: string;
|
|
74
|
+
lastReadAt?: string | null;
|
|
75
|
+
now?: Date;
|
|
76
|
+
}) {
|
|
77
|
+
// While the tenant is read (a loading scope) the access tag and its last read are their loading forms.
|
|
78
|
+
const loading = useLoadingScope();
|
|
79
|
+
return (
|
|
80
|
+
<div className="flex min-w-0 flex-col justify-center gap-[var(--spacing-system-xxs)]">
|
|
81
|
+
<Tag {...accessStateTag(accessState)} loading={loading} className="self-start" />
|
|
82
|
+
<TruncateText variant="h6" tone="secondary" loading={loading}>
|
|
83
|
+
{`Last read: ${formatLastRead(lastReadAt, now)}`}
|
|
84
|
+
</TruncateText>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The row's open-in-new-tab button: static chrome, drawn real and inert while the row is read. */
|
|
90
|
+
function OpenTenantButton({ href, name }: { href: string; name: string }) {
|
|
91
|
+
const loading = useLoadingScope();
|
|
92
|
+
return (
|
|
93
|
+
// The row itself is the details link; a nested `<a>` is invalid, so this opens the same href.
|
|
94
|
+
<div data-no-row-click className="pointer-events-auto flex items-center justify-end">
|
|
95
|
+
<Button
|
|
96
|
+
onClick={loading ? undefined : openInNewTab(href)}
|
|
97
|
+
disabled={loading}
|
|
98
|
+
variant="outline"
|
|
99
|
+
size="icon"
|
|
100
|
+
leftIcon={<ArrowRightUpIcon className="h-5 w-5" />}
|
|
101
|
+
aria-label={`Open ${name} in new tab`}
|
|
102
|
+
className="bg-ods-card"
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SingleCustomerTenantsTableViewProps {
|
|
109
|
+
/** The default: one customer per tenant, no licences and no funnels. */
|
|
110
|
+
variant?: 'single-customer';
|
|
111
|
+
rows: TenantRow[];
|
|
112
|
+
/** Where a row leads: the row is a link to it, and its last cell opens it in a new tab. */
|
|
113
|
+
getHref: (row: TenantRow) => string;
|
|
114
|
+
/** The search the rows answer; named by the "no match" message. */
|
|
115
|
+
search?: string;
|
|
116
|
+
/** Funnels belong to the multi-customer list. */
|
|
117
|
+
filters?: never;
|
|
118
|
+
/** The rows on screen are the previous result of a search still loading: they dim. */
|
|
119
|
+
isPending?: boolean;
|
|
120
|
+
/** All matching tenants, loaded or not. Default: the rows given. */
|
|
121
|
+
totalCount?: number;
|
|
122
|
+
stickyHeaderOffset?: string;
|
|
123
|
+
skeletonRows?: number;
|
|
124
|
+
hasNextPage?: boolean;
|
|
125
|
+
isFetchingNextPage?: boolean;
|
|
126
|
+
onLoadMore?: () => void;
|
|
127
|
+
/** The clock "Last read" is relative to. Default: the moment of the render. */
|
|
128
|
+
now?: Date;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The rows a loading tenants table draws when the host states none (a typical page of tenants). */
|
|
132
|
+
const TENANTS_TABLE_SKELETON_ROWS = 20;
|
|
133
|
+
|
|
134
|
+
/** The tenants list: tenant, customer, access state and a way into each tenant's details. */
|
|
135
|
+
export function SingleCustomerTenantsTableView({
|
|
136
|
+
rows,
|
|
137
|
+
getHref,
|
|
138
|
+
search = '',
|
|
139
|
+
isPending = false,
|
|
140
|
+
totalCount,
|
|
141
|
+
stickyHeaderOffset,
|
|
142
|
+
skeletonRows = TENANTS_TABLE_SKELETON_ROWS,
|
|
143
|
+
hasNextPage = false,
|
|
144
|
+
isFetchingNextPage = false,
|
|
145
|
+
onLoadMore,
|
|
146
|
+
now,
|
|
147
|
+
}: SingleCustomerTenantsTableViewProps) {
|
|
148
|
+
const columns = useMemo<ColumnDef<TenantRow>[]>(
|
|
149
|
+
() => [
|
|
150
|
+
{
|
|
151
|
+
id: SINGLE_CUSTOMER_TENANT_COLUMNS.tenant.id,
|
|
152
|
+
header: SINGLE_CUSTOMER_TENANT_COLUMNS.tenant.header,
|
|
153
|
+
cell: ({ row }: { row: Row<TenantRow> }) => (
|
|
154
|
+
<TenantCell provider={row.original.provider} name={row.original.name} domain={row.original.domain} />
|
|
155
|
+
),
|
|
156
|
+
enableSorting: false,
|
|
157
|
+
meta: columnMeta(SINGLE_CUSTOMER_TENANT_COLUMNS.tenant),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: SINGLE_CUSTOMER_TENANT_COLUMNS.customer.id,
|
|
161
|
+
header: SINGLE_CUSTOMER_TENANT_COLUMNS.customer.header,
|
|
162
|
+
cell: ({ row }: { row: Row<TenantRow> }) => (
|
|
163
|
+
<SingleCustomerTenantCustomerCell
|
|
164
|
+
name={row.original.customer.name}
|
|
165
|
+
imageUrl={row.original.customer.imageUrl}
|
|
166
|
+
userCount={row.original.userCount}
|
|
167
|
+
/>
|
|
168
|
+
),
|
|
169
|
+
enableSorting: false,
|
|
170
|
+
meta: columnMeta(SINGLE_CUSTOMER_TENANT_COLUMNS.customer),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: SINGLE_CUSTOMER_TENANT_COLUMNS.access.id,
|
|
174
|
+
header: SINGLE_CUSTOMER_TENANT_COLUMNS.access.header,
|
|
175
|
+
cell: ({ row }: { row: Row<TenantRow> }) => (
|
|
176
|
+
<SingleCustomerTenantAccessCell
|
|
177
|
+
accessState={row.original.accessState}
|
|
178
|
+
lastReadAt={row.original.lastReadAt}
|
|
179
|
+
now={now}
|
|
180
|
+
/>
|
|
181
|
+
),
|
|
182
|
+
enableSorting: false,
|
|
183
|
+
meta: columnMeta(SINGLE_CUSTOMER_TENANT_COLUMNS.access),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: SINGLE_CUSTOMER_TENANT_COLUMNS.open.id,
|
|
187
|
+
cell: ({ row }: { row: Row<TenantRow> }) => (
|
|
188
|
+
<OpenTenantButton href={getHref(row.original)} name={row.original.name} />
|
|
189
|
+
),
|
|
190
|
+
enableSorting: false,
|
|
191
|
+
meta: columnMeta(SINGLE_CUSTOMER_TENANT_COLUMNS.open),
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
[getHref, now],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// A page's loading scope: the table holds one page of placeholder rows, drawn with the real cells (`loadingCells`).
|
|
198
|
+
const loading = useLoadingScope();
|
|
199
|
+
const loadingRows = useMemo(
|
|
200
|
+
() => (loading ? dataTableLoadingRows(skeletonRows, loadingTenant) : null),
|
|
201
|
+
[loading, skeletonRows],
|
|
202
|
+
);
|
|
203
|
+
const table = useDataTable<TenantRow>({ data: loadingRows ?? rows, columns, getRowId, enableSorting: false });
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
// Dim, don't unmount, the stale rows while a deferred search refetches.
|
|
207
|
+
<div className={cn('transition-opacity duration-200', isPending && 'opacity-60')}>
|
|
208
|
+
<DataTable table={table}>
|
|
209
|
+
<DataTable.Header
|
|
210
|
+
stickyHeader={stickyHeaderOffset !== undefined}
|
|
211
|
+
stickyHeaderOffset={stickyHeaderOffset}
|
|
212
|
+
rightSlot={<DataTable.RowCount itemName="result" totalCount={totalCount ?? rows.length} loading={loading} />}
|
|
213
|
+
/>
|
|
214
|
+
<DataTable.Body
|
|
215
|
+
loadingCells
|
|
216
|
+
skeletonRows={skeletonRows}
|
|
217
|
+
emptyState={{
|
|
218
|
+
title: 'No tenants match',
|
|
219
|
+
description: `Nothing matches "${search}". Try a different name, domain or customer.`,
|
|
220
|
+
}}
|
|
221
|
+
rowClassName="mb-[var(--spacing-system-xxs)]"
|
|
222
|
+
rowHref={getHref}
|
|
223
|
+
/>
|
|
224
|
+
{rows.length > 0 && onLoadMore && (
|
|
225
|
+
<DataTable.InfiniteFooter
|
|
226
|
+
hasNextPage={hasNextPage}
|
|
227
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
228
|
+
onLoadMore={onLoadMore}
|
|
229
|
+
/>
|
|
230
|
+
)}
|
|
231
|
+
</DataTable>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -8,20 +8,27 @@ import { accessStateTag, formatLastRead } from './tenant-presentation';
|
|
|
8
8
|
export interface TenantAccessCellProps {
|
|
9
9
|
accessState: string;
|
|
10
10
|
lastReadAt?: string | null;
|
|
11
|
+
/** A person paused the sync: said instead of the last read. */
|
|
12
|
+
syncPaused?: boolean;
|
|
11
13
|
/** The clock "Last read" is relative to. Default: the moment of the render. */
|
|
12
14
|
now?: Date;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
/** ACCESS column: the access tag over "Last read: ...". */
|
|
16
|
-
export function TenantAccessCell({ accessState, lastReadAt, now }: TenantAccessCellProps) {
|
|
17
|
+
/** ACCESS column: the access tag over "Last read: ..." or "Sync paused"; a phone keeps the tag alone (Figma 3033-9343). */
|
|
18
|
+
export function TenantAccessCell({ accessState, lastReadAt, syncPaused = false, now }: TenantAccessCellProps) {
|
|
17
19
|
// While the tenant is read (a loading scope) the access tag and its last read are their loading forms.
|
|
18
20
|
const loading = useLoadingScope();
|
|
21
|
+
const line = syncPaused ? 'Sync paused' : lastReadAt ? `Last read: ${formatLastRead(lastReadAt, now)}` : null;
|
|
19
22
|
return (
|
|
20
23
|
<div className="flex min-w-0 flex-col justify-center gap-[var(--spacing-system-xxs)]">
|
|
21
24
|
<Tag {...accessStateTag(accessState)} loading={loading} className="self-start" />
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
{(loading || line) && (
|
|
26
|
+
<div className="hidden min-w-0 content-md:block">
|
|
27
|
+
<TruncateText variant="h6" tone="secondary" loading={loading}>
|
|
28
|
+
{line ?? ''}
|
|
29
|
+
</TruncateText>
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
25
32
|
</div>
|
|
26
33
|
);
|
|
27
34
|
}
|
|
@@ -3,27 +3,63 @@
|
|
|
3
3
|
import { useLoadingScope } from '../../ui/loading-scope';
|
|
4
4
|
import { SquareAvatar } from '../../ui/square-avatar';
|
|
5
5
|
import { TruncateText } from '../../ui/truncate-text';
|
|
6
|
-
import {
|
|
6
|
+
import { lowercaseUsersCountLabel, TENANT_EMPTY_VALUE, unattributedLabel } from './tenant-presentation';
|
|
7
|
+
import type { TenantCustomer } from './tenant-row';
|
|
7
8
|
|
|
8
9
|
export interface TenantCustomerCellProps {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
imageUrl?: string | null;
|
|
10
|
+
/** In the host's order: the cell names the first and counts the rest. */
|
|
11
|
+
customers: readonly TenantCustomer[];
|
|
12
12
|
userCount?: number | null;
|
|
13
|
+
/** Users no customer's rule claims: while there are any, the count line says how many, in the warning colour. */
|
|
14
|
+
unattributedUserCount?: number | null;
|
|
15
|
+
/** Nothing has been read from this directory yet: the count line says so. */
|
|
16
|
+
neverRead?: boolean;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* CUSTOMERS column: the first customer's logo + name and "+N" for the rest, over the synced user count — or
|
|
21
|
+
* "30 of 412 unattributed" while some users belong to no customer, or "Never read" before the first read.
|
|
22
|
+
*/
|
|
23
|
+
export function TenantCustomerCell({
|
|
24
|
+
customers,
|
|
25
|
+
userCount,
|
|
26
|
+
unattributedUserCount,
|
|
27
|
+
neverRead = false,
|
|
28
|
+
}: TenantCustomerCellProps) {
|
|
17
29
|
// While the tenant is read (a loading scope) the user count is a bar too (its label is never empty).
|
|
18
30
|
const loading = useLoadingScope();
|
|
31
|
+
const [first] = customers;
|
|
32
|
+
const more = customers.length - 1;
|
|
33
|
+
const unattributed = unattributedLabel(unattributedUserCount, userCount);
|
|
19
34
|
return (
|
|
20
35
|
<div className="flex min-w-0 flex-col justify-center">
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
{first ? (
|
|
37
|
+
<div className="flex min-w-0 items-center gap-[var(--spacing-system-xxs)]">
|
|
38
|
+
<SquareAvatar
|
|
39
|
+
src={first.imageUrl ?? undefined}
|
|
40
|
+
alt={first.name}
|
|
41
|
+
fallback={first.name}
|
|
42
|
+
size="xs"
|
|
43
|
+
variant="square"
|
|
44
|
+
/>
|
|
45
|
+
<TruncateText>{first.name}</TruncateText>
|
|
46
|
+
{more > 0 && (
|
|
47
|
+
<span className="shrink-0 text-ods-text-secondary text-h4">
|
|
48
|
+
+{more}
|
|
49
|
+
<span className="sr-only">{more === 1 ? ' more customer' : ' more customers'}</span>
|
|
50
|
+
</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
) : (
|
|
54
|
+
<TruncateText>{TENANT_EMPTY_VALUE}</TruncateText>
|
|
55
|
+
)}
|
|
56
|
+
<TruncateText
|
|
57
|
+
variant="h6"
|
|
58
|
+
tone="secondary"
|
|
59
|
+
loading={loading}
|
|
60
|
+
className={unattributed ? 'text-ods-warning' : undefined}
|
|
61
|
+
>
|
|
62
|
+
{unattributed ?? (neverRead ? 'Never read' : lowercaseUsersCountLabel(userCount))}
|
|
27
63
|
</TruncateText>
|
|
28
64
|
</div>
|
|
29
65
|
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../../utils/cn';
|
|
4
|
+
import { useLoadingScope } from '../../ui/loading-scope';
|
|
5
|
+
import { TruncateText } from '../../ui/truncate-text';
|
|
6
|
+
import { licenseSummaryLines } from './tenant-presentation';
|
|
7
|
+
import type { TenantLicenseSummary } from './tenant-row';
|
|
8
|
+
|
|
9
|
+
export interface TenantLicensesCellProps {
|
|
10
|
+
licenses?: TenantLicenseSummary | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** LICENCES column: assigned of purchased over what is unused (Figma 3025-3536). */
|
|
14
|
+
export function TenantLicensesCell({ licenses }: TenantLicensesCellProps) {
|
|
15
|
+
// While the tenant is read (a loading scope) both lines are bars.
|
|
16
|
+
const loading = useLoadingScope();
|
|
17
|
+
const { value, detail, warning } = licenseSummaryLines(licenses);
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex min-w-0 flex-col justify-center">
|
|
20
|
+
<TruncateText loading={loading}>{value}</TruncateText>
|
|
21
|
+
{(loading || detail) && (
|
|
22
|
+
<TruncateText variant="h6" tone="secondary" loading={loading} className={cn(warning && 'text-ods-warning')}>
|
|
23
|
+
{detail ?? ''}
|
|
24
|
+
</TruncateText>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
import type { ComponentType, SVGProps } from 'react';
|
|
12
12
|
import { formatRelativeTimeFrom } from '../../../utils/date-utils';
|
|
13
13
|
import { formatDate } from '../../../utils/format-date';
|
|
14
|
+
import { presentationFor } from '../../../utils/presentation-for';
|
|
14
15
|
import { GoogleLogoIcon } from '../../icons-v2-generated/brand-logos/google-logo-icon';
|
|
15
16
|
import { Office365LogoIcon } from '../../icons-v2-generated/brand-logos/office-365-logo-icon';
|
|
16
17
|
import { CodingForkIcon } from '../../icons-v2-generated/coding/coding-fork-icon';
|
|
17
18
|
import type { TagProps } from '../../ui/tag';
|
|
19
|
+
import type { TenantLicenseSummary } from './tenant-row';
|
|
18
20
|
|
|
19
21
|
/** What a cell shows when it has no value. */
|
|
20
22
|
export const TENANT_EMPTY_VALUE = '—';
|
|
@@ -33,10 +35,6 @@ export type TenantAccessState =
|
|
|
33
35
|
/** The directory providers a tenant connects to. */
|
|
34
36
|
export type TenantProvider = 'MICROSOFT_365' | 'GOOGLE_WORKSPACE';
|
|
35
37
|
|
|
36
|
-
function presentationFor<K extends string, V>(map: Record<K, V>, key: string | null | undefined): V | undefined {
|
|
37
|
-
return key != null && key in map ? map[key as K] : undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
38
|
// Colours read off the Figma list (node 1699-8249): red for the two states that
|
|
41
39
|
// need the customer's admin, grey for read-only, outline for write-available,
|
|
42
40
|
// green for write-enabled. NOT_AUTHORISED has no frame of its own
|
|
@@ -109,3 +107,47 @@ export function usersCountLabel(count: number | null | undefined): string {
|
|
|
109
107
|
if (count == null) return TENANT_EMPTY_VALUE;
|
|
110
108
|
return `${count} ${count === 1 ? 'User' : 'Users'}`;
|
|
111
109
|
}
|
|
110
|
+
|
|
111
|
+
/** The multi-customer list's count line (Figma 3025-3536): "227 users", the empty mark before the first read. */
|
|
112
|
+
export function lowercaseUsersCountLabel(count: number | null | undefined): string {
|
|
113
|
+
if (count == null) return TENANT_EMPTY_VALUE;
|
|
114
|
+
return `${count} ${count === 1 ? 'user' : 'users'}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* "30 of 412 unattributed" while some synced users belong to no customer (no customer's rule claims them), or `null`
|
|
119
|
+
* while every user is attributed or nothing has been read.
|
|
120
|
+
*/
|
|
121
|
+
export function unattributedLabel(
|
|
122
|
+
unattributed: number | null | undefined,
|
|
123
|
+
users: number | null | undefined,
|
|
124
|
+
): string | null {
|
|
125
|
+
if (unattributed == null || unattributed <= 0) return null;
|
|
126
|
+
return users == null ? `${unattributed} unattributed` : `${unattributed} of ${users} unattributed`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The share of purchased seats left unused that the list calls out: every row of Figma 3025-3536 falls on its side. */
|
|
130
|
+
const UNUSED_WARNING_SHARE = 0.1;
|
|
131
|
+
|
|
132
|
+
export interface TenantLicenseLines {
|
|
133
|
+
value: string;
|
|
134
|
+
detail: string | null;
|
|
135
|
+
/** The detail is worth a look: draw it in the warning colour. */
|
|
136
|
+
warning: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The LICENCES cell: "118 of 120" over "2 unused"; "88 assigned" over "Total not available" where the provider reports
|
|
141
|
+
* no purchased seats; the empty mark until the numbers have been read.
|
|
142
|
+
*/
|
|
143
|
+
export function licenseSummaryLines(summary: TenantLicenseSummary | null | undefined): TenantLicenseLines {
|
|
144
|
+
if (!summary) return { value: TENANT_EMPTY_VALUE, detail: null, warning: false };
|
|
145
|
+
const { assigned, purchased } = summary;
|
|
146
|
+
if (purchased == null) return { value: `${assigned} assigned`, detail: 'Total not available', warning: false };
|
|
147
|
+
const unused = summary.unused ?? purchased - assigned;
|
|
148
|
+
return {
|
|
149
|
+
value: `${assigned} of ${purchased}`,
|
|
150
|
+
detail: unused < 0 ? `${-unused} over` : `${unused} unused`,
|
|
151
|
+
warning: unused > 0 && unused / purchased >= UNUSED_WARNING_SHARE,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -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
|
+
}
|