@datashow/super-visual-cli 0.0.0-stage → 0.1.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/bin/supervisual.mjs +9 -0
  2. package/package.json +45 -4
  3. package/src/fabric-client.mjs +101 -0
  4. package/src/main.mjs +663 -0
  5. package/src/project-target.mjs +80 -0
  6. package/src/project.mjs +289 -0
  7. package/src/semantic-preview.mjs +528 -0
  8. package/templates/blank/README.md +26 -0
  9. package/templates/blank/contracts/page-set.yaml +78 -0
  10. package/templates/blank/contracts/report-source.json +30 -0
  11. package/templates/blank/contracts/semantic-model.yaml +39 -0
  12. package/templates/blank/contracts/widget-broker.json +33 -0
  13. package/templates/blank/deployment/CERTIFICATION.md +18 -0
  14. package/templates/blank/package.json +15 -0
  15. package/templates/blank/source/report-design.less +20 -0
  16. package/templates/blank/source/reportApplication.tsx +29 -0
  17. package/templates/blank/supervisual.yaml +24 -0
  18. package/templates/hello-world/README.md +29 -0
  19. package/templates/hello-world/contracts/page-set.yaml +74 -0
  20. package/templates/hello-world/contracts/report-source.json +45 -0
  21. package/templates/hello-world/contracts/semantic-model.yaml +32 -0
  22. package/templates/hello-world/contracts/widget-broker.json +37 -0
  23. package/templates/hello-world/deployment/CERTIFICATION.md +17 -0
  24. package/templates/hello-world/package.json +15 -0
  25. package/templates/hello-world/source/report-design.less +35 -0
  26. package/templates/hello-world/source/reportApplication.tsx +69 -0
  27. package/templates/hello-world/supervisual.yaml +24 -0
  28. package/templates/semantic-preview/index.html +14 -0
  29. package/templates/semantic-preview/src/main.tsx +172 -0
  30. package/README.md +0 -3
@@ -0,0 +1,39 @@
1
+ contractVersion: 1
2
+ name: {{PROJECT_TITLE}}
3
+ culture: en-AU
4
+ compatibilityLevel: 1702
5
+ storageMode: import
6
+ sourceParameters:
7
+ server: localhost
8
+ database: SuperVisualDemo
9
+ tables:
10
+ - name: Sales
11
+ role: fact
12
+ grain: One row per example category.
13
+ description: Minimal self-contained sales data used by the Super Visual hello-world project.
14
+ source:
15
+ referencedTables: []
16
+ sql: |-
17
+ SELECT example.[Category], CAST(example.[Amount] AS decimal(19, 4)) AS [Amount]
18
+ FROM (VALUES (N'Bikes', 42000), (N'Clothing', 18000), (N'Accessories', 27000)) AS example([Category], [Amount])
19
+ columns:
20
+ - name: Category
21
+ sourceColumn: Category
22
+ dataType: String
23
+ hidden: false
24
+ summarizeBy: none
25
+ description: Example product category.
26
+ - name: Amount
27
+ sourceColumn: Amount
28
+ dataType: Decimal
29
+ hidden: true
30
+ summarizeBy: none
31
+ description: Example sales amount.
32
+ measures:
33
+ - name: Total Sales
34
+ expression: "SUM('Sales'[Amount])"
35
+ formatString: "$#,##0"
36
+ displayFolder: Measures
37
+ description: Total example sales in the current category context.
38
+ hierarchies: []
39
+ relationships: []
@@ -0,0 +1,33 @@
1
+ {
2
+ "contractVersion": 1,
3
+ "summary": "One governed categorical dataset for the hello-world chart.",
4
+ "adapterMode": "source-direct-import",
5
+ "baseModelMutation": false,
6
+ "dataView": {
7
+ "mapping": "table",
8
+ "discriminator": "WidgetId",
9
+ "fields": ["WidgetId", "AxisKey", "Label", "Sort", "SeriesKey", "SeriesLabel", "WidgetValue", "BrokerContext"]
10
+ },
11
+ "widgets": [
12
+ {
13
+ "id": "sales",
14
+ "title": "Sales by category",
15
+ "reportWidgetIds": ["sales"],
16
+ "businessQuestionIds": ["sales-mix"],
17
+ "shape": "categorical",
18
+ "dimensions": [{ "key": "Sales[Category]", "label": "Sales[Category]" }],
19
+ "axis": "Sales[Category]",
20
+ "measures": ["[Total Sales]"],
21
+ "rowLimit": 10,
22
+ "rowSelection": "top-by-value",
23
+ "selectable": false,
24
+ "invalidates": []
25
+ }
26
+ ],
27
+ "cachePolicy": {
28
+ "retainSourceWidget": true,
29
+ "cancelStaleRequests": true,
30
+ "externalFilterInvalidation": "all-dependent-widgets"
31
+ },
32
+ "risks": []
33
+ }
@@ -0,0 +1,18 @@
1
+ # Live certification
2
+
3
+ First update the semantic and broker contracts for the selected Power BI
4
+ semantic model. Initialize this project with that model's workspace and
5
+ `--semantic-model-id`, then run the current live workflow:
6
+
7
+ ```bash
8
+ npx supervisual preview test --json
9
+ npx supervisual publish --json
10
+ ```
11
+
12
+ Review the returned workspace, operations, and plan ID. Do not apply until a
13
+ human authorizes that exact plan ID and target workspace. Apply with
14
+ `npx supervisual publish --confirm <authorized-plan-id> --json`.
15
+
16
+ Only an `accepted` result and a successful browser check constitute a passing
17
+ live certification. The selected semantic-model ID is the only model ID users
18
+ provide.
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "{{PROJECT_ID}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "check": "supervisual check .",
8
+ "compile": "supervisual compile .",
9
+ "package": "supervisual package .",
10
+ "verify": "supervisual verify ."
11
+ },
12
+ "devDependencies": {
13
+ "@datashow/super-visual-cli": "0.1.0-rc.0"
14
+ }
15
+ }
@@ -0,0 +1,20 @@
1
+ .hello-world {
2
+ box-sizing: border-box;
3
+ min-height: 100%;
4
+ padding: 32px;
5
+ color: #173c36;
6
+ background: #f4f7f5;
7
+ font-family: Aptos, "Segoe UI", sans-serif;
8
+ }
9
+
10
+ .hello-world .eyebrow { margin: 0 0 8px; color: #167d6c; font-size: 12px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
11
+ .hello-world h1 { margin: 0; font: 600 clamp(36px, 6vw, 64px)/1 Georgia, serif; }
12
+ .hello-world .lede { max-width: 620px; color: #5e6d68; font-size: 17px; }
13
+ .hello-world section { margin-top: 28px; padding: 24px; border: 1px solid #d3ddd8; border-radius: 8px; background: #fff; }
14
+ .hello-world h2 { margin: 0 0 16px; font-size: 18px; }
15
+ .hello-world .total { font: 600 42px/1 Georgia, serif; }
16
+ .hello-world .bars { display: grid; gap: 14px; margin: 0; padding: 0; list-style: none; }
17
+ .hello-world .bars li { display: grid; grid-template-columns: minmax(90px, 1fr) 3fr minmax(70px, auto); gap: 12px; align-items: center; }
18
+ .hello-world .track { height: 14px; overflow: hidden; border-radius: 7px; background: #eaf0ed; }
19
+ .hello-world .bar { display: block; height: 100%; border-radius: inherit; background: #167d6c; }
20
+ @media (max-width: 640px) { .hello-world { padding: 18px; } .hello-world .bars li { grid-template-columns: 1fr auto; } .hello-world .track { grid-column: 1 / -1; } }
@@ -0,0 +1,29 @@
1
+ import { useEffect, type ReactElement } from "react";
2
+ import type { ReportApplicationProps } from "./reportRuntime";
3
+
4
+ export function ReportApplication({ model, onVisibleDatasetsChange }: ReportApplicationProps): ReactElement {
5
+ useEffect(() => onVisibleDatasetsChange?.(["sales"]), [onVisibleDatasetsChange]);
6
+ const dataset = model.datasets.sales;
7
+ const maximum = Math.max(1, ...(dataset?.points.map((point) => point.value) ?? [1]));
8
+ const total = dataset?.points.reduce((sum, point) => sum + point.value, 0) ?? 0;
9
+
10
+ return <main className="hello-world">
11
+ <p className="eyebrow">Super Visual / minimal example</p>
12
+ <h1>Hello, governed data.</h1>
13
+ <p className="lede">This React page is bound only through ReportApplicationProps.</p>
14
+ <section aria-labelledby="total-sales">
15
+ <h2 id="total-sales">Total sales</h2>
16
+ <strong className="total">{new Intl.NumberFormat(undefined, { style: "currency", currency: "AUD", maximumFractionDigits: 0 }).format(total)}</strong>
17
+ </section>
18
+ <section aria-labelledby="sales-by-category">
19
+ <h2 id="sales-by-category">Sales by category</h2>
20
+ {!dataset?.points.length ? <p>Waiting for Power BI data…</p> : <ol className="bars">
21
+ {dataset.points.map((point) => <li key={point.key}>
22
+ <span>{point.label}</span>
23
+ <span className="track"><span className="bar" style={{ width: `${Math.max(2, (point.value / maximum) * 100)}%` }} /></span>
24
+ <strong>{new Intl.NumberFormat().format(point.value)}</strong>
25
+ </li>)}
26
+ </ol>}
27
+ </section>
28
+ </main>;
29
+ }
@@ -0,0 +1,24 @@
1
+ contractVersion: 1
2
+ application:
3
+ id: {{PROJECT_ID}}
4
+ title: {{PROJECT_TITLE}}
5
+ entry: contracts/report-source.json
6
+ style: contracts/report-source.json
7
+ target:
8
+ platform: powerbi
9
+ visual:
10
+ name: {{VISUAL_NAME}}
11
+ displayName: {{PROJECT_TITLE}}
12
+ guid: {{VISUAL_GUID}}
13
+ version: 1.0.0.1
14
+ apiVersion: 5.11.1
15
+ semantic:
16
+ model: contracts/semantic-model.yaml
17
+ broker: contracts/widget-broker.json
18
+ pageSet: contracts/page-set.yaml
19
+ source:
20
+ bundle: contracts/report-source.json
21
+ output:
22
+ directory: dist
23
+ compliance:
24
+ policies: [core, powerbi]
@@ -0,0 +1,29 @@
1
+ # {{PROJECT_TITLE}}
2
+
3
+ This is the smallest complete Super Visual connectivity proof. Its React page
4
+ shows a searchable catalog of the entities declared by the selected Power BI
5
+ semantic model. Visible rows prove the governed data path without exposing
6
+ expressions, parameter values, or business rows.
7
+
8
+ This project is ready for the three-command workflow. If `init` was given a
9
+ workspace and semantic model, run:
10
+
11
+ ```bash
12
+ npx supervisual dev --open
13
+ npx supervisual publish
14
+ ```
15
+
16
+ `publish` performs the checks, packaging, verification, and planning, then asks
17
+ for confirmation of the exact plan ID. For a project without a saved binding,
18
+ pass the IDs once:
19
+
20
+ ```bash
21
+ npx supervisual dev . --workspace-id <workspace-guid> \
22
+ --semantic-model-id <semantic-model-guid> --open
23
+ npx supervisual preview test . --workspace-id <workspace-guid> \
24
+ --semantic-model-id <semantic-model-guid> --json
25
+ ```
26
+
27
+ Point the project at another semantic-model ID and the catalog updates
28
+ automatically. Application source receives data only through
29
+ `ReportApplicationProps`.
@@ -0,0 +1,74 @@
1
+ contractVersion: 1
2
+ shell:
3
+ id: {{PROJECT_ID}}
4
+ title: {{PROJECT_TITLE}}
5
+ defaultPage: overview
6
+ designSystem:
7
+ id: hello-world
8
+ sha256: {{DESIGN_HASH}}
9
+ acceptedConcept: { id: {{PROJECT_ID}} }
10
+ governanceLabel: Power BI governed
11
+ locale: en-AU
12
+ currencyCode: ''
13
+ filterContext: []
14
+ navigation:
15
+ - { page: overview, label: Model catalog }
16
+ theme:
17
+ background: '#F4F7F5'
18
+ surface: '#FFFFFF'
19
+ quietSurface: '#EAF0ED'
20
+ text: '#173C36'
21
+ mutedText: '#5E6D68'
22
+ metadata: '#71807A'
23
+ rule: '#D3DDD8'
24
+ grid: '#E3EAE6'
25
+ accent: '#167D6C'
26
+ attention: '#9B6B23'
27
+ positive: '#2E7D5B'
28
+ negative: '#B84A52'
29
+ context: ['#167D6C', '#6F6A9A']
30
+ dataColors: ['#167D6C', '#6F6A9A', '#9B6B23']
31
+ displayFont: Georgia, serif
32
+ bodyFont: Aptos, Segoe UI, sans-serif
33
+ contentWidth: 1200
34
+ layoutColumns: 12
35
+ layoutGap: 16
36
+ cardRadius: 8
37
+ cardPadding: [20, 20, 20, 20]
38
+ kpiTopAccent: 3
39
+ density: comfortable
40
+ typography: { findingMin: 40, findingMax: 56, sectionTitle: 22, kpiValue: 32, body: 15, chart: 12, table: 13, metadata: 11 }
41
+ pages:
42
+ - id: overview
43
+ title: Semantic Model Catalog
44
+ eyebrow: Super Visual / connectivity proof
45
+ summary:
46
+ headline: Hello, semantic model.
47
+ body: Search the declared model entities returned through the governed Super Visual broker.
48
+ emphasisBinding: null
49
+ search:
50
+ placeholder: Search type, parent, or name
51
+ dataset: model-catalog
52
+ filterTarget: Super Visual Model Catalog[Entity]
53
+ kpis: []
54
+ widgets:
55
+ - id: model-catalog
56
+ conceptRegionId: model-catalog
57
+ primitive: ranked-table
58
+ title: Semantic model entities
59
+ subtitle: Read from the selected live Power BI semantic model
60
+ dataset: model-catalog
61
+ reportWidgetIds: [model-catalog]
62
+ businessQuestionIds: [semantic-connectivity]
63
+ layout: { columns: 12, minHeight: 420 }
64
+ settings: { orientation: null, curve: null, showLegend: false, showLabels: true, sort: ascending, limit: 100, emphasis: null }
65
+ interactions: []
66
+ designRationale:
67
+ narrative: Make the smallest sample an inspectable proof that React is receiving governed semantic-model rows.
68
+ decisions:
69
+ - Replace arbitrary business data with a generated semantic-model catalog.
70
+ - Keep search client-side so it is immediate and does not create model filters.
71
+ accessibility:
72
+ - Use a labelled search field, semantic table headings, visible connection status, and text counts.
73
+ risks:
74
+ - Catalog rows prove the report-to-semantic-provider path; source refresh remains a separate deployment acceptance check.
@@ -0,0 +1,45 @@
1
+ {
2
+ "contractVersion": 1,
3
+ "designSystem": {
4
+ "id": "hello-world",
5
+ "sha256": "{{DESIGN_HASH}}"
6
+ },
7
+ "acceptedConcept": {
8
+ "id": "{{PROJECT_ID}}"
9
+ },
10
+ "entry": "src/reportApplication.tsx",
11
+ "files": [
12
+ {
13
+ "path": "src/reportApplication.tsx",
14
+ "content": "Template source is synchronized from source/reportApplication.tsx during initialization."
15
+ },
16
+ {
17
+ "path": "style/report-design.less",
18
+ "content": "Template style is synchronized from source/report-design.less during initialization."
19
+ }
20
+ ],
21
+ "routes": [
22
+ {
23
+ "id": "overview",
24
+ "title": "Semantic Model Catalog",
25
+ "datasets": ["model-catalog"],
26
+ "businessQuestionIds": ["semantic-connectivity"]
27
+ }
28
+ ],
29
+ "bindings": [
30
+ {
31
+ "dataset": "model-catalog",
32
+ "component": "searchable-table",
33
+ "reportWidgetIds": ["model-catalog"],
34
+ "businessQuestionIds": ["semantic-connectivity"]
35
+ }
36
+ ],
37
+ "designRationale": {
38
+ "direction": "A searchable semantic-model catalog that proves the governed provider path.",
39
+ "pageCompositions": ["One semantic-model catalog page"],
40
+ "dataVisualisation": ["Searchable semantic-entity table and entity-type counts"],
41
+ "responsiveBehaviour": ["Summary cards wrap and the semantic table scrolls horizontally on narrow screens."],
42
+ "accessibility": ["Labelled search input, semantic table headings, visible numeric counts, and a live connection status."],
43
+ "risks": ["Catalog rows prove the report-to-semantic-provider path; source refresh remains a separate deployment acceptance check."]
44
+ }
45
+ }
@@ -0,0 +1,32 @@
1
+ contractVersion: 1
2
+ name: {{PROJECT_TITLE}}
3
+ culture: en-AU
4
+ compatibilityLevel: 1702
5
+ storageMode: import
6
+ sourceParameters:
7
+ server: localhost
8
+ database: SuperVisualDemo
9
+ tables:
10
+ - name: Hello World
11
+ role: dimension
12
+ grain: One row containing the connectivity greeting.
13
+ description: Minimal source-backed table used to prove model refresh and catalog generation.
14
+ source:
15
+ referencedTables: []
16
+ sql: |-
17
+ SELECT N'Hello, semantic model.' AS [Message]
18
+ columns:
19
+ - name: Message
20
+ sourceColumn: Message
21
+ dataType: String
22
+ hidden: false
23
+ summarizeBy: none
24
+ description: Greeting returned by the source-backed connectivity table.
25
+ measures:
26
+ - name: Row Count
27
+ expression: "COUNTROWS('Hello World')"
28
+ formatString: "0"
29
+ displayFolder: Connectivity
30
+ description: Count of rows returned by the connectivity table.
31
+ hierarchies: []
32
+ relationships: []
@@ -0,0 +1,37 @@
1
+ {
2
+ "contractVersion": 1,
3
+ "summary": "Generated searchable semantic-model catalog used as an end-to-end connectivity proof.",
4
+ "adapterMode": "source-direct-import",
5
+ "baseModelMutation": false,
6
+ "dataView": {
7
+ "mapping": "table",
8
+ "discriminator": "WidgetId",
9
+ "fields": ["WidgetId", "AxisKey", "Label", "Sort", "SeriesKey", "SeriesLabel", "WidgetValue", "BrokerContext"]
10
+ },
11
+ "widgets": [
12
+ {
13
+ "id": "model-catalog",
14
+ "title": "Semantic model entities",
15
+ "reportWidgetIds": ["model-catalog"],
16
+ "businessQuestionIds": ["semantic-connectivity"],
17
+ "shape": "hierarchy",
18
+ "dimensions": [
19
+ { "key": "Super Visual Model Catalog[Entity Type]", "label": "Super Visual Model Catalog[Entity Type]" },
20
+ { "key": "Super Visual Model Catalog[Parent]", "label": "Super Visual Model Catalog[Parent]" },
21
+ { "key": "Super Visual Model Catalog[Entity]", "label": "Super Visual Model Catalog[Entity]" }
22
+ ],
23
+ "axis": "Super Visual Model Catalog[Entity]",
24
+ "measures": ["[Entity Count]"],
25
+ "rowLimit": 250,
26
+ "rowSelection": "first-by-axis",
27
+ "selectable": false,
28
+ "invalidates": []
29
+ }
30
+ ],
31
+ "cachePolicy": {
32
+ "retainSourceWidget": true,
33
+ "cancelStaleRequests": true,
34
+ "externalFilterInvalidation": "all-dependent-widgets"
35
+ },
36
+ "risks": ["The catalog lists declared model metadata but does not expose expressions, parameter values, or business rows."]
37
+ }
@@ -0,0 +1,17 @@
1
+ # Hello World live certification
2
+
3
+ Initialize this project with a workspace and Power BI semantic-model ID using
4
+ `--semantic-model-id`, then run the current live workflow:
5
+
6
+ ```bash
7
+ npx supervisual preview test --json
8
+ npx supervisual publish --json
9
+ ```
10
+
11
+ Review the returned workspace, operations, and plan ID. Do not apply until a
12
+ human authorizes that exact plan ID and target workspace. Apply with
13
+ `npx supervisual publish --confirm <authorized-plan-id> --json`.
14
+
15
+ Only an `accepted` result and a successful browser check constitute a passing
16
+ live certification. The selected semantic-model ID is the only model ID users
17
+ provide.
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "{{PROJECT_ID}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "check": "supervisual check .",
8
+ "compile": "supervisual compile .",
9
+ "package": "supervisual package .",
10
+ "verify": "supervisual verify ."
11
+ },
12
+ "devDependencies": {
13
+ "@datashow/super-visual-cli": "0.1.0-rc.0"
14
+ }
15
+ }
@@ -0,0 +1,35 @@
1
+ .hello-world { box-sizing: border-box; min-height: 100%; padding: 32px; color: #173c36; background: #f4f7f5; font-family: Aptos, "Segoe UI", sans-serif; }
2
+ .hello-world * { box-sizing: border-box; }
3
+ .hero { display: flex; gap: 32px; align-items: flex-start; justify-content: space-between; max-width: 1200px; margin: 0 auto; }
4
+ .eyebrow { margin: 0 0 8px; color: #167d6c; font-size: 12px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
5
+ .hero h1 { margin: 0; font: 600 clamp(36px, 6vw, 64px)/1 Georgia, serif; }
6
+ .lede { max-width: 720px; margin: 16px 0 0; color: #5e6d68; font-size: 17px; line-height: 1.5; }
7
+ .connection { display: inline-flex; flex: 0 0 auto; gap: 8px; align-items: center; margin: 4px 0 0; padding: 9px 13px; border: 1px solid #d3ddd8; border-radius: 999px; background: #fff; color: #5e6d68; font-size: 13px; font-weight: 700; }
8
+ .connection span { width: 9px; height: 9px; border-radius: 50%; background: #9b6b23; }
9
+ .connection.connected span { background: #2e7d5b; box-shadow: 0 0 0 3px #dcebe5; }
10
+ .summary { display: flex; flex-wrap: wrap; gap: 1px; max-width: 1200px; margin: 28px auto 0; overflow: hidden; border: 1px solid #d3ddd8; border-radius: 8px; background: #d3ddd8; }
11
+ .summary div { display: grid; min-width: 128px; flex: 1 1 128px; gap: 3px; padding: 16px 18px; background: #fff; }
12
+ .summary strong { font: 600 28px/1 Georgia, serif; }
13
+ .summary span { color: #5e6d68; font-size: 12px; }
14
+ .catalog { max-width: 1200px; margin: 18px auto 0; overflow: hidden; border: 1px solid #d3ddd8; border-radius: 8px; background: #fff; }
15
+ .catalog-heading { display: flex; gap: 24px; align-items: end; justify-content: space-between; padding: 22px 24px; border-bottom: 1px solid #d3ddd8; }
16
+ .catalog-heading h2 { margin: 0; font-size: 20px; }
17
+ .catalog-heading p { margin: 6px 0 0; color: #5e6d68; font-size: 13px; }
18
+ .search { display: grid; min-width: min(340px, 100%); gap: 6px; color: #5e6d68; font-size: 12px; font-weight: 700; }
19
+ .search input { width: 100%; padding: 10px 12px; border: 1px solid #b9c7c0; border-radius: 6px; color: #173c36; background: #fff; font: inherit; font-size: 14px; font-weight: 400; }
20
+ .search input:focus { outline: 3px solid #b8ddd5; outline-offset: 1px; border-color: #167d6c; }
21
+ .table-scroll { overflow-x: auto; }
22
+ .catalog table { width: 100%; border-collapse: collapse; text-align: left; }
23
+ .catalog th, .catalog td { padding: 13px 24px; border-bottom: 1px solid #e3eae6; }
24
+ .catalog thead th { color: #5e6d68; background: #f8faf9; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
25
+ .catalog tbody th { font-weight: 600; }
26
+ .catalog tbody td { color: #5e6d68; }
27
+ .catalog tbody tr:last-child th, .catalog tbody tr:last-child td { border-bottom: 0; }
28
+ .type { display: inline-block; padding: 4px 8px; border-radius: 999px; color: #126858; background: #e5f2ee; font-size: 11px; font-weight: 700; }
29
+ .empty { margin: 0; padding: 32px 24px; color: #5e6d68; }
30
+ @media (max-width: 720px) {
31
+ .hello-world { padding: 20px; }
32
+ .hero, .catalog-heading { align-items: stretch; flex-direction: column; }
33
+ .search { min-width: 0; }
34
+ .catalog th, .catalog td { padding: 12px 16px; }
35
+ }
@@ -0,0 +1,69 @@
1
+ import { useEffect, useMemo, useState, type ReactElement } from "react";
2
+ import type { ReportApplicationProps } from "./reportRuntime";
3
+
4
+ interface CatalogRow {
5
+ type: string;
6
+ parent: string;
7
+ entity: string;
8
+ }
9
+
10
+ const TYPE_ORDER = ["Model", "Table", "Column", "Measure", "Hierarchy", "Hierarchy level", "Relationship", "Parameter"];
11
+ const TYPE_PLURALS: Record<string, string> = { Hierarchy: "Hierarchies", "Hierarchy level": "Hierarchy levels" };
12
+
13
+ export function ReportApplication({ model, onVisibleDatasetsChange }: ReportApplicationProps): ReactElement {
14
+ const [query, setQuery] = useState("");
15
+ useEffect(() => onVisibleDatasetsChange?.(["model-catalog"]), [onVisibleDatasetsChange]);
16
+
17
+ const dataset = model.datasets["model-catalog"];
18
+ const rows = useMemo<CatalogRow[]>(() => (dataset?.points ?? [])
19
+ .map((point) => ({
20
+ type: point.dimensions[0]?.label ?? "Entity",
21
+ parent: point.dimensions[1]?.label ?? "",
22
+ entity: point.dimensions[2]?.label ?? point.label,
23
+ }))
24
+ .sort((left, right) => (TYPE_ORDER.indexOf(left.type) - TYPE_ORDER.indexOf(right.type))
25
+ || left.parent.localeCompare(right.parent)
26
+ || left.entity.localeCompare(right.entity)), [dataset]);
27
+
28
+ const normalizedQuery = query.trim().toLowerCase();
29
+ const visibleRows = normalizedQuery
30
+ ? rows.filter((row) => [row.type, row.parent, row.entity].some((value) => value.toLowerCase().includes(normalizedQuery)))
31
+ : rows;
32
+ const counts = rows.reduce<Record<string, number>>((result, row) => {
33
+ result[row.type] = (result[row.type] ?? 0) + 1;
34
+ return result;
35
+ }, {});
36
+ const countTypes = [...TYPE_ORDER.filter((type) => counts[type] !== undefined), ...Object.keys(counts).filter((type) => !TYPE_ORDER.includes(type)).sort()];
37
+ const connected = rows.length > 0;
38
+
39
+ return <main className="hello-world">
40
+ <header className="hero">
41
+ <div>
42
+ <p className="eyebrow">Super Visual / connectivity proof</p>
43
+ <h1>Hello, semantic model.</h1>
44
+ <p className="lede">This searchable catalog arrived through the same governed semantic provider used by every Super Visual application.</p>
45
+ </div>
46
+ <p className={connected ? "connection connected" : "connection waiting"} role="status">
47
+ <span aria-hidden="true" />
48
+ {connected ? "Connected · " + rows.length + " entities" : "Waiting for semantic data…"}
49
+ </p>
50
+ </header>
51
+
52
+ <section className="summary" aria-label="Semantic model summary">
53
+ {countTypes.map((type) => <div key={type}><strong>{counts[type]}</strong><span>{counts[type] === 1 ? type : TYPE_PLURALS[type] ?? `${type}s`}</span></div>)}
54
+ </section>
55
+
56
+ <section className="catalog" aria-labelledby="catalog-title">
57
+ <div className="catalog-heading">
58
+ <div><h2 id="catalog-title">Semantic model entities</h2><p>Tables, columns, measures, hierarchies, relationships, and parameters declared by the model.</p></div>
59
+ <label className="search"><span>Search entities</span><input type="search" value={query} onChange={(event) => setQuery(event.currentTarget.value)} placeholder="Search type, parent, or name" /></label>
60
+ </div>
61
+ {!connected ? <p className="empty">The page will populate when the selected semantic model returns its first rows.</p>
62
+ : visibleRows.length === 0 ? <p className="empty">No semantic entities match “{query}”.</p>
63
+ : <div className="table-scroll"><table>
64
+ <thead><tr><th scope="col">Type</th><th scope="col">Parent</th><th scope="col">Entity</th></tr></thead>
65
+ <tbody>{visibleRows.map((row) => <tr key={[row.type, row.parent, row.entity].join("\u0000")}><td><span className="type">{row.type}</span></td><td>{row.parent || "—"}</td><th scope="row">{row.entity}</th></tr>)}</tbody>
66
+ </table></div>}
67
+ </section>
68
+ </main>;
69
+ }
@@ -0,0 +1,24 @@
1
+ contractVersion: 1
2
+ application:
3
+ id: {{PROJECT_ID}}
4
+ title: {{PROJECT_TITLE}}
5
+ entry: contracts/report-source.json
6
+ style: contracts/report-source.json
7
+ target:
8
+ platform: powerbi
9
+ visual:
10
+ name: {{VISUAL_NAME}}
11
+ displayName: {{PROJECT_TITLE}}
12
+ guid: {{VISUAL_GUID}}
13
+ version: 1.0.0.1
14
+ apiVersion: 5.11.1
15
+ semantic:
16
+ model: contracts/semantic-model.yaml
17
+ broker: contracts/widget-broker.json
18
+ pageSet: contracts/page-set.yaml
19
+ source:
20
+ bundle: contracts/report-source.json
21
+ output:
22
+ directory: dist
23
+ compliance:
24
+ policies: [core, powerbi]
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="referrer" content="no-referrer" />
7
+ <title>Super Visual semantic preview</title>
8
+ </head>
9
+ <body>
10
+ <aside id="supervisual-preview-status" role="status">Connecting to semantic model…</aside>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+ </html>