@cosmicdrift/kumiko-samples 0.391.1 → 0.392.1
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/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/admin-shell/changes.json +6 -0
- package/packages/bundled-features/src/admin-shell/feature.ts +60 -31
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -2
- package/packages/bundled-features/src/cap-overview/changes.json +18 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +1 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +3 -2
- package/packages/bundled-features/src/cap-overview/i18n.ts +4 -0
- package/packages/bundled-features/src/cap-overview/types.ts +4 -1
- package/packages/bundled-features/src/cap-overview/web/cap-amount-format.ts +37 -0
- package/packages/bundled-features/src/cap-overview/web/cap-cards-panel.tsx +137 -54
- package/packages/bundled-features/src/cap-overview/web/cap-usage-bar.tsx +18 -4
- package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -3
- package/packages/bundled-features/src/cap-overview/web/usage-math.ts +5 -1
- package/packages/bundled-features/src/channel-email/app-transport.ts +37 -0
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +6 -0
- package/packages/bundled-features/src/channel-email/index.ts +5 -0
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +2 -22
- package/packages/bundled-features/src/foundation-shared/changes.json +6 -0
- package/packages/bundled-features/src/foundation-shared/index.ts +1 -0
- package/packages/bundled-features/src/foundation-shared/mail-display-name.ts +20 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +17 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +8 -45
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +14 -0
- package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +118 -0
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +8 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +13 -8
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +18 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +158 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +44 -0
- package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +47 -1
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +12 -35
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +49 -0
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +1 -1
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +52 -0
- package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +73 -0
- package/packages/bundled-features/src/inbound-provider-imap/sent-folder.ts +49 -0
- package/packages/bundled-features/src/inbound-provider-imap/smtp-client.ts +135 -0
- package/packages/bundled-features/src/legal-pages/README.md +5 -10
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/i18n.ts +9 -1
- package/packages/bundled-features/src/mail-foundation/probe.ts +43 -22
- package/packages/bundled-features/src/readiness/built-in-probes.ts +15 -2
- package/packages/bundled-features/src/readiness/changes.json +30 -0
- package/packages/bundled-features/src/readiness/constants.ts +2 -0
- package/packages/bundled-features/src/readiness/i18n.ts +7 -1
- package/packages/bundled-features/src/readiness/index.ts +6 -1
- package/packages/bundled-features/src/readiness/screens.ts +25 -2
- package/packages/bundled-features/src/readiness/visible-results.ts +2 -1
- package/packages/bundled-features/src/renderer-foundation/README.md +19 -21
- package/packages/bundled-features/src/template-resolver/README.md +144 -165
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +17 -6
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +3 -3
- package/packages/bundled-features/src/user-data-rights/schema/download-token.ts +5 -3
- package/packages/bundled-features/src/user-data-rights/token-helpers.ts +5 -4
- package/samples/apps/cap-billing-demo/bin/server.ts +7 -7
- package/samples/apps/cap-billing-demo/src/feature.ts +38 -38
- package/samples/apps/cap-billing-demo/src/run-config.ts +20 -20
- package/samples/apps/cap-billing-demo/src/tier-map.ts +14 -14
- package/samples/apps/config-demo/src/app/server.ts +4 -4
- package/samples/apps/config-demo/src/features/demo/feature.ts +4 -4
- package/samples/apps/marketing-demo/playwright.config.ts +3 -3
- package/samples/apps/marketing-demo/src/app/client.tsx +2 -2
- package/samples/apps/marketing-demo/src/app/seed-data.ts +9 -9
- package/samples/apps/marketing-demo/src/app/seed.ts +14 -14
- package/samples/apps/marketing-demo/src/app/server.ts +7 -7
- package/samples/apps/marketing-demo/src/app/shell.tsx +3 -3
- package/samples/apps/marketing-demo/src/features/assets/feature.ts +3 -3
- package/samples/apps/marketing-demo/src/features/assets/i18n.ts +7 -7
- package/samples/apps/marketing-demo/src/features/assets/web/index.ts +2 -2
- package/samples/apps/showcase/src/app/client.tsx +3 -3
- package/samples/apps/showcase/src/app/seed-items.ts +14 -14
- package/samples/apps/showcase/src/app/server.ts +7 -7
- package/samples/apps/showcase/src/features/demos/feature.ts +13 -13
- package/samples/apps/showcase/src/features/demos/i18n.ts +2 -2
- package/samples/apps/showcase/src/features/demos/web/components/page.tsx +5 -5
- package/samples/apps/showcase/src/features/demos/web/index.ts +4 -4
- package/samples/apps/showcase/src/features/demos/web/pages/dialog.tsx +1 -1
- package/samples/apps/showcase/src/features/demos/web/pages/inputs.tsx +8 -8
- package/samples/apps/showcase/src/features/demos/web/pages/layout.tsx +5 -5
- package/samples/apps/showcase/src/features/demos/web/pages/toast.tsx +3 -3
- package/samples/apps/showcase/src/features/items/feature.ts +8 -8
- package/samples/apps/showcase/src/features/items/i18n.ts +3 -3
- package/samples/apps/showcase/src/features/items/index.ts +3 -3
- package/samples/apps/showcase/src/features/items/schema/index.ts +4 -4
- package/samples/apps/showcase/src/features/items/schema/item.ts +27 -27
- package/samples/apps/showcase/src/features/items/web/index.ts +6 -6
- package/samples/apps/styleguide/src/app/seed.ts +3 -3
- package/samples/apps/styleguide/src/app/server.ts +2 -2
- package/samples/apps/styleguide/src/app/shell.tsx +4 -4
- package/samples/apps/styleguide/src/features/content/web.tsx +6 -6
- package/samples/apps/styleguide/src/features/examples/feature.ts +8 -8
- package/samples/apps/styleguide/src/features/gallery/feature.ts +9 -9
- package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +12 -12
- package/samples/apps/styleguide/src/features/widgets/feature.ts +2 -2
- package/samples/apps/styleguide/src/features/widgets/web/DashboardFilterEcho.tsx +3 -3
- package/samples/apps/styleguide/src/features/widgets/web/DashboardKpiIcon.tsx +4 -4
- package/samples/apps/ui-walkthrough/playwright.config.ts +6 -6
- package/samples/apps/ui-walkthrough/scripts/emit-e2e-data.ts +5 -5
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +13 -13
- package/samples/apps/ui-walkthrough/src/app/client.tsx +2 -2
- package/samples/apps/ui-walkthrough/src/app/server.ts +14 -14
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +3 -3
- package/samples/apps/use-all-bundled/bin/main.ts +5 -5
- package/samples/apps/use-all-bundled/feature-manifest.json +10 -4
- package/samples/apps/use-all-bundled/kumiko/schema.ts +7 -7
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +20 -20
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +4 -4
- package/samples/apps/use-all-bundled/src/app/client.tsx +4 -4
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +12 -12
- package/samples/apps/use-all-bundled/src/app/seed.ts +6 -6
- package/samples/apps/user-data-rights-demo/src/feature.ts +23 -23
- package/samples/apps/workspaces/playwright.config.ts +5 -5
- package/samples/apps/workspaces/src/app/auth-constants.ts +3 -3
- package/samples/apps/workspaces/src/app/server.ts +16 -16
- package/samples/apps/workspaces/src/app/shell.tsx +2 -2
- package/samples/apps/workspaces/src/features/demo/schema/order.ts +4 -4
- package/samples/apps/workspaces/src/features/demo-driver/feature.ts +5 -5
- package/samples/recipes/apex-surface-auth/src/feature.ts +31 -31
- package/samples/recipes/designer-demo/src/feature.ts +6 -6
- package/samples/recipes/encrypted-tenant-config/src/feature.ts +20 -20
- package/samples/recipes/lane-routing/src/feature.ts +5 -5
- package/samples/recipes/legal-pages/src/feature.ts +29 -29
- package/samples/recipes/record-detail-layout/src/feature.ts +21 -2
- package/samples/recipes/seed-migration/scripts/smoke.ts +20 -20
- package/samples/recipes/seed-migration/seeds/2026-01-01-initial-tenants.ts +11 -11
- package/samples/recipes/seed-migration/seeds/2026-02-01-fix-admin-roles.ts +13 -13
- package/samples/recipes/user-data-rights/src/feature.ts +13 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.392.1",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.392.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.392.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "includeTenantOverview turns the tenant overview off",
|
|
6
|
+
"detail": "createAdminShellFeature({ includeTenantOverview: false }) registers neither the tenant overview screen nor its nav and leaves it out of the tenant workspace. The default stays true."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.363.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -39,6 +39,9 @@ export type CreateAdminShellOptions = {
|
|
|
39
39
|
/** Tenant nav → document-designer:nav:templates, platform nav → document-designer:nav:platform-templates.
|
|
40
40
|
* Requires document-designer mounted (a separate package, so no import here). Default false. */
|
|
41
41
|
readonly includeDocumentDesigner?: boolean;
|
|
42
|
+
/** Tenant overview dashboard: screen, nav and the entry in the tenant workspace. Default true.
|
|
43
|
+
* Set false when the app ships its own landing screen for tenant admins. */
|
|
44
|
+
readonly includeTenantOverview?: boolean;
|
|
42
45
|
/** When false, only overview screens + nav are registered — app owns r.workspace(). Default true. */
|
|
43
46
|
readonly registerWorkspaces?: boolean;
|
|
44
47
|
/** Metric list the overview panels are built from — pass the SAME list to
|
|
@@ -47,18 +50,48 @@ export type CreateAdminShellOptions = {
|
|
|
47
50
|
readonly metrics?: readonly MetricDefinition[];
|
|
48
51
|
};
|
|
49
52
|
|
|
53
|
+
type NavIdOptions = {
|
|
54
|
+
readonly includeTenantOverview?: boolean;
|
|
55
|
+
readonly includeTierAdmin?: boolean;
|
|
56
|
+
readonly includeCapOverview: boolean;
|
|
57
|
+
readonly includeDocumentDesigner: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function tenantWorkspaceNavIds(options: NavIdOptions): string[] {
|
|
61
|
+
return [
|
|
62
|
+
...(options.includeTenantOverview === true ? ["admin-shell:nav:tenant-overview"] : []),
|
|
63
|
+
"tenant:nav:members",
|
|
64
|
+
"audit:nav:audit-log",
|
|
65
|
+
...(options.includeCapOverview ? ["admin-shell:nav:my-caps"] : []),
|
|
66
|
+
...(options.includeDocumentDesigner ? ["document-designer:nav:templates"] : []),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function platformWorkspaceNavIds(options: NavIdOptions): string[] {
|
|
71
|
+
return [
|
|
72
|
+
"admin-shell:nav:platform-overview",
|
|
73
|
+
"admin-shell:nav:tenants",
|
|
74
|
+
...(options.includeCapOverview ? ["admin-shell:nav:tenant-caps"] : []),
|
|
75
|
+
"audit:nav:audit-log-all-tenants",
|
|
76
|
+
"jobs:nav:job-runs",
|
|
77
|
+
...(options.includeDocumentDesigner ? ["document-designer:nav:platform-templates"] : []),
|
|
78
|
+
...(options.includeTierAdmin === true ? ["admin-shell:nav:tier-admin"] : []),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
export function createAdminShellFeature(options: CreateAdminShellOptions = {}): FeatureDefinition {
|
|
51
83
|
const tenantWsId = options.workspaceIds?.tenant ?? DEFAULT_TENANT_WORKSPACE_ID;
|
|
52
84
|
const platformWsId = options.workspaceIds?.platform ?? DEFAULT_PLATFORM_WORKSPACE_ID;
|
|
53
85
|
const includeTierAdmin = options.includeTierAdmin ?? true;
|
|
54
86
|
const includeCapOverview = options.includeCapOverview ?? false;
|
|
55
87
|
const includeDocumentDesigner = options.includeDocumentDesigner ?? false;
|
|
88
|
+
const includeTenantOverview = options.includeTenantOverview ?? true;
|
|
56
89
|
const registerWorkspaces = options.registerWorkspaces ?? true;
|
|
57
90
|
const metrics = options.metrics ?? DEFAULT_METRICS;
|
|
58
91
|
|
|
59
92
|
return defineFeature(ADMIN_SHELL_FEATURE, (r) => {
|
|
60
93
|
r.describe(
|
|
61
|
-
"Registers tenant-admin and platform-admin workspaces with provider nav into owner-feature screens (`tenant:screen:members`, `audit:screen:audit-log`, `audit:screen:audit-log-all-tenants`, `tenant:screen:tenant-list`, `jobs:screen:job-runs`, optional `tier-engine:screen:tier-admin`, optional `cap-overview:screen:my-caps`/`cap-overview:screen:tenant-cap-list`, optional `document-designer` template navs) and the two overview dashboards, built from the metrics list (`metrics` option, default DEFAULT_METRICS). Mount after user, tenant, audit, jobs, metrics and metrics-system; pass `workspaceIds` to match app URL conventions (e.g. Studio `d`/`s`, PublicStatus `admin`/`sysadmin`). Client: `adminShellClient()`, `tenantClient()`, `auditClient()`, `jobsClient()`, optional `tierEngineClient()`.",
|
|
94
|
+
"Registers tenant-admin and platform-admin workspaces with provider nav into owner-feature screens (`tenant:screen:members`, `audit:screen:audit-log`, `audit:screen:audit-log-all-tenants`, `tenant:screen:tenant-list`, `jobs:screen:job-runs`, optional `tier-engine:screen:tier-admin`, optional `cap-overview:screen:my-caps`/`cap-overview:screen:tenant-cap-list`, optional `document-designer` template navs) and the two overview dashboards (the tenant one can be dropped with `includeTenantOverview: false`), built from the metrics list (`metrics` option, default DEFAULT_METRICS). Mount after user, tenant, audit, jobs, metrics and metrics-system; pass `workspaceIds` to match app URL conventions (e.g. Studio `d`/`s`, PublicStatus `admin`/`sysadmin`). Client: `adminShellClient()`, `tenantClient()`, `auditClient()`, `jobsClient()`, optional `tierEngineClient()`.",
|
|
62
95
|
);
|
|
63
96
|
r.uiHints({
|
|
64
97
|
displayLabel: "Admin Shell",
|
|
@@ -77,39 +110,35 @@ export function createAdminShellFeature(options: CreateAdminShellOptions = {}):
|
|
|
77
110
|
|
|
78
111
|
r.translations({ keys: ADMIN_SHELL_I18N });
|
|
79
112
|
|
|
80
|
-
const tenantNav
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
...(includeCapOverview ? (["admin-shell:nav:my-caps"] as const) : []),
|
|
85
|
-
...(includeDocumentDesigner ? (["document-designer:nav:templates"] as const) : []),
|
|
86
|
-
];
|
|
87
|
-
const platformNav: string[] = [
|
|
88
|
-
"admin-shell:nav:platform-overview",
|
|
89
|
-
"admin-shell:nav:tenants",
|
|
90
|
-
...(includeCapOverview ? (["admin-shell:nav:tenant-caps"] as const) : []),
|
|
91
|
-
"audit:nav:audit-log-all-tenants",
|
|
92
|
-
"jobs:nav:job-runs",
|
|
93
|
-
...(includeDocumentDesigner ? (["document-designer:nav:platform-templates"] as const) : []),
|
|
94
|
-
...(includeTierAdmin ? (["admin-shell:nav:tier-admin"] as const) : []),
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
r.screen({
|
|
98
|
-
id: TENANT_OVERVIEW_SCREEN_ID,
|
|
99
|
-
type: "dashboard",
|
|
100
|
-
access: { roles: access.admin },
|
|
101
|
-
description: "admin-shell.screen.tenant-overview.subtitle",
|
|
102
|
-
timeRange: OVERVIEW_TIME_RANGE,
|
|
103
|
-
panels: [...tenantOverviewPanels(metrics, { includeCapOverview })],
|
|
113
|
+
const tenantNav = tenantWorkspaceNavIds({
|
|
114
|
+
includeTenantOverview,
|
|
115
|
+
includeCapOverview,
|
|
116
|
+
includeDocumentDesigner,
|
|
104
117
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
screen: "admin-shell:screen:tenant-overview",
|
|
110
|
-
order: 1,
|
|
118
|
+
const platformNav = platformWorkspaceNavIds({
|
|
119
|
+
includeTierAdmin,
|
|
120
|
+
includeCapOverview,
|
|
121
|
+
includeDocumentDesigner,
|
|
111
122
|
});
|
|
112
123
|
|
|
124
|
+
if (includeTenantOverview) {
|
|
125
|
+
r.screen({
|
|
126
|
+
id: TENANT_OVERVIEW_SCREEN_ID,
|
|
127
|
+
type: "dashboard",
|
|
128
|
+
access: { roles: access.admin },
|
|
129
|
+
description: "admin-shell.screen.tenant-overview.subtitle",
|
|
130
|
+
timeRange: OVERVIEW_TIME_RANGE,
|
|
131
|
+
panels: [...tenantOverviewPanels(metrics, { includeCapOverview })],
|
|
132
|
+
});
|
|
133
|
+
r.nav({
|
|
134
|
+
id: "tenant-overview",
|
|
135
|
+
label: "admin-shell:nav.tenantOverview",
|
|
136
|
+
icon: "home",
|
|
137
|
+
screen: "admin-shell:screen:tenant-overview",
|
|
138
|
+
order: 1,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
113
142
|
r.screen({
|
|
114
143
|
id: PLATFORM_OVERVIEW_SCREEN_ID,
|
|
115
144
|
type: "dashboard",
|
package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/error
|
|
|
37
37
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
38
38
|
import * as z from "zod";
|
|
39
39
|
import { decryptStoredPii, sessionLocaleField, sessionTimezoneField } from "../../shared/index.ts";
|
|
40
|
-
// kumiko-lint-ignore cross-feature-import
|
|
40
|
+
// kumiko-lint-ignore cross-feature-import the handler loads the pending invitation and marks it accepted
|
|
41
41
|
import {
|
|
42
42
|
INVITATION_STATUS,
|
|
43
43
|
tenantInvitationEntity,
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
grantInvitedGlobalRoles,
|
|
51
51
|
parseInvitedGlobalRoles,
|
|
52
52
|
} from "../../user/invited-global-roles.ts";
|
|
53
|
-
// kumiko-lint-ignore cross-feature-import
|
|
53
|
+
// kumiko-lint-ignore cross-feature-import the handler verifies the invited email password against the user row, like login
|
|
54
54
|
import { userTable } from "../../user/schema/user.ts";
|
|
55
55
|
import { invalidCredentials, invalidInviteToken, inviteEmailMismatch } from "../errors.ts";
|
|
56
56
|
import {
|
package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
34
34
|
import * as z from "zod";
|
|
35
35
|
import { decryptStoredPii, registrationLocaleFromRequest } from "../../shared/index.ts";
|
|
36
|
-
// kumiko-lint-ignore cross-feature-import
|
|
36
|
+
// kumiko-lint-ignore cross-feature-import the handler loads the pending invitation and marks it accepted
|
|
37
37
|
import {
|
|
38
38
|
INVITATION_STATUS,
|
|
39
39
|
tenantInvitationEntity,
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
import { seedTenantMembership } from "../../tenant/seeding.ts";
|
|
49
49
|
// kumiko-lint-ignore cross-feature-import global roles granted on invite accept live on the user row
|
|
50
50
|
import { parseInvitedGlobalRoles } from "../../user/invited-global-roles.ts";
|
|
51
|
-
// kumiko-lint-ignore cross-feature-import
|
|
51
|
+
// kumiko-lint-ignore cross-feature-import the handler rejects an invitation whose email already has a user row
|
|
52
52
|
import { userTable } from "../../user/schema/user.ts";
|
|
53
53
|
import { invalidInviteToken } from "../errors.ts";
|
|
54
54
|
import {
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.392.1",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Cap cards \"view all\" link uses the primary link style",
|
|
6
|
+
"detail": "The link under the cap cards no longer renders in muted grey; it uses the default link variant like other text links."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.392.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Cap amounts follow their unit and a limit of 0 reads \"Not included\"",
|
|
12
|
+
"detail": "Values with unit \"mb\" show as MB, GB or TB (base 1024), count and tokens use the locale's digit grouping. A limit of 0 or less shows \"Not included\" instead of a 0 bar, also when nothing is measured. caps:usage rows and the tenant-caps list cells now carry the unit of the cap. New key cap-overview.notIncluded in all locale packages."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.392.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "CapCardsPanel can be limited to specific cap keys via params.capKeys",
|
|
18
|
+
"detail": "A custom dashboard panel with params capKeys as a list of cap ids shows only those cards, in the order of the capsUsage query. Anything other than a list of strings means no limit, and capKeys is not sent to the server. Each limited cap card also shows how much is left (or that nothing is), next to the percent chip, and a limited panel adds a link to the full usage screen."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.383.0",
|
|
4
22
|
"type": "improvement",
|
|
@@ -86,6 +86,7 @@ export function createCapsUsageQuery(
|
|
|
86
86
|
id: cap.id,
|
|
87
87
|
label: cap.label,
|
|
88
88
|
limit,
|
|
89
|
+
...(cap.unit !== undefined && { unit: cap.unit }),
|
|
89
90
|
...(cap.icon !== undefined && { icon: cap.icon }),
|
|
90
91
|
...(cap.accentColor !== undefined && { accentColor: cap.accentColor }),
|
|
91
92
|
};
|
|
@@ -262,10 +262,11 @@ export function createTenantCapsListQuery(caps: readonly CapSpec[], listCaps: re
|
|
|
262
262
|
const rawUsed = usageByCap.get(cap.id)?.get(row.tenantId);
|
|
263
263
|
const used = rawUsed === undefined ? 0 : rawUsed;
|
|
264
264
|
const limit = limitByTenantAndCap.get(tenantCapKey(row.tenantId, cap.id)) ?? null;
|
|
265
|
+
const unit = cap.unit !== undefined ? { unit: cap.unit } : {};
|
|
265
266
|
capFields[capFieldName(cap.id)] =
|
|
266
267
|
used === null
|
|
267
|
-
? { used: null, limit, fraction: 0 }
|
|
268
|
-
: { used, limit, fraction: computeFraction(used, limit) };
|
|
268
|
+
? { used: null, limit, fraction: 0, ...unit }
|
|
269
|
+
: { used, limit, fraction: computeFraction(used, limit), ...unit };
|
|
269
270
|
}
|
|
270
271
|
return {
|
|
271
272
|
tenantId: row.tenantId,
|
|
@@ -12,7 +12,11 @@ export const CAP_OVERVIEW_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
|
12
12
|
"cap-overview.platform.filter.tenant": { en: "Tenant" },
|
|
13
13
|
"cap-overview.cards.empty": { en: "No caps configured for this tenant." },
|
|
14
14
|
"cap-overview.cards.loading": { en: "Loading usage…" },
|
|
15
|
+
"cap-overview.cards.remaining": { en: "{amount} left" },
|
|
16
|
+
"cap-overview.cards.noneLeft": { en: "None left" },
|
|
17
|
+
"cap-overview.cards.viewAll": { en: "View all usage" },
|
|
15
18
|
"cap-overview.notMeasured": { en: "Not measured yet" },
|
|
19
|
+
"cap-overview.notIncluded": { en: "Not included" },
|
|
16
20
|
"cap-overview.unlimited": { en: "{used} · unlimited" },
|
|
17
21
|
"cap-overview.errors.progressPrimitiveMissing": {
|
|
18
22
|
en: "Usage bar unavailable — Progress primitive is not registered.",
|
|
@@ -2,6 +2,8 @@ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
|
2
2
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import type { CapLimitContext } from "../cap-counter/index.ts";
|
|
4
4
|
|
|
5
|
+
export type CapUnit = "count" | "mb" | "tokens";
|
|
6
|
+
|
|
5
7
|
// Closed vocabulary for the dashboard cards' icon chip — cap-cards-panel.tsx
|
|
6
8
|
// holds the matching lucide-react lookup. Small on purpose (YAGNI): extend
|
|
7
9
|
// both together when a cap needs a shape not covered here.
|
|
@@ -35,7 +37,7 @@ export type CapSpec = {
|
|
|
35
37
|
db: TenantDb,
|
|
36
38
|
tenantIds: readonly TenantId[],
|
|
37
39
|
) => Promise<Map<TenantId, number | null>>;
|
|
38
|
-
readonly unit?:
|
|
40
|
+
readonly unit?: CapUnit;
|
|
39
41
|
/** Icon chip on the dashboard cards. Optional — omitted renders the card
|
|
40
42
|
* without an icon, same as before this field existed. */
|
|
41
43
|
readonly icon?: CapIconKey;
|
|
@@ -54,6 +56,7 @@ export type CapUsage = {
|
|
|
54
56
|
// `limit: null` means unlimited — a pure usage meter with no cap to hit.
|
|
55
57
|
readonly limit: number | null;
|
|
56
58
|
readonly fraction: number;
|
|
59
|
+
readonly unit?: CapUnit;
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
export type CapUsageWithMeta = CapUsage & {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CapUnit } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
const MB_PER_GB = 1024;
|
|
4
|
+
const MB_PER_TB = 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
function megabyteUnit(valueInMb: number): { readonly unit: string; readonly divisor: number } {
|
|
7
|
+
const magnitude = Math.abs(valueInMb);
|
|
8
|
+
if (magnitude < MB_PER_GB) return { unit: "megabyte", divisor: 1 };
|
|
9
|
+
if (magnitude < MB_PER_TB) return { unit: "gigabyte", divisor: MB_PER_GB };
|
|
10
|
+
return { unit: "terabyte", divisor: MB_PER_TB };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatCapAmount(value: number, unit: CapUnit | undefined, locale: string): string {
|
|
14
|
+
try {
|
|
15
|
+
if (unit === "mb") {
|
|
16
|
+
const scaled = megabyteUnit(value);
|
|
17
|
+
return new Intl.NumberFormat(locale, {
|
|
18
|
+
style: "unit",
|
|
19
|
+
unit: scaled.unit,
|
|
20
|
+
unitDisplay: "short",
|
|
21
|
+
maximumFractionDigits: 1,
|
|
22
|
+
}).format(value / scaled.divisor);
|
|
23
|
+
}
|
|
24
|
+
return new Intl.NumberFormat(locale).format(value);
|
|
25
|
+
} catch {
|
|
26
|
+
return String(value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatCapUsedOfLimit(
|
|
31
|
+
used: number,
|
|
32
|
+
limit: number,
|
|
33
|
+
unit: CapUnit | undefined,
|
|
34
|
+
locale: string,
|
|
35
|
+
): string {
|
|
36
|
+
return `${formatCapAmount(used, unit, locale)} / ${formatCapAmount(limit, unit, locale)}`;
|
|
37
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { ExtensionSectionProps } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
useLocale,
|
|
4
|
+
useNav,
|
|
5
|
+
usePrimitives,
|
|
6
|
+
useQuery,
|
|
7
|
+
useTranslation,
|
|
8
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
9
|
+
import { EMPTY_VALUE_PLACEHOLDER, type Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
4
10
|
import {
|
|
5
11
|
EmptyState,
|
|
6
12
|
StatCard,
|
|
@@ -9,9 +15,11 @@ import {
|
|
|
9
15
|
} from "@cosmicdrift/kumiko-renderer/web";
|
|
10
16
|
import { Database, FileText, Gauge, Hash, Mail, Users } from "lucide-react";
|
|
11
17
|
import type { ComponentType, ReactNode } from "react";
|
|
12
|
-
import type { CapIconKey, CapUsageTone, CapUsageWithMeta } from "../types.ts";
|
|
18
|
+
import type { CapIconKey, CapUnit, CapUsageTone, CapUsageWithMeta } from "../types.ts";
|
|
19
|
+
import { formatCapAmount, formatCapUsedOfLimit } from "./cap-amount-format.ts";
|
|
13
20
|
import { CapUsageBar } from "./cap-usage-bar.tsx";
|
|
14
|
-
import { CapOverviewQueries } from "./constants.ts";
|
|
21
|
+
import { CapOverviewQueries, MY_CAPS_SCREEN_ID } from "./constants.ts";
|
|
22
|
+
import { isCapNotIncluded } from "./usage-math.ts";
|
|
15
23
|
|
|
16
24
|
type CapsUsageResponse = { readonly rows: readonly CapUsageWithMeta[] };
|
|
17
25
|
|
|
@@ -41,6 +49,105 @@ function isMeasured(cap: CapUsageWithMeta): cap is CapUsageWithMeta & { used: nu
|
|
|
41
49
|
return cap.used !== null;
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
// Boundary check: only an array of strings restricts; anything else means no restriction.
|
|
53
|
+
function readCapKeys(value: unknown): ReadonlySet<string> | undefined {
|
|
54
|
+
if (!Array.isArray(value)) return undefined;
|
|
55
|
+
return new Set(value.filter((entry): entry is string => typeof entry === "string"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ViewAllLink(): ReactNode {
|
|
59
|
+
const t = useTranslation();
|
|
60
|
+
const { Link } = usePrimitives();
|
|
61
|
+
const nav = useNav();
|
|
62
|
+
const target = { screenId: MY_CAPS_SCREEN_ID };
|
|
63
|
+
return (
|
|
64
|
+
<Link
|
|
65
|
+
href={nav.hrefFor(target)}
|
|
66
|
+
onPress={() => nav.navigate(target)}
|
|
67
|
+
testId="cap-cards-view-all"
|
|
68
|
+
>
|
|
69
|
+
{t("cap-overview.cards.viewAll")}
|
|
70
|
+
</Link>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function CapUsageSummaryRow({ cap }: { readonly cap: CapUsageWithMeta }): ReactNode {
|
|
75
|
+
const t = useTranslation();
|
|
76
|
+
const locale = useLocale().locale();
|
|
77
|
+
const remainingText =
|
|
78
|
+
isMeasured(cap) && cap.limit !== null && cap.limit > 0
|
|
79
|
+
? remainingLabel(t, Math.max(cap.limit - cap.used, 0), cap.unit, locale)
|
|
80
|
+
: undefined;
|
|
81
|
+
if (cap.percent === null && remainingText === undefined) return null;
|
|
82
|
+
return (
|
|
83
|
+
<div className="flex items-center gap-2">
|
|
84
|
+
{cap.percent !== null && (
|
|
85
|
+
<StatusBadge tone={TONE_TO_STATUS[cap.tone]}>{`${cap.percent}%`}</StatusBadge>
|
|
86
|
+
)}
|
|
87
|
+
{remainingText !== undefined && (
|
|
88
|
+
<span className="text-sm text-muted-foreground" data-testid="cap-card-remaining">
|
|
89
|
+
{remainingText}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function remainingLabel(
|
|
97
|
+
t: Translate,
|
|
98
|
+
rest: number,
|
|
99
|
+
unit: CapUnit | undefined,
|
|
100
|
+
locale: string,
|
|
101
|
+
): string {
|
|
102
|
+
return rest > 0
|
|
103
|
+
? t("cap-overview.cards.remaining", { amount: formatCapAmount(rest, unit, locale) })
|
|
104
|
+
: t("cap-overview.cards.noneLeft");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function CapUsageStat({
|
|
108
|
+
cap,
|
|
109
|
+
footer,
|
|
110
|
+
}: {
|
|
111
|
+
readonly cap: CapUsageWithMeta;
|
|
112
|
+
readonly footer?: ReactNode;
|
|
113
|
+
}): ReactNode {
|
|
114
|
+
const t = useTranslation();
|
|
115
|
+
const locale = useLocale().locale();
|
|
116
|
+
const Icon = cap.icon !== undefined ? CAP_ICONS[cap.icon] : undefined;
|
|
117
|
+
// An unlimited, measured cap has neither a badge nor a bar; StatCard still
|
|
118
|
+
// wraps any defined child in a spacing div, so pass undefined then.
|
|
119
|
+
const notIncluded = isCapNotIncluded(cap.limit);
|
|
120
|
+
const body =
|
|
121
|
+
!notIncluded && (!isMeasured(cap) || cap.limit !== null) ? (
|
|
122
|
+
<div className="flex flex-col gap-2">
|
|
123
|
+
<CapUsageSummaryRow cap={cap} />
|
|
124
|
+
<CapUsageBar usage={cap} showLabel={false} />
|
|
125
|
+
{footer}
|
|
126
|
+
</div>
|
|
127
|
+
) : (
|
|
128
|
+
footer
|
|
129
|
+
);
|
|
130
|
+
return (
|
|
131
|
+
<StatCard
|
|
132
|
+
icon={Icon !== undefined ? <Icon className="size-4" /> : undefined}
|
|
133
|
+
label={t(cap.label)}
|
|
134
|
+
value={
|
|
135
|
+
notIncluded
|
|
136
|
+
? t("cap-overview.notIncluded")
|
|
137
|
+
: !isMeasured(cap)
|
|
138
|
+
? EMPTY_VALUE_PLACEHOLDER
|
|
139
|
+
: cap.limit === null
|
|
140
|
+
? formatCapAmount(cap.used, cap.unit, locale)
|
|
141
|
+
: formatCapUsedOfLimit(cap.used, cap.limit, cap.unit, locale)
|
|
142
|
+
}
|
|
143
|
+
{...(cap.accentColor !== undefined && { accentColor: cap.accentColor })}
|
|
144
|
+
testId="cap-card"
|
|
145
|
+
>
|
|
146
|
+
{body}
|
|
147
|
+
</StatCard>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
44
151
|
export function CapCardsPanel({ filterParams }: ExtensionSectionProps): ReactNode {
|
|
45
152
|
const t = useTranslation();
|
|
46
153
|
const { Banner } = usePrimitives();
|
|
@@ -58,62 +165,38 @@ export function CapCardsPanel({ filterParams }: ExtensionSectionProps): ReactNod
|
|
|
58
165
|
);
|
|
59
166
|
}
|
|
60
167
|
|
|
61
|
-
const
|
|
168
|
+
const capKeys = readCapKeys(filterParams?.["capKeys"]);
|
|
169
|
+
const allRows = query.data?.rows ?? [];
|
|
170
|
+
const rows = capKeys === undefined ? allRows : allRows.filter((row) => capKeys.has(row.id));
|
|
62
171
|
|
|
63
172
|
if (!query.loading && rows.length === 0) {
|
|
64
173
|
return <EmptyState title={t("cap-overview.cards.empty")} testId="cap-cards-panel-empty" />;
|
|
65
174
|
}
|
|
66
175
|
|
|
176
|
+
// A single limited card carries the link itself; several share one under the grid.
|
|
177
|
+
const linkInCard = capKeys !== undefined && rows.length === 1;
|
|
67
178
|
return (
|
|
68
|
-
<div
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<div className="flex flex-col gap-2">
|
|
90
|
-
{cap.percent !== null && (
|
|
91
|
-
<div className="flex items-center justify-between gap-2">
|
|
92
|
-
<StatusBadge tone={TONE_TO_STATUS[cap.tone]}>{`${cap.percent}%`}</StatusBadge>
|
|
93
|
-
</div>
|
|
94
|
-
)}
|
|
95
|
-
<CapUsageBar usage={cap} showLabel={false} />
|
|
96
|
-
</div>
|
|
97
|
-
) : undefined;
|
|
98
|
-
return (
|
|
99
|
-
<StatCard
|
|
100
|
-
key={cap.id}
|
|
101
|
-
icon={Icon !== undefined ? <Icon className="size-4" /> : undefined}
|
|
102
|
-
label={t(cap.label)}
|
|
103
|
-
value={
|
|
104
|
-
!isMeasured(cap)
|
|
105
|
-
? EMPTY_VALUE_PLACEHOLDER
|
|
106
|
-
: cap.limit === null
|
|
107
|
-
? String(cap.used)
|
|
108
|
-
: `${cap.used} / ${cap.limit}`
|
|
109
|
-
}
|
|
110
|
-
{...(cap.accentColor !== undefined && { accentColor: cap.accentColor })}
|
|
111
|
-
testId="cap-card"
|
|
112
|
-
>
|
|
113
|
-
{body}
|
|
114
|
-
</StatCard>
|
|
115
|
-
);
|
|
116
|
-
})}
|
|
179
|
+
<div className="flex flex-col gap-2">
|
|
180
|
+
<div
|
|
181
|
+
// Arbitrary bracket values (grid-cols-[repeat(...)]) never compiled:
|
|
182
|
+
// styles.css's Tailwind v4 @source scan only covers renderer/web/src,
|
|
183
|
+
// renderer/src and samples/**/src (see its comment) — bundled-features
|
|
184
|
+
// isn't scanned, so a class unique to this file has no generated rule
|
|
185
|
+
// and silently no-ops (the grid fell back to one implicit column,
|
|
186
|
+
// stacking every card full-width). Reusing the exact classes
|
|
187
|
+
// dashboard-body.tsx's own panel grid already uses guarantees the
|
|
188
|
+
// rule exists, since that file IS in the scanned surface.
|
|
189
|
+
className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"
|
|
190
|
+
data-testid="cap-cards-panel"
|
|
191
|
+
>
|
|
192
|
+
{query.loading && rows.length === 0 && (
|
|
193
|
+
<div className="text-sm text-muted-foreground">{t("cap-overview.cards.loading")}</div>
|
|
194
|
+
)}
|
|
195
|
+
{rows.map((cap) => (
|
|
196
|
+
<CapUsageStat key={cap.id} cap={cap} footer={linkInCard ? <ViewAllLink /> : undefined} />
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
{capKeys !== undefined && !linkInCard && <ViewAllLink />}
|
|
117
200
|
</div>
|
|
118
201
|
);
|
|
119
202
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
1
|
+
import { useLocale, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import type { CapUsage } from "../types.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { formatCapAmount, formatCapUsedOfLimit } from "./cap-amount-format.ts";
|
|
5
|
+
import { computeTone, isCapNotIncluded } from "./usage-math.ts";
|
|
5
6
|
|
|
6
7
|
export function CapUsageBar({
|
|
7
8
|
usage,
|
|
@@ -14,6 +15,15 @@ export function CapUsageBar({
|
|
|
14
15
|
}): ReactNode {
|
|
15
16
|
const { Progress, Text, Banner } = usePrimitives();
|
|
16
17
|
const t = useTranslation();
|
|
18
|
+
const locale = useLocale().locale();
|
|
19
|
+
|
|
20
|
+
if (isCapNotIncluded(usage.limit)) {
|
|
21
|
+
return (
|
|
22
|
+
<Text variant="small" testId="cap-usage-not-included">
|
|
23
|
+
{t("cap-overview.notIncluded")}
|
|
24
|
+
</Text>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
if (usage.used === null) {
|
|
19
29
|
return (
|
|
@@ -27,7 +37,7 @@ export function CapUsageBar({
|
|
|
27
37
|
// A progress bar with no denominator says nothing — show the raw count instead.
|
|
28
38
|
return showLabel ? (
|
|
29
39
|
<Text variant="small" testId="cap-usage-unlimited">
|
|
30
|
-
{t("cap-overview.unlimited", { used: usage.used })}
|
|
40
|
+
{t("cap-overview.unlimited", { used: formatCapAmount(usage.used, usage.unit, locale) })}
|
|
31
41
|
</Text>
|
|
32
42
|
) : null;
|
|
33
43
|
}
|
|
@@ -49,7 +59,11 @@ export function CapUsageBar({
|
|
|
49
59
|
{t("cap-overview.errors.progressPrimitiveMissing")}
|
|
50
60
|
</Banner>
|
|
51
61
|
)}
|
|
52
|
-
{showLabel &&
|
|
62
|
+
{showLabel && (
|
|
63
|
+
<Text variant="small">
|
|
64
|
+
{formatCapUsedOfLimit(usage.used, usage.limit, usage.unit, locale)}
|
|
65
|
+
</Text>
|
|
66
|
+
)}
|
|
53
67
|
</div>
|
|
54
68
|
);
|
|
55
69
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ColumnRendererProps } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
-
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { isOneOf, isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
-
import type { CapUsage } from "../types.ts";
|
|
4
|
+
import type { CapUnit, CapUsage } from "../types.ts";
|
|
5
5
|
import { CapUsageBar } from "./cap-usage-bar.tsx";
|
|
6
6
|
|
|
7
|
+
const CAP_UNITS: readonly CapUnit[] = ["count", "mb", "tokens"];
|
|
8
|
+
|
|
7
9
|
function isCapUsage(value: unknown): value is CapUsage {
|
|
8
10
|
if (!isPlainObject(value)) return false;
|
|
9
11
|
const used = value["used"];
|
|
@@ -11,7 +13,8 @@ function isCapUsage(value: unknown): value is CapUsage {
|
|
|
11
13
|
return (
|
|
12
14
|
(typeof used === "number" || used === null) &&
|
|
13
15
|
(typeof limit === "number" || limit === null) &&
|
|
14
|
-
typeof value["fraction"] === "number"
|
|
16
|
+
typeof value["fraction"] === "number" &&
|
|
17
|
+
(value["unit"] === undefined || isOneOf(CAP_UNITS, value["unit"]))
|
|
15
18
|
);
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -8,9 +8,13 @@ const DANGER_THRESHOLD = 1;
|
|
|
8
8
|
|
|
9
9
|
// limit === null means unlimited (no cap to measure against); limit <= 0
|
|
10
10
|
// means "not part of this tier" — both render as 0, never NaN/Infinity.
|
|
11
|
+
export function isCapNotIncluded(limit: number | null): boolean {
|
|
12
|
+
return limit !== null && limit <= 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
function rawFraction(used: number, limit: number | null): number {
|
|
12
16
|
if (limit === null) return 0;
|
|
13
|
-
if (limit
|
|
17
|
+
if (isCapNotIncluded(limit)) return 0;
|
|
14
18
|
const fraction = used / limit;
|
|
15
19
|
return Number.isFinite(fraction) ? fraction : 0;
|
|
16
20
|
}
|