@contractspec/example.saas-boilerplate 3.7.7 → 3.8.4
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/.turbo/turbo-build.log +36 -24
- package/CHANGELOG.md +72 -0
- package/README.md +1 -0
- package/dist/browser/index.js +371 -92
- package/dist/browser/saas-boilerplate.feature.js +208 -0
- package/dist/browser/ui/SaasDashboard.js +311 -60
- package/dist/browser/ui/SaasDashboard.visualizations.js +249 -0
- package/dist/browser/ui/index.js +362 -92
- package/dist/browser/ui/renderers/index.js +229 -3
- package/dist/browser/ui/renderers/project-list.markdown.js +229 -3
- package/dist/browser/visualizations/catalog.js +155 -0
- package/dist/browser/visualizations/index.js +217 -0
- package/dist/browser/visualizations/selectors.js +210 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +371 -92
- package/dist/node/index.js +371 -92
- package/dist/node/saas-boilerplate.feature.js +208 -0
- package/dist/node/ui/SaasDashboard.js +311 -60
- package/dist/node/ui/SaasDashboard.visualizations.js +249 -0
- package/dist/node/ui/index.js +362 -92
- package/dist/node/ui/renderers/index.js +229 -3
- package/dist/node/ui/renderers/project-list.markdown.js +229 -3
- package/dist/node/visualizations/catalog.js +155 -0
- package/dist/node/visualizations/index.js +217 -0
- package/dist/node/visualizations/selectors.js +210 -0
- package/dist/saas-boilerplate.feature.js +208 -0
- package/dist/ui/SaasDashboard.js +311 -60
- package/dist/ui/SaasDashboard.visualizations.d.ts +5 -0
- package/dist/ui/SaasDashboard.visualizations.js +250 -0
- package/dist/ui/index.js +362 -92
- package/dist/ui/renderers/index.js +229 -3
- package/dist/ui/renderers/project-list.markdown.d.ts +1 -1
- package/dist/ui/renderers/project-list.markdown.js +229 -3
- package/dist/ui/renderers/project-list.renderer.d.ts +1 -1
- package/dist/visualizations/catalog.d.ts +11 -0
- package/dist/visualizations/catalog.js +156 -0
- package/dist/visualizations/index.d.ts +2 -0
- package/dist/visualizations/index.js +218 -0
- package/dist/visualizations/selectors.d.ts +8 -0
- package/dist/visualizations/selectors.js +211 -0
- package/dist/visualizations/selectors.test.d.ts +1 -0
- package/package.json +70 -13
- package/src/index.ts +1 -0
- package/src/saas-boilerplate.feature.ts +3 -0
- package/src/ui/SaasDashboard.tsx +8 -0
- package/src/ui/SaasDashboard.visualizations.tsx +41 -0
- package/src/ui/renderers/project-list.markdown.ts +39 -15
- package/src/ui/renderers/project-list.renderer.tsx +1 -1
- package/src/visualizations/catalog.ts +153 -0
- package/src/visualizations/index.ts +2 -0
- package/src/visualizations/selectors.test.ts +25 -0
- package/src/visualizations/selectors.ts +85 -0
|
@@ -344,6 +344,213 @@ function createSaasHandlers(db) {
|
|
|
344
344
|
getSubscription
|
|
345
345
|
};
|
|
346
346
|
}
|
|
347
|
+
// src/visualizations/catalog.ts
|
|
348
|
+
import {
|
|
349
|
+
defineVisualization,
|
|
350
|
+
VisualizationRegistry
|
|
351
|
+
} from "@contractspec/lib.contracts-spec/visualizations";
|
|
352
|
+
var PROJECT_LIST_REF = {
|
|
353
|
+
key: "saas.project.list",
|
|
354
|
+
version: "1.0.0"
|
|
355
|
+
};
|
|
356
|
+
var META = {
|
|
357
|
+
version: "1.0.0",
|
|
358
|
+
domain: "saas",
|
|
359
|
+
stability: "experimental",
|
|
360
|
+
owners: ["@example.saas-boilerplate"],
|
|
361
|
+
tags: ["saas", "visualization", "projects"]
|
|
362
|
+
};
|
|
363
|
+
var SaasProjectUsageVisualization = defineVisualization({
|
|
364
|
+
meta: {
|
|
365
|
+
...META,
|
|
366
|
+
key: "saas-boilerplate.visualization.project-usage",
|
|
367
|
+
title: "Project Capacity",
|
|
368
|
+
description: "Current project count against the current plan limit.",
|
|
369
|
+
goal: "Show usage against the active plan allowance.",
|
|
370
|
+
context: "SaaS account overview."
|
|
371
|
+
},
|
|
372
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
373
|
+
visualization: {
|
|
374
|
+
kind: "metric",
|
|
375
|
+
measure: "totalProjects",
|
|
376
|
+
comparisonMeasure: "projectLimit",
|
|
377
|
+
measures: [
|
|
378
|
+
{
|
|
379
|
+
key: "totalProjects",
|
|
380
|
+
label: "Projects",
|
|
381
|
+
dataPath: "totalProjects",
|
|
382
|
+
format: "number"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
key: "projectLimit",
|
|
386
|
+
label: "Plan Limit",
|
|
387
|
+
dataPath: "projectLimit",
|
|
388
|
+
format: "number"
|
|
389
|
+
}
|
|
390
|
+
],
|
|
391
|
+
table: { caption: "Current project count and plan limit." }
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
var SaasProjectStatusVisualization = defineVisualization({
|
|
395
|
+
meta: {
|
|
396
|
+
...META,
|
|
397
|
+
key: "saas-boilerplate.visualization.project-status",
|
|
398
|
+
title: "Project Status",
|
|
399
|
+
description: "Distribution of project states.",
|
|
400
|
+
goal: "Show the mix of active, draft, and archived projects.",
|
|
401
|
+
context: "Project portfolio overview."
|
|
402
|
+
},
|
|
403
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
404
|
+
visualization: {
|
|
405
|
+
kind: "pie",
|
|
406
|
+
nameDimension: "status",
|
|
407
|
+
valueMeasure: "projects",
|
|
408
|
+
dimensions: [
|
|
409
|
+
{ key: "status", label: "Status", dataPath: "status", type: "category" }
|
|
410
|
+
],
|
|
411
|
+
measures: [
|
|
412
|
+
{
|
|
413
|
+
key: "projects",
|
|
414
|
+
label: "Projects",
|
|
415
|
+
dataPath: "projects",
|
|
416
|
+
format: "number"
|
|
417
|
+
}
|
|
418
|
+
],
|
|
419
|
+
table: { caption: "Project counts by status." }
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
var SaasProjectTierVisualization = defineVisualization({
|
|
423
|
+
meta: {
|
|
424
|
+
...META,
|
|
425
|
+
key: "saas-boilerplate.visualization.project-tiers",
|
|
426
|
+
title: "Tier Comparison",
|
|
427
|
+
description: "Distribution of projects across tiers.",
|
|
428
|
+
goal: "Compare how the current portfolio is distributed by tier.",
|
|
429
|
+
context: "Plan and packaging overview."
|
|
430
|
+
},
|
|
431
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
432
|
+
visualization: {
|
|
433
|
+
kind: "cartesian",
|
|
434
|
+
variant: "bar",
|
|
435
|
+
xDimension: "tier",
|
|
436
|
+
yMeasures: ["projects"],
|
|
437
|
+
dimensions: [
|
|
438
|
+
{ key: "tier", label: "Tier", dataPath: "tier", type: "category" }
|
|
439
|
+
],
|
|
440
|
+
measures: [
|
|
441
|
+
{
|
|
442
|
+
key: "projects",
|
|
443
|
+
label: "Projects",
|
|
444
|
+
dataPath: "projects",
|
|
445
|
+
format: "number",
|
|
446
|
+
color: "#1d4ed8"
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
table: { caption: "Project counts by tier." }
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
var SaasProjectActivityVisualization = defineVisualization({
|
|
453
|
+
meta: {
|
|
454
|
+
...META,
|
|
455
|
+
key: "saas-boilerplate.visualization.project-activity",
|
|
456
|
+
title: "Recent Project Activity",
|
|
457
|
+
description: "Daily project creation activity.",
|
|
458
|
+
goal: "Show recent project activity over time.",
|
|
459
|
+
context: "Project portfolio trend view."
|
|
460
|
+
},
|
|
461
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
462
|
+
visualization: {
|
|
463
|
+
kind: "cartesian",
|
|
464
|
+
variant: "line",
|
|
465
|
+
xDimension: "day",
|
|
466
|
+
yMeasures: ["projects"],
|
|
467
|
+
dimensions: [{ key: "day", label: "Day", dataPath: "day", type: "time" }],
|
|
468
|
+
measures: [
|
|
469
|
+
{
|
|
470
|
+
key: "projects",
|
|
471
|
+
label: "Projects",
|
|
472
|
+
dataPath: "projects",
|
|
473
|
+
format: "number",
|
|
474
|
+
color: "#0f766e"
|
|
475
|
+
}
|
|
476
|
+
],
|
|
477
|
+
table: { caption: "Daily project creation counts." }
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
var SaasVisualizationSpecs = [
|
|
481
|
+
SaasProjectUsageVisualization,
|
|
482
|
+
SaasProjectStatusVisualization,
|
|
483
|
+
SaasProjectTierVisualization,
|
|
484
|
+
SaasProjectActivityVisualization
|
|
485
|
+
];
|
|
486
|
+
var SaasVisualizationRegistry = new VisualizationRegistry([
|
|
487
|
+
...SaasVisualizationSpecs
|
|
488
|
+
]);
|
|
489
|
+
var SaasVisualizationRefs = SaasVisualizationSpecs.map((spec) => ({
|
|
490
|
+
key: spec.meta.key,
|
|
491
|
+
version: spec.meta.version
|
|
492
|
+
}));
|
|
493
|
+
|
|
494
|
+
// src/visualizations/selectors.ts
|
|
495
|
+
function toDayKey(value) {
|
|
496
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
497
|
+
return date.toISOString().slice(0, 10);
|
|
498
|
+
}
|
|
499
|
+
function createSaasVisualizationItems(projects, projectLimit = 10) {
|
|
500
|
+
const statusCounts = new Map;
|
|
501
|
+
const tierCounts = new Map;
|
|
502
|
+
const activityCounts = new Map;
|
|
503
|
+
for (const project of projects) {
|
|
504
|
+
statusCounts.set(project.status, (statusCounts.get(project.status) ?? 0) + 1);
|
|
505
|
+
tierCounts.set(project.tier, (tierCounts.get(project.tier) ?? 0) + 1);
|
|
506
|
+
const day = toDayKey(project.createdAt);
|
|
507
|
+
activityCounts.set(day, (activityCounts.get(day) ?? 0) + 1);
|
|
508
|
+
}
|
|
509
|
+
return [
|
|
510
|
+
{
|
|
511
|
+
key: "saas-capacity",
|
|
512
|
+
spec: SaasProjectUsageVisualization,
|
|
513
|
+
data: { data: [{ totalProjects: projects.length, projectLimit }] },
|
|
514
|
+
title: "Project Capacity",
|
|
515
|
+
description: "Current project count compared to the active limit.",
|
|
516
|
+
height: 220
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
key: "saas-status",
|
|
520
|
+
spec: SaasProjectStatusVisualization,
|
|
521
|
+
data: {
|
|
522
|
+
data: Array.from(statusCounts.entries()).map(([status, count]) => ({
|
|
523
|
+
status,
|
|
524
|
+
projects: count
|
|
525
|
+
}))
|
|
526
|
+
},
|
|
527
|
+
title: "Project Status",
|
|
528
|
+
description: "Status mix across the current project portfolio.",
|
|
529
|
+
height: 260
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
key: "saas-tier",
|
|
533
|
+
spec: SaasProjectTierVisualization,
|
|
534
|
+
data: {
|
|
535
|
+
data: Array.from(tierCounts.entries()).map(([tier, count]) => ({
|
|
536
|
+
tier,
|
|
537
|
+
projects: count
|
|
538
|
+
}))
|
|
539
|
+
},
|
|
540
|
+
title: "Tier Comparison",
|
|
541
|
+
description: "How projects are distributed across tiers."
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
key: "saas-activity",
|
|
545
|
+
spec: SaasProjectActivityVisualization,
|
|
546
|
+
data: {
|
|
547
|
+
data: Array.from(activityCounts.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([day, count]) => ({ day, projects: count }))
|
|
548
|
+
},
|
|
549
|
+
title: "Recent Project Activity",
|
|
550
|
+
description: "Daily project creation activity."
|
|
551
|
+
}
|
|
552
|
+
];
|
|
553
|
+
}
|
|
347
554
|
// src/ui/hooks/useProjectList.ts
|
|
348
555
|
import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
|
|
349
556
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
@@ -409,6 +616,18 @@ function useProjectList(options = {}) {
|
|
|
409
616
|
}
|
|
410
617
|
|
|
411
618
|
// src/ui/renderers/project-list.markdown.ts
|
|
619
|
+
var PROJECT_TIERS = [
|
|
620
|
+
"FREE",
|
|
621
|
+
"PRO",
|
|
622
|
+
"ENTERPRISE"
|
|
623
|
+
];
|
|
624
|
+
function toVisualizationProject(project, index) {
|
|
625
|
+
return {
|
|
626
|
+
status: project.status === "DELETED" ? "ARCHIVED" : project.status,
|
|
627
|
+
tier: PROJECT_TIERS[index % PROJECT_TIERS.length] ?? "FREE",
|
|
628
|
+
createdAt: project.createdAt
|
|
629
|
+
};
|
|
630
|
+
}
|
|
412
631
|
var projectListMarkdownRenderer = {
|
|
413
632
|
target: "markdown",
|
|
414
633
|
render: async (desc, _ctx) => {
|
|
@@ -419,7 +638,7 @@ var projectListMarkdownRenderer = {
|
|
|
419
638
|
limit: 20,
|
|
420
639
|
offset: 0
|
|
421
640
|
});
|
|
422
|
-
const items = data.projects ??
|
|
641
|
+
const items = data.projects ?? [];
|
|
423
642
|
const lines = [
|
|
424
643
|
"# Projects",
|
|
425
644
|
"",
|
|
@@ -456,6 +675,7 @@ var saasDashboardMarkdownRenderer = {
|
|
|
456
675
|
const projects = projectsData.projects ?? [];
|
|
457
676
|
const activeProjects = projects.filter((p) => p.status === "ACTIVE").length;
|
|
458
677
|
const archivedProjects = projects.filter((p) => p.status === "ARCHIVED").length;
|
|
678
|
+
const visualizations = createSaasVisualizationItems(projects.map(toVisualizationProject), 10);
|
|
459
679
|
const lines = [
|
|
460
680
|
"# SaaS Dashboard",
|
|
461
681
|
"",
|
|
@@ -470,10 +690,16 @@ var saasDashboardMarkdownRenderer = {
|
|
|
470
690
|
`| Archived Projects | ${archivedProjects} |`,
|
|
471
691
|
`| Subscription Plan | ${subscription.planName} |`,
|
|
472
692
|
`| Subscription Status | ${subscription.status} |`,
|
|
473
|
-
"",
|
|
474
|
-
"## Projects",
|
|
475
693
|
""
|
|
476
694
|
];
|
|
695
|
+
lines.push("## Visualization Overview");
|
|
696
|
+
lines.push("");
|
|
697
|
+
for (const item of visualizations) {
|
|
698
|
+
lines.push(`- **${item.title}** via \`${item.spec.meta.key}\``);
|
|
699
|
+
}
|
|
700
|
+
lines.push("");
|
|
701
|
+
lines.push("## Projects");
|
|
702
|
+
lines.push("");
|
|
477
703
|
if (projects.length === 0) {
|
|
478
704
|
lines.push("_No projects yet._");
|
|
479
705
|
} else {
|
|
@@ -344,7 +344,226 @@ function createSaasHandlers(db) {
|
|
|
344
344
|
getSubscription
|
|
345
345
|
};
|
|
346
346
|
}
|
|
347
|
+
// src/visualizations/catalog.ts
|
|
348
|
+
import {
|
|
349
|
+
defineVisualization,
|
|
350
|
+
VisualizationRegistry
|
|
351
|
+
} from "@contractspec/lib.contracts-spec/visualizations";
|
|
352
|
+
var PROJECT_LIST_REF = {
|
|
353
|
+
key: "saas.project.list",
|
|
354
|
+
version: "1.0.0"
|
|
355
|
+
};
|
|
356
|
+
var META = {
|
|
357
|
+
version: "1.0.0",
|
|
358
|
+
domain: "saas",
|
|
359
|
+
stability: "experimental",
|
|
360
|
+
owners: ["@example.saas-boilerplate"],
|
|
361
|
+
tags: ["saas", "visualization", "projects"]
|
|
362
|
+
};
|
|
363
|
+
var SaasProjectUsageVisualization = defineVisualization({
|
|
364
|
+
meta: {
|
|
365
|
+
...META,
|
|
366
|
+
key: "saas-boilerplate.visualization.project-usage",
|
|
367
|
+
title: "Project Capacity",
|
|
368
|
+
description: "Current project count against the current plan limit.",
|
|
369
|
+
goal: "Show usage against the active plan allowance.",
|
|
370
|
+
context: "SaaS account overview."
|
|
371
|
+
},
|
|
372
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
373
|
+
visualization: {
|
|
374
|
+
kind: "metric",
|
|
375
|
+
measure: "totalProjects",
|
|
376
|
+
comparisonMeasure: "projectLimit",
|
|
377
|
+
measures: [
|
|
378
|
+
{
|
|
379
|
+
key: "totalProjects",
|
|
380
|
+
label: "Projects",
|
|
381
|
+
dataPath: "totalProjects",
|
|
382
|
+
format: "number"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
key: "projectLimit",
|
|
386
|
+
label: "Plan Limit",
|
|
387
|
+
dataPath: "projectLimit",
|
|
388
|
+
format: "number"
|
|
389
|
+
}
|
|
390
|
+
],
|
|
391
|
+
table: { caption: "Current project count and plan limit." }
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
var SaasProjectStatusVisualization = defineVisualization({
|
|
395
|
+
meta: {
|
|
396
|
+
...META,
|
|
397
|
+
key: "saas-boilerplate.visualization.project-status",
|
|
398
|
+
title: "Project Status",
|
|
399
|
+
description: "Distribution of project states.",
|
|
400
|
+
goal: "Show the mix of active, draft, and archived projects.",
|
|
401
|
+
context: "Project portfolio overview."
|
|
402
|
+
},
|
|
403
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
404
|
+
visualization: {
|
|
405
|
+
kind: "pie",
|
|
406
|
+
nameDimension: "status",
|
|
407
|
+
valueMeasure: "projects",
|
|
408
|
+
dimensions: [
|
|
409
|
+
{ key: "status", label: "Status", dataPath: "status", type: "category" }
|
|
410
|
+
],
|
|
411
|
+
measures: [
|
|
412
|
+
{
|
|
413
|
+
key: "projects",
|
|
414
|
+
label: "Projects",
|
|
415
|
+
dataPath: "projects",
|
|
416
|
+
format: "number"
|
|
417
|
+
}
|
|
418
|
+
],
|
|
419
|
+
table: { caption: "Project counts by status." }
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
var SaasProjectTierVisualization = defineVisualization({
|
|
423
|
+
meta: {
|
|
424
|
+
...META,
|
|
425
|
+
key: "saas-boilerplate.visualization.project-tiers",
|
|
426
|
+
title: "Tier Comparison",
|
|
427
|
+
description: "Distribution of projects across tiers.",
|
|
428
|
+
goal: "Compare how the current portfolio is distributed by tier.",
|
|
429
|
+
context: "Plan and packaging overview."
|
|
430
|
+
},
|
|
431
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
432
|
+
visualization: {
|
|
433
|
+
kind: "cartesian",
|
|
434
|
+
variant: "bar",
|
|
435
|
+
xDimension: "tier",
|
|
436
|
+
yMeasures: ["projects"],
|
|
437
|
+
dimensions: [
|
|
438
|
+
{ key: "tier", label: "Tier", dataPath: "tier", type: "category" }
|
|
439
|
+
],
|
|
440
|
+
measures: [
|
|
441
|
+
{
|
|
442
|
+
key: "projects",
|
|
443
|
+
label: "Projects",
|
|
444
|
+
dataPath: "projects",
|
|
445
|
+
format: "number",
|
|
446
|
+
color: "#1d4ed8"
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
table: { caption: "Project counts by tier." }
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
var SaasProjectActivityVisualization = defineVisualization({
|
|
453
|
+
meta: {
|
|
454
|
+
...META,
|
|
455
|
+
key: "saas-boilerplate.visualization.project-activity",
|
|
456
|
+
title: "Recent Project Activity",
|
|
457
|
+
description: "Daily project creation activity.",
|
|
458
|
+
goal: "Show recent project activity over time.",
|
|
459
|
+
context: "Project portfolio trend view."
|
|
460
|
+
},
|
|
461
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
462
|
+
visualization: {
|
|
463
|
+
kind: "cartesian",
|
|
464
|
+
variant: "line",
|
|
465
|
+
xDimension: "day",
|
|
466
|
+
yMeasures: ["projects"],
|
|
467
|
+
dimensions: [{ key: "day", label: "Day", dataPath: "day", type: "time" }],
|
|
468
|
+
measures: [
|
|
469
|
+
{
|
|
470
|
+
key: "projects",
|
|
471
|
+
label: "Projects",
|
|
472
|
+
dataPath: "projects",
|
|
473
|
+
format: "number",
|
|
474
|
+
color: "#0f766e"
|
|
475
|
+
}
|
|
476
|
+
],
|
|
477
|
+
table: { caption: "Daily project creation counts." }
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
var SaasVisualizationSpecs = [
|
|
481
|
+
SaasProjectUsageVisualization,
|
|
482
|
+
SaasProjectStatusVisualization,
|
|
483
|
+
SaasProjectTierVisualization,
|
|
484
|
+
SaasProjectActivityVisualization
|
|
485
|
+
];
|
|
486
|
+
var SaasVisualizationRegistry = new VisualizationRegistry([
|
|
487
|
+
...SaasVisualizationSpecs
|
|
488
|
+
]);
|
|
489
|
+
var SaasVisualizationRefs = SaasVisualizationSpecs.map((spec) => ({
|
|
490
|
+
key: spec.meta.key,
|
|
491
|
+
version: spec.meta.version
|
|
492
|
+
}));
|
|
493
|
+
|
|
494
|
+
// src/visualizations/selectors.ts
|
|
495
|
+
function toDayKey(value) {
|
|
496
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
497
|
+
return date.toISOString().slice(0, 10);
|
|
498
|
+
}
|
|
499
|
+
function createSaasVisualizationItems(projects, projectLimit = 10) {
|
|
500
|
+
const statusCounts = new Map;
|
|
501
|
+
const tierCounts = new Map;
|
|
502
|
+
const activityCounts = new Map;
|
|
503
|
+
for (const project of projects) {
|
|
504
|
+
statusCounts.set(project.status, (statusCounts.get(project.status) ?? 0) + 1);
|
|
505
|
+
tierCounts.set(project.tier, (tierCounts.get(project.tier) ?? 0) + 1);
|
|
506
|
+
const day = toDayKey(project.createdAt);
|
|
507
|
+
activityCounts.set(day, (activityCounts.get(day) ?? 0) + 1);
|
|
508
|
+
}
|
|
509
|
+
return [
|
|
510
|
+
{
|
|
511
|
+
key: "saas-capacity",
|
|
512
|
+
spec: SaasProjectUsageVisualization,
|
|
513
|
+
data: { data: [{ totalProjects: projects.length, projectLimit }] },
|
|
514
|
+
title: "Project Capacity",
|
|
515
|
+
description: "Current project count compared to the active limit.",
|
|
516
|
+
height: 220
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
key: "saas-status",
|
|
520
|
+
spec: SaasProjectStatusVisualization,
|
|
521
|
+
data: {
|
|
522
|
+
data: Array.from(statusCounts.entries()).map(([status, count]) => ({
|
|
523
|
+
status,
|
|
524
|
+
projects: count
|
|
525
|
+
}))
|
|
526
|
+
},
|
|
527
|
+
title: "Project Status",
|
|
528
|
+
description: "Status mix across the current project portfolio.",
|
|
529
|
+
height: 260
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
key: "saas-tier",
|
|
533
|
+
spec: SaasProjectTierVisualization,
|
|
534
|
+
data: {
|
|
535
|
+
data: Array.from(tierCounts.entries()).map(([tier, count]) => ({
|
|
536
|
+
tier,
|
|
537
|
+
projects: count
|
|
538
|
+
}))
|
|
539
|
+
},
|
|
540
|
+
title: "Tier Comparison",
|
|
541
|
+
description: "How projects are distributed across tiers."
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
key: "saas-activity",
|
|
545
|
+
spec: SaasProjectActivityVisualization,
|
|
546
|
+
data: {
|
|
547
|
+
data: Array.from(activityCounts.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([day, count]) => ({ day, projects: count }))
|
|
548
|
+
},
|
|
549
|
+
title: "Recent Project Activity",
|
|
550
|
+
description: "Daily project creation activity."
|
|
551
|
+
}
|
|
552
|
+
];
|
|
553
|
+
}
|
|
347
554
|
// src/ui/renderers/project-list.markdown.ts
|
|
555
|
+
var PROJECT_TIERS = [
|
|
556
|
+
"FREE",
|
|
557
|
+
"PRO",
|
|
558
|
+
"ENTERPRISE"
|
|
559
|
+
];
|
|
560
|
+
function toVisualizationProject(project, index) {
|
|
561
|
+
return {
|
|
562
|
+
status: project.status === "DELETED" ? "ARCHIVED" : project.status,
|
|
563
|
+
tier: PROJECT_TIERS[index % PROJECT_TIERS.length] ?? "FREE",
|
|
564
|
+
createdAt: project.createdAt
|
|
565
|
+
};
|
|
566
|
+
}
|
|
348
567
|
var projectListMarkdownRenderer = {
|
|
349
568
|
target: "markdown",
|
|
350
569
|
render: async (desc, _ctx) => {
|
|
@@ -355,7 +574,7 @@ var projectListMarkdownRenderer = {
|
|
|
355
574
|
limit: 20,
|
|
356
575
|
offset: 0
|
|
357
576
|
});
|
|
358
|
-
const items = data.projects ??
|
|
577
|
+
const items = data.projects ?? [];
|
|
359
578
|
const lines = [
|
|
360
579
|
"# Projects",
|
|
361
580
|
"",
|
|
@@ -392,6 +611,7 @@ var saasDashboardMarkdownRenderer = {
|
|
|
392
611
|
const projects = projectsData.projects ?? [];
|
|
393
612
|
const activeProjects = projects.filter((p) => p.status === "ACTIVE").length;
|
|
394
613
|
const archivedProjects = projects.filter((p) => p.status === "ARCHIVED").length;
|
|
614
|
+
const visualizations = createSaasVisualizationItems(projects.map(toVisualizationProject), 10);
|
|
395
615
|
const lines = [
|
|
396
616
|
"# SaaS Dashboard",
|
|
397
617
|
"",
|
|
@@ -406,10 +626,16 @@ var saasDashboardMarkdownRenderer = {
|
|
|
406
626
|
`| Archived Projects | ${archivedProjects} |`,
|
|
407
627
|
`| Subscription Plan | ${subscription.planName} |`,
|
|
408
628
|
`| Subscription Status | ${subscription.status} |`,
|
|
409
|
-
"",
|
|
410
|
-
"## Projects",
|
|
411
629
|
""
|
|
412
630
|
];
|
|
631
|
+
lines.push("## Visualization Overview");
|
|
632
|
+
lines.push("");
|
|
633
|
+
for (const item of visualizations) {
|
|
634
|
+
lines.push(`- **${item.title}** via \`${item.spec.meta.key}\``);
|
|
635
|
+
}
|
|
636
|
+
lines.push("");
|
|
637
|
+
lines.push("## Projects");
|
|
638
|
+
lines.push("");
|
|
413
639
|
if (projects.length === 0) {
|
|
414
640
|
lines.push("_No projects yet._");
|
|
415
641
|
} else {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/visualizations/catalog.ts
|
|
2
|
+
import {
|
|
3
|
+
defineVisualization,
|
|
4
|
+
VisualizationRegistry
|
|
5
|
+
} from "@contractspec/lib.contracts-spec/visualizations";
|
|
6
|
+
var PROJECT_LIST_REF = {
|
|
7
|
+
key: "saas.project.list",
|
|
8
|
+
version: "1.0.0"
|
|
9
|
+
};
|
|
10
|
+
var META = {
|
|
11
|
+
version: "1.0.0",
|
|
12
|
+
domain: "saas",
|
|
13
|
+
stability: "experimental",
|
|
14
|
+
owners: ["@example.saas-boilerplate"],
|
|
15
|
+
tags: ["saas", "visualization", "projects"]
|
|
16
|
+
};
|
|
17
|
+
var SaasProjectUsageVisualization = defineVisualization({
|
|
18
|
+
meta: {
|
|
19
|
+
...META,
|
|
20
|
+
key: "saas-boilerplate.visualization.project-usage",
|
|
21
|
+
title: "Project Capacity",
|
|
22
|
+
description: "Current project count against the current plan limit.",
|
|
23
|
+
goal: "Show usage against the active plan allowance.",
|
|
24
|
+
context: "SaaS account overview."
|
|
25
|
+
},
|
|
26
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
27
|
+
visualization: {
|
|
28
|
+
kind: "metric",
|
|
29
|
+
measure: "totalProjects",
|
|
30
|
+
comparisonMeasure: "projectLimit",
|
|
31
|
+
measures: [
|
|
32
|
+
{
|
|
33
|
+
key: "totalProjects",
|
|
34
|
+
label: "Projects",
|
|
35
|
+
dataPath: "totalProjects",
|
|
36
|
+
format: "number"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "projectLimit",
|
|
40
|
+
label: "Plan Limit",
|
|
41
|
+
dataPath: "projectLimit",
|
|
42
|
+
format: "number"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
table: { caption: "Current project count and plan limit." }
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
var SaasProjectStatusVisualization = defineVisualization({
|
|
49
|
+
meta: {
|
|
50
|
+
...META,
|
|
51
|
+
key: "saas-boilerplate.visualization.project-status",
|
|
52
|
+
title: "Project Status",
|
|
53
|
+
description: "Distribution of project states.",
|
|
54
|
+
goal: "Show the mix of active, draft, and archived projects.",
|
|
55
|
+
context: "Project portfolio overview."
|
|
56
|
+
},
|
|
57
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
58
|
+
visualization: {
|
|
59
|
+
kind: "pie",
|
|
60
|
+
nameDimension: "status",
|
|
61
|
+
valueMeasure: "projects",
|
|
62
|
+
dimensions: [
|
|
63
|
+
{ key: "status", label: "Status", dataPath: "status", type: "category" }
|
|
64
|
+
],
|
|
65
|
+
measures: [
|
|
66
|
+
{
|
|
67
|
+
key: "projects",
|
|
68
|
+
label: "Projects",
|
|
69
|
+
dataPath: "projects",
|
|
70
|
+
format: "number"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
table: { caption: "Project counts by status." }
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
var SaasProjectTierVisualization = defineVisualization({
|
|
77
|
+
meta: {
|
|
78
|
+
...META,
|
|
79
|
+
key: "saas-boilerplate.visualization.project-tiers",
|
|
80
|
+
title: "Tier Comparison",
|
|
81
|
+
description: "Distribution of projects across tiers.",
|
|
82
|
+
goal: "Compare how the current portfolio is distributed by tier.",
|
|
83
|
+
context: "Plan and packaging overview."
|
|
84
|
+
},
|
|
85
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
86
|
+
visualization: {
|
|
87
|
+
kind: "cartesian",
|
|
88
|
+
variant: "bar",
|
|
89
|
+
xDimension: "tier",
|
|
90
|
+
yMeasures: ["projects"],
|
|
91
|
+
dimensions: [
|
|
92
|
+
{ key: "tier", label: "Tier", dataPath: "tier", type: "category" }
|
|
93
|
+
],
|
|
94
|
+
measures: [
|
|
95
|
+
{
|
|
96
|
+
key: "projects",
|
|
97
|
+
label: "Projects",
|
|
98
|
+
dataPath: "projects",
|
|
99
|
+
format: "number",
|
|
100
|
+
color: "#1d4ed8"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
table: { caption: "Project counts by tier." }
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
var SaasProjectActivityVisualization = defineVisualization({
|
|
107
|
+
meta: {
|
|
108
|
+
...META,
|
|
109
|
+
key: "saas-boilerplate.visualization.project-activity",
|
|
110
|
+
title: "Recent Project Activity",
|
|
111
|
+
description: "Daily project creation activity.",
|
|
112
|
+
goal: "Show recent project activity over time.",
|
|
113
|
+
context: "Project portfolio trend view."
|
|
114
|
+
},
|
|
115
|
+
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
116
|
+
visualization: {
|
|
117
|
+
kind: "cartesian",
|
|
118
|
+
variant: "line",
|
|
119
|
+
xDimension: "day",
|
|
120
|
+
yMeasures: ["projects"],
|
|
121
|
+
dimensions: [{ key: "day", label: "Day", dataPath: "day", type: "time" }],
|
|
122
|
+
measures: [
|
|
123
|
+
{
|
|
124
|
+
key: "projects",
|
|
125
|
+
label: "Projects",
|
|
126
|
+
dataPath: "projects",
|
|
127
|
+
format: "number",
|
|
128
|
+
color: "#0f766e"
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
table: { caption: "Daily project creation counts." }
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
var SaasVisualizationSpecs = [
|
|
135
|
+
SaasProjectUsageVisualization,
|
|
136
|
+
SaasProjectStatusVisualization,
|
|
137
|
+
SaasProjectTierVisualization,
|
|
138
|
+
SaasProjectActivityVisualization
|
|
139
|
+
];
|
|
140
|
+
var SaasVisualizationRegistry = new VisualizationRegistry([
|
|
141
|
+
...SaasVisualizationSpecs
|
|
142
|
+
]);
|
|
143
|
+
var SaasVisualizationRefs = SaasVisualizationSpecs.map((spec) => ({
|
|
144
|
+
key: spec.meta.key,
|
|
145
|
+
version: spec.meta.version
|
|
146
|
+
}));
|
|
147
|
+
export {
|
|
148
|
+
SaasVisualizationSpecs,
|
|
149
|
+
SaasVisualizationRegistry,
|
|
150
|
+
SaasVisualizationRefs,
|
|
151
|
+
SaasProjectUsageVisualization,
|
|
152
|
+
SaasProjectTierVisualization,
|
|
153
|
+
SaasProjectStatusVisualization,
|
|
154
|
+
SaasProjectActivityVisualization
|
|
155
|
+
};
|