@datashow/super-visual-compiler 0.0.0-stage → 0.1.0-rc.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 +29 -4
- package/src/build-solution.mjs +140 -0
- package/src/embed-visual.mjs +24 -0
- package/src/index.mjs +69 -0
- package/src/report-wrapper.mjs +88 -0
- package/template/visual/assets/icon.png +0 -0
- package/template/visual/capabilities.json +41 -0
- package/template/visual/package.json +31 -0
- package/template/visual/pbiviz.json +20 -0
- package/template/visual/src/styles.d.ts +1 -0
- package/template/visual/src/visual.ts +139 -0
- package/template/visual/style/visual.less +7 -0
- package/template/visual/tsconfig.json +19 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datashow/super-visual-compiler",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
|
+
"description": "Compile governed React applications into inspectable Super Visual solution bundles.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./src/index.mjs",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"template"
|
|
10
|
+
],
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22.12.0"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/Datashow-Pty-Ltd/super-visual.git",
|
|
18
|
+
"directory": "packages/compiler"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/Datashow-Pty-Ltd/super-visual#readme",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@datashow/super-visual-compliance": "0.1.0-rc.1",
|
|
26
|
+
"@datashow/super-visual-contracts": "0.1.0-rc.1",
|
|
27
|
+
"@datashow/super-visual-semantic": "0.1.0-rc.1",
|
|
28
|
+
"fflate": "0.8.3",
|
|
29
|
+
"yaml": "2.9.1"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
import { loadContract, resolveProjectPath } from "@datashow/super-visual-contracts";
|
|
7
|
+
import { compileBrokerAdapter, compileTmdl, semanticModelWithCatalog } from "@datashow/super-visual-semantic";
|
|
8
|
+
import { generateRuntimeManifests, validateCompilation } from "./index.mjs";
|
|
9
|
+
import { generateReportWrapper } from "./report-wrapper.mjs";
|
|
10
|
+
|
|
11
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
const runtimeSource = path.resolve(packageRoot, "../runtime-powerbi/src");
|
|
13
|
+
const visualTemplate = path.join(packageRoot, "template", "visual");
|
|
14
|
+
|
|
15
|
+
async function readStructured(file) {
|
|
16
|
+
const source = await readFile(file, "utf8");
|
|
17
|
+
return file.endsWith(".json") ? JSON.parse(source) : YAML.parse(source);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function files(root, relative = "") {
|
|
21
|
+
const entries = await readdir(path.join(root, relative), { withFileTypes: true });
|
|
22
|
+
const output = [];
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const child = path.join(relative, entry.name);
|
|
25
|
+
if (entry.isDirectory()) output.push(...await files(root, child));
|
|
26
|
+
else output.push(child);
|
|
27
|
+
}
|
|
28
|
+
return output.sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function hashes(root) {
|
|
32
|
+
const output = {};
|
|
33
|
+
for (const relative of await files(root)) {
|
|
34
|
+
if (relative === "supervisual.manifest.json") continue;
|
|
35
|
+
output[relative.split(path.sep).join("/")] = createHash("sha256").update(await readFile(path.join(root, relative))).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
return output;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function refreshSolutionManifest(outputDirectory) {
|
|
41
|
+
const manifestPath = path.join(outputDirectory, "supervisual.manifest.json");
|
|
42
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
43
|
+
manifest.files = await hashes(outputDirectory);
|
|
44
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
45
|
+
return manifest;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function verifySolutionManifest(outputDirectory) {
|
|
49
|
+
const manifestPath = path.join(outputDirectory, "supervisual.manifest.json");
|
|
50
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
51
|
+
const actual = await hashes(outputDirectory);
|
|
52
|
+
const expected = manifest.files ?? {};
|
|
53
|
+
const missing = Object.keys(expected).filter((file) => !(file in actual));
|
|
54
|
+
const unexpected = Object.keys(actual).filter((file) => !(file in expected));
|
|
55
|
+
const changed = Object.keys(expected).filter((file) => actual[file] && actual[file] !== expected[file]);
|
|
56
|
+
return { ok: missing.length === 0 && unexpected.length === 0 && changed.length === 0, missing, unexpected, changed, fileCount: Object.keys(actual).length };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function generateVisual(project, inputs, visualRoot) {
|
|
60
|
+
await cp(visualTemplate, visualRoot, { recursive: true });
|
|
61
|
+
await cp(runtimeSource, path.join(visualRoot, "src"), { recursive: true });
|
|
62
|
+
await generateRuntimeManifests(inputs, visualRoot);
|
|
63
|
+
const pbivizPath = path.join(visualRoot, "pbiviz.json");
|
|
64
|
+
const pbiviz = JSON.parse(await readFile(pbivizPath, "utf8"));
|
|
65
|
+
pbiviz.visual = { ...pbiviz.visual, ...project.target.visual };
|
|
66
|
+
pbiviz.apiVersion = project.target.visual.apiVersion;
|
|
67
|
+
await writeFile(pbivizPath, `${JSON.stringify(pbiviz, null, 2)}\n`);
|
|
68
|
+
const packagePath = path.join(visualRoot, "package.json");
|
|
69
|
+
const manifest = JSON.parse(await readFile(packagePath, "utf8"));
|
|
70
|
+
manifest.name = `${project.application.id}-powerbi`;
|
|
71
|
+
manifest.version = project.target.visual.version.split(".").slice(0, 3).join(".");
|
|
72
|
+
await writeFile(packagePath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
73
|
+
return visualRoot;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function loadProject(projectFile) {
|
|
77
|
+
const root = path.dirname(path.resolve(projectFile));
|
|
78
|
+
const project = await loadContract("supervisual-project", path.resolve(projectFile));
|
|
79
|
+
const semanticModel = await readStructured(resolveProjectPath(root, project.semantic.model));
|
|
80
|
+
const broker = await loadContract("widget-broker", resolveProjectPath(root, project.semantic.broker));
|
|
81
|
+
const pageSet = await loadContract("page-set", resolveProjectPath(root, project.semantic.pageSet));
|
|
82
|
+
const reportSource = await loadContract("report-source-bundle", resolveProjectPath(root, project.source.bundle));
|
|
83
|
+
return { root, project, semanticModel, broker, pageSet, reportSource };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function buildSolution(projectFile, options = {}) {
|
|
87
|
+
const loaded = await loadProject(projectFile);
|
|
88
|
+
const { root, project, broker, pageSet, reportSource } = loaded;
|
|
89
|
+
const semanticModel = options.semanticParameters
|
|
90
|
+
? { ...loaded.semanticModel, sourceParameters: { ...loaded.semanticModel.sourceParameters, ...options.semanticParameters } }
|
|
91
|
+
: loaded.semanticModel;
|
|
92
|
+
const diagnostics = validateCompilation({ semanticModel, broker, pageSet, reportSource });
|
|
93
|
+
if (diagnostics.length) throw new Error(`Compilation failed:\n- ${diagnostics.join("\n- ")}`);
|
|
94
|
+
const output = options.outputDirectory ?? resolveProjectPath(root, project.output.directory);
|
|
95
|
+
await rm(output, { recursive: true, force: true });
|
|
96
|
+
await mkdir(output, { recursive: true });
|
|
97
|
+
await writeFile(path.join(output, "compliance.report.json"), `${JSON.stringify({
|
|
98
|
+
contractVersion: 1,
|
|
99
|
+
ok: true,
|
|
100
|
+
policies: project.compliance?.policies ?? ["core", "powerbi"],
|
|
101
|
+
checks: ["project-contract", "widget-broker", "page-set-primitives", "report-graph-bindings", "report-source-policy", "semantic-references"],
|
|
102
|
+
diagnostics: [],
|
|
103
|
+
}, null, 2)}\n`);
|
|
104
|
+
const baseModel = path.join(output, "Base.SemanticModel");
|
|
105
|
+
const adapterBaseModel = path.join(output, ".SemanticAdapterBase.SemanticModel");
|
|
106
|
+
const adapterModel = path.join(output, "SemanticAdapter.SemanticModel");
|
|
107
|
+
const visual = path.join(output, "Visual");
|
|
108
|
+
const report = path.join(output, "Report.Report");
|
|
109
|
+
await compileTmdl(semanticModel, baseModel);
|
|
110
|
+
const brokerSemanticModel = semanticModelWithCatalog(semanticModel, broker, options.semanticCatalogSource ?? semanticModel);
|
|
111
|
+
const brokerBaseModel = brokerSemanticModel === semanticModel ? baseModel : adapterBaseModel;
|
|
112
|
+
if (brokerBaseModel === baseModel) {
|
|
113
|
+
await compileBrokerAdapter({ baseModelDirectory: baseModel, outputDirectory: adapterModel, broker, semanticModel: brokerSemanticModel });
|
|
114
|
+
} else {
|
|
115
|
+
await compileTmdl(brokerSemanticModel, brokerBaseModel);
|
|
116
|
+
try {
|
|
117
|
+
await compileBrokerAdapter({ baseModelDirectory: brokerBaseModel, outputDirectory: adapterModel, broker, semanticModel: brokerSemanticModel });
|
|
118
|
+
} finally {
|
|
119
|
+
await rm(brokerBaseModel, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await generateVisual(project, { broker, pageSet, reportSource }, visual);
|
|
123
|
+
await generateReportWrapper({ outputDirectory: report, title: project.application.title, visualGuid: project.target.visual.guid });
|
|
124
|
+
const manifest = {
|
|
125
|
+
contractVersion: 1,
|
|
126
|
+
application: project.application,
|
|
127
|
+
target: project.target,
|
|
128
|
+
inputs: { project: path.basename(projectFile), semanticModel: project.semantic.model, broker: project.semantic.broker, pageSet: project.semantic.pageSet, reportSource: project.source.bundle },
|
|
129
|
+
outputs: { baseModel: "Base.SemanticModel", semanticAdapter: "SemanticAdapter.SemanticModel", visualSource: "Visual", report: "Report.Report" },
|
|
130
|
+
provenance: {
|
|
131
|
+
compiler: "@datashow/super-visual-compiler",
|
|
132
|
+
reference: "AdventureWorks deployed v38",
|
|
133
|
+
semanticParameterOverrides: Object.keys(options.semanticParameters ?? {}).sort(),
|
|
134
|
+
semanticCatalogSource: options.semanticCatalogSource ? { id: options.semanticCatalogSource.id ?? null, displayName: options.semanticCatalogSource.displayName ?? options.semanticCatalogSource.name ?? null } : null,
|
|
135
|
+
},
|
|
136
|
+
files: await hashes(output),
|
|
137
|
+
};
|
|
138
|
+
await writeFile(path.join(output, "supervisual.manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
139
|
+
return { outputDirectory: output, diagnostics: [], manifest };
|
|
140
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { unzipSync } from "fflate";
|
|
4
|
+
|
|
5
|
+
export async function embedVisualPackage(pbivizPath, reportDirectory) {
|
|
6
|
+
const archive = unzipSync(new Uint8Array(await readFile(pbivizPath)));
|
|
7
|
+
const packageBytes = archive["package.json"];
|
|
8
|
+
if (!packageBytes) throw new Error("The .pbiviz archive is missing package.json");
|
|
9
|
+
const packageManifest = JSON.parse(Buffer.from(packageBytes).toString("utf8"));
|
|
10
|
+
const guid = packageManifest.visual?.guid;
|
|
11
|
+
const resource = packageManifest.resources?.[0]?.file;
|
|
12
|
+
if (!guid || !resource || !archive[resource]) throw new Error("The .pbiviz archive does not identify its visual resource");
|
|
13
|
+
const target = path.join(reportDirectory, "CustomVisuals", guid);
|
|
14
|
+
await mkdir(path.join(target, "resources"), { recursive: true });
|
|
15
|
+
await writeFile(path.join(target, "package.json"), Buffer.from(packageBytes));
|
|
16
|
+
const resourceName = path.basename(resource);
|
|
17
|
+
await writeFile(path.join(target, "resources", resourceName), Buffer.from(archive[resource]));
|
|
18
|
+
const reportPath = path.join(reportDirectory, "definition", "report.json");
|
|
19
|
+
const report = JSON.parse(await readFile(reportPath, "utf8"));
|
|
20
|
+
report.resourcePackages = (report.resourcePackages ?? []).filter((item) => item.name !== guid);
|
|
21
|
+
report.resourcePackages.push({ name: guid, type: "CustomVisual", items: [{ name: `resources/${resourceName}`, path: resourceName, type: "CustomVisualMetadata" }] });
|
|
22
|
+
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
23
|
+
return { guid, resourceName, reportDirectory };
|
|
24
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { validatePageSetPrimitives, validateReportGraphBindings, validateSourceBundle, validateWidgetBrokerContract } from "@datashow/super-visual-compliance";
|
|
4
|
+
import { semanticModelWithCatalog } from "@datashow/super-visual-semantic";
|
|
5
|
+
|
|
6
|
+
function moduleText(comment, name, value) {
|
|
7
|
+
return `${comment}\nexport const ${name} = ${JSON.stringify(value, null, 2)} as const;\n`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function qualifiedReference(value) {
|
|
11
|
+
if (typeof value !== "string" || !value.endsWith("]")) return null;
|
|
12
|
+
const separator = value.lastIndexOf("[", value.length - 2);
|
|
13
|
+
if (separator <= 0 || separator === value.length - 2) return null;
|
|
14
|
+
return { table: value.slice(0, separator), object: value.slice(separator + 1, -1) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validateCompilation({ semanticModel, broker, pageSet, reportSource }) {
|
|
18
|
+
const brokerSemanticModel = semanticModelWithCatalog(semanticModel, broker);
|
|
19
|
+
return [
|
|
20
|
+
...validateWidgetBrokerContract(broker, brokerSemanticModel),
|
|
21
|
+
...validateReportGraphBindings(pageSet, broker),
|
|
22
|
+
...validatePageSetPrimitives(pageSet),
|
|
23
|
+
...validateSourceBundle(reportSource).map((diagnostic) => `${diagnostic.code}: ${diagnostic.path} ${diagnostic.message}`),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function generateRuntimeManifests({ broker, pageSet, reportSource }, outputRoot) {
|
|
28
|
+
const referenced = new Set((pageSet.pages ?? []).flatMap((page) => [
|
|
29
|
+
page.summary?.emphasisBinding?.dataset,
|
|
30
|
+
page.search?.dataset,
|
|
31
|
+
...(page.kpis ?? []).map((kpi) => kpi.binding?.dataset),
|
|
32
|
+
...(page.widgets ?? []).map((widget) => widget.dataset),
|
|
33
|
+
]).filter(Boolean));
|
|
34
|
+
for (const interaction of pageSet.interactions ?? []) for (const id of interaction.invalidates ?? []) referenced.add(id);
|
|
35
|
+
const widgets = broker.widgets.filter((widget) => referenced.has(widget.id));
|
|
36
|
+
const sourceRoot = path.join(outputRoot, "src");
|
|
37
|
+
await mkdir(sourceRoot, { recursive: true });
|
|
38
|
+
await writeFile(path.join(sourceRoot, "datasetRegistry.ts"), `export const DATASET_KEYS = ${JSON.stringify(widgets.map((widget) => widget.id))} as const;\nexport type DatasetKey = string;\n`);
|
|
39
|
+
await writeFile(path.join(sourceRoot, "pageSetManifest.ts"), moduleText("/* Generated from the page-set contract. */", "PAGE_SET_MANIFEST", pageSet));
|
|
40
|
+
const dependencies = Object.fromEntries(widgets.filter((widget) => widget.selectable).map((widget) => [widget.id, widget.invalidates.filter((id) => referenced.has(id))]));
|
|
41
|
+
await writeFile(path.join(sourceRoot, "dependencyManifest.ts"), `import type { DatasetKey } from "./datasetRegistry";\nexport const SELECTION_DEPENDENCIES: Record<DatasetKey, DatasetKey[]> = ${JSON.stringify(dependencies, null, 2)};\n`);
|
|
42
|
+
const manifest = Object.fromEntries(widgets.map((widget) => [widget.id, { id: widget.id, order: ["top-by-value", "bottom-by-value"].includes(widget.rowSelection) ? "value" : "axis", selectable: widget.selectable }]));
|
|
43
|
+
await writeFile(path.join(sourceRoot, "widgetManifest.ts"), `import type { DatasetKey } from "./model";\nexport const WIDGET_MANIFEST: Record<DatasetKey, { id: DatasetKey; order: "axis" | "value"; selectable: boolean }> = ${JSON.stringify(manifest, null, 2)};\n`);
|
|
44
|
+
const targets = Object.fromEntries(widgets.map((widget) => {
|
|
45
|
+
const reference = qualifiedReference(widget.axis);
|
|
46
|
+
if (!reference) throw new Error(`Broker axis must use Table[Column] notation: ${widget.axis}`);
|
|
47
|
+
return [widget.id, { table: reference.table, column: reference.object }];
|
|
48
|
+
}));
|
|
49
|
+
await writeFile(path.join(sourceRoot, "filterTargets.ts"), `import type { DatasetKey } from "./model";\nimport type { FilterTarget } from "./filter";\nexport const FILTER_TARGETS: Record<DatasetKey, FilterTarget> = ${JSON.stringify(targets, null, 2)};\n`);
|
|
50
|
+
const claudeParameters = reportSource.adapter?.kind === "claude-dashboard" ? reportSource.adapter.parameters : [];
|
|
51
|
+
const claudeTargets = Object.fromEntries(claudeParameters.map((parameter) => {
|
|
52
|
+
const reference = qualifiedReference(parameter.target);
|
|
53
|
+
if (!reference) throw new Error(`Claude parameter target must use Table[Column] notation: ${parameter.target}`);
|
|
54
|
+
return [parameter.name, { table: reference.table, column: reference.object }];
|
|
55
|
+
}));
|
|
56
|
+
const claudeValueMaps = Object.fromEntries(claudeParameters.filter((parameter) => parameter.values).map((parameter) => [parameter.name, parameter.values]));
|
|
57
|
+
await writeFile(path.join(sourceRoot, "claudeParamTargets.ts"), `import type { FilterTarget, FilterValue } from "./filter";\nexport const CLAUDE_PARAM_FILTER_TARGETS: Record<string, FilterTarget> = ${JSON.stringify(claudeTargets, null, 2)};\nconst CLAUDE_PARAM_VALUE_MAPS: Record<string, Record<string, FilterValue>> = ${JSON.stringify(claudeValueMaps, null, 2)};\nexport function claudeFilterValues(name: string, value: unknown): FilterValue[] {\n const values = Array.isArray(value) ? value : [value];\n const mapped = CLAUDE_PARAM_VALUE_MAPS[name] ?? {};\n return values.flatMap((candidate) => {\n if (candidate === null || candidate === undefined || candidate === \"\") return [];\n const resolved = mapped[String(candidate)] ?? candidate;\n return typeof resolved === \"string\" || typeof resolved === \"number\" || typeof resolved === \"boolean\" ? [resolved] : [];\n });\n}\n`);
|
|
58
|
+
for (const file of reportSource.files) {
|
|
59
|
+
const target = path.resolve(outputRoot, file.path);
|
|
60
|
+
if (target !== path.resolve(outputRoot) && !target.startsWith(`${path.resolve(outputRoot)}${path.sep}`)) throw new Error(`Generated source escapes output: ${file.path}`);
|
|
61
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
62
|
+
await writeFile(target, file.content, "utf8");
|
|
63
|
+
}
|
|
64
|
+
return { datasets: widgets.map((widget) => widget.id), generatedFiles: 6 + reportSource.files.length };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { buildSolution, loadProject, refreshSolutionManifest, verifySolutionManifest } from "./build-solution.mjs";
|
|
68
|
+
export { generateReportWrapper } from "./report-wrapper.mjs";
|
|
69
|
+
export { embedVisualPackage } from "./embed-visual.mjs";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
function id(seed, length = 20) {
|
|
6
|
+
return createHash("sha256").update(seed).digest("hex").slice(0, length);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function uuid(seed) {
|
|
10
|
+
const bytes = createHash("sha256").update(seed).digest().subarray(0, 16);
|
|
11
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
|
|
12
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
13
|
+
const hex = bytes.toString("hex");
|
|
14
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function json(file, value) {
|
|
18
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
19
|
+
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function projection(role, property, kind = "Column") {
|
|
23
|
+
return {
|
|
24
|
+
field: { [kind]: { Expression: { SourceRef: { Entity: "Widget Axis" } }, Property: property } },
|
|
25
|
+
queryRef: `Widget Axis.${property}`,
|
|
26
|
+
nativeQueryRef: property,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function generateReportWrapper({ outputDirectory, title, visualGuid, semanticPath = "../SemanticAdapter.SemanticModel", background = "#F6F5EF" }) {
|
|
31
|
+
const pageName = id(`${title}:page`);
|
|
32
|
+
const visualName = id(`${title}:visual`);
|
|
33
|
+
const definition = path.join(outputDirectory, "definition");
|
|
34
|
+
await json(path.join(outputDirectory, ".platform"), {
|
|
35
|
+
$schema: "https://developer.microsoft.com/json-schemas/fabric/gitIntegration/platformProperties/2.0.0/schema.json",
|
|
36
|
+
metadata: { type: "Report", displayName: title },
|
|
37
|
+
config: { version: "2.0", logicalId: uuid(`${title}:report`) },
|
|
38
|
+
});
|
|
39
|
+
await json(path.join(outputDirectory, "definition.pbir"), {
|
|
40
|
+
$schema: "https://developer.microsoft.com/json-schemas/fabric/item/report/definitionProperties/2.0.0/schema.json",
|
|
41
|
+
version: "4.0", datasetReference: { byPath: { path: semanticPath } },
|
|
42
|
+
});
|
|
43
|
+
await json(path.join(definition, "version.json"), {
|
|
44
|
+
$schema: "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/versionMetadata/1.0.0/schema.json",
|
|
45
|
+
version: "2.0.0",
|
|
46
|
+
});
|
|
47
|
+
await json(path.join(definition, "report.json"), {
|
|
48
|
+
$schema: "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/report/3.3.0/schema.json",
|
|
49
|
+
themeCollection: { baseTheme: { name: "CY24SU10", reportVersionAtImport: { visual: "2.6.0", report: "3.1.0", page: "2.3.0" }, type: "SharedResources" } },
|
|
50
|
+
resourcePackages: [],
|
|
51
|
+
settings: { useStylableVisualContainerHeader: true, exportDataMode: "AllowSummarized" },
|
|
52
|
+
});
|
|
53
|
+
await json(path.join(definition, "pages", "pages.json"), {
|
|
54
|
+
$schema: "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/pagesMetadata/1.0.0/schema.json",
|
|
55
|
+
pageOrder: [pageName], activePageName: pageName,
|
|
56
|
+
});
|
|
57
|
+
await json(path.join(definition, "pages", pageName, "page.json"), {
|
|
58
|
+
$schema: "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/page/2.1.0/schema.json",
|
|
59
|
+
name: pageName, displayName: title, displayOption: "FitToPage", height: 1080, width: 1920,
|
|
60
|
+
objects: Object.fromEntries(["background", "outspace"].map((name) => [name, [{ properties: { color: { solid: { color: { expr: { Literal: { Value: `'${background}'` } } } } }, transparency: { expr: { Literal: { Value: "0D" } } } } }]])),
|
|
61
|
+
});
|
|
62
|
+
await json(path.join(definition, "pages", pageName, "visuals", visualName, "visual.json"), {
|
|
63
|
+
$schema: "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.12.0/schema.json",
|
|
64
|
+
name: visualName,
|
|
65
|
+
position: { x: 0, y: 0, z: 0, height: 1080, width: 1920, tabOrder: 0 },
|
|
66
|
+
visual: {
|
|
67
|
+
visualType: visualGuid,
|
|
68
|
+
query: { queryState: {
|
|
69
|
+
widgetId: { projections: [projection("widgetId", "WidgetId")] },
|
|
70
|
+
axisKey: { projections: [projection("axisKey", "AxisKey")] },
|
|
71
|
+
label: { projections: [projection("label", "Label")] },
|
|
72
|
+
sort: { projections: [projection("sort", "Sort")] },
|
|
73
|
+
seriesKey: { projections: [projection("seriesKey", "SeriesKey")] },
|
|
74
|
+
seriesLabel: { projections: [projection("seriesLabel", "SeriesLabel")] },
|
|
75
|
+
value: { projections: [projection("value", "Widget Value", "Measure")] },
|
|
76
|
+
context: { projections: [projection("context", "Broker Context", "Measure")] },
|
|
77
|
+
} },
|
|
78
|
+
visualContainerObjects: {
|
|
79
|
+
general: [{ properties: { altText: { expr: { Literal: { Value: `'${title.replaceAll("'", "''")} compiled analytical application.'` } } } } }],
|
|
80
|
+
title: [{ properties: { show: { expr: { Literal: { Value: "false" } } } } }],
|
|
81
|
+
background: [{ properties: { show: { expr: { Literal: { Value: "false" } } } } }],
|
|
82
|
+
border: [{ properties: { show: { expr: { Literal: { Value: "false" } } } } }],
|
|
83
|
+
visualHeader: [{ properties: { show: { expr: { Literal: { Value: "false" } } } } }],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return { pageName, visualName, outputDirectory };
|
|
88
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dataRoles": [
|
|
3
|
+
{ "name": "widgetId", "displayName": "Broker — widget ID", "kind": "Grouping" },
|
|
4
|
+
{ "name": "axisKey", "displayName": "Broker — axis key", "kind": "Grouping" },
|
|
5
|
+
{ "name": "label", "displayName": "Broker — label", "kind": "Grouping" },
|
|
6
|
+
{ "name": "sort", "displayName": "Broker — sort", "kind": "Grouping" },
|
|
7
|
+
{ "name": "seriesKey", "displayName": "Broker — series key", "kind": "Grouping" },
|
|
8
|
+
{ "name": "seriesLabel", "displayName": "Broker — series label", "kind": "Grouping" },
|
|
9
|
+
{ "name": "value", "displayName": "Broker — value", "kind": "Measure" },
|
|
10
|
+
{ "name": "context", "displayName": "Broker — context", "kind": "Measure" }
|
|
11
|
+
],
|
|
12
|
+
"objects": {
|
|
13
|
+
"general": {
|
|
14
|
+
"displayName": "General",
|
|
15
|
+
"properties": {
|
|
16
|
+
"filter": { "displayName": "Business filter", "type": { "filter": true } },
|
|
17
|
+
"selfFilter": { "displayName": "Widget request", "type": { "filter": { "selfFilter": true } } }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dataViewMappings": [{
|
|
22
|
+
"conditions": [{
|
|
23
|
+
"widgetId": { "min": 1, "max": 1 }, "axisKey": { "min": 1, "max": 1 },
|
|
24
|
+
"label": { "min": 1, "max": 1 }, "sort": { "min": 1, "max": 1 },
|
|
25
|
+
"seriesKey": { "max": 1 }, "seriesLabel": { "max": 1 },
|
|
26
|
+
"value": { "min": 1, "max": 1 }, "context": { "min": 1, "max": 1 }
|
|
27
|
+
}],
|
|
28
|
+
"table": { "rows": { "select": [
|
|
29
|
+
{ "for": { "in": "widgetId" } }, { "for": { "in": "axisKey" } },
|
|
30
|
+
{ "for": { "in": "label" } }, { "for": { "in": "sort" } },
|
|
31
|
+
{ "for": { "in": "seriesKey" } }, { "for": { "in": "seriesLabel" } },
|
|
32
|
+
{ "for": { "in": "value" } }, { "for": { "in": "context" } }
|
|
33
|
+
], "dataReductionAlgorithm": { "top": { "count": 30000 } } } }
|
|
34
|
+
}],
|
|
35
|
+
"supportsHighlight": false,
|
|
36
|
+
"supportsKeyboardFocus": true,
|
|
37
|
+
"supportsSynchronizingFilterState": true,
|
|
38
|
+
"supportsLandingPage": true,
|
|
39
|
+
"supportsEmptyDataView": true,
|
|
40
|
+
"privileges": []
|
|
41
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "super-visual-generated-powerbi",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"typecheck": "tsc --noEmit",
|
|
7
|
+
"test": "vitest run --passWithNoTests",
|
|
8
|
+
"package": "pbiviz package"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@fontsource/source-serif-4": "5.3.1",
|
|
12
|
+
"@nivo/bar": "0.99.0",
|
|
13
|
+
"@nivo/line": "0.99.0",
|
|
14
|
+
"@nivo/pie": "0.99.0",
|
|
15
|
+
"@nivo/sankey": "^0.99.0",
|
|
16
|
+
"@nivo/tree": "0.99.0",
|
|
17
|
+
"d3": "7.9.0",
|
|
18
|
+
"powerbi-visuals-api": "5.11.1",
|
|
19
|
+
"react": "19.3.1",
|
|
20
|
+
"react-dom": "19.3.1",
|
|
21
|
+
"react-router-dom": "7.18.4"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/d3": "7.4.3",
|
|
25
|
+
"@types/react": "19.3.0",
|
|
26
|
+
"@types/react-dom": "19.3.0",
|
|
27
|
+
"powerbi-visuals-tools": "7.2.1",
|
|
28
|
+
"typescript": "5.9.3",
|
|
29
|
+
"vitest": "5.0.5"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"visual": {
|
|
3
|
+
"name": "superVisualGenerated",
|
|
4
|
+
"displayName": "Super Visual",
|
|
5
|
+
"guid": "superVisualGenerated000000000000000000000000",
|
|
6
|
+
"visualClassName": "Visual",
|
|
7
|
+
"version": "1.0.0.0",
|
|
8
|
+
"description": "A compiled, semantically bound analytical application.",
|
|
9
|
+
"supportUrl": "https://github.com/Datashow-Pty-Ltd/super-visual",
|
|
10
|
+
"gitHubUrl": "https://github.com/Datashow-Pty-Ltd/super-visual"
|
|
11
|
+
},
|
|
12
|
+
"apiVersion": "5.11.1",
|
|
13
|
+
"author": { "name": "Datashow", "email": "support@datashow.com.au" },
|
|
14
|
+
"assets": { "icon": "assets/icon.png" },
|
|
15
|
+
"externalJS": null,
|
|
16
|
+
"style": "style/visual.less",
|
|
17
|
+
"capabilities": "capabilities.json",
|
|
18
|
+
"dependencies": null,
|
|
19
|
+
"stringResources": []
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.less";
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import powerbi from "powerbi-visuals-api";
|
|
6
|
+
import "./../style/visual.less";
|
|
7
|
+
import { ReportApplication } from "./reportApplication";
|
|
8
|
+
import { buildBrokerAdapter, completeRequestedDatasets } from "./dataView";
|
|
9
|
+
import { dependentWidgets } from "./dependency";
|
|
10
|
+
import { buildBasicFilter, filterValueFromLabel } from "./filter";
|
|
11
|
+
import { FILTER_TARGETS } from "./filterTargets";
|
|
12
|
+
import { CLAUDE_PARAM_FILTER_TARGETS, claudeFilterValues } from "./claudeParamTargets";
|
|
13
|
+
import type { ClaudeParams } from "./claudeDashboard/types";
|
|
14
|
+
import { DATASET_KEYS, emptyDatasets, emptyStatuses, type ActiveSelections, type DashboardModel, type DatasetKey, type MetricPoint, type WidgetStatus } from "./model";
|
|
15
|
+
|
|
16
|
+
import IVisual = powerbi.extensibility.visual.IVisual;
|
|
17
|
+
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
|
|
18
|
+
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
|
|
19
|
+
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
|
|
20
|
+
|
|
21
|
+
const BROKER_TARGET = { table: "Widget Axis", column: "WidgetId" };
|
|
22
|
+
const DATASET_SET = new Set<string>(DATASET_KEYS);
|
|
23
|
+
|
|
24
|
+
export class Visual implements IVisual {
|
|
25
|
+
private readonly host: IVisualHost;
|
|
26
|
+
private readonly root: Root;
|
|
27
|
+
private readonly datasets = emptyDatasets();
|
|
28
|
+
private readonly status: Record<DatasetKey, WidgetStatus> = emptyStatuses();
|
|
29
|
+
private readonly selections: ActiveSelections = {};
|
|
30
|
+
private visible: DatasetKey[] = [...DATASET_KEYS];
|
|
31
|
+
private context = "";
|
|
32
|
+
private warnings: string[] = [];
|
|
33
|
+
private sequence = 0;
|
|
34
|
+
private readonly handleSelect = (widget: DatasetKey, point: MetricPoint): void => this.select(widget, point);
|
|
35
|
+
private readonly handleClear = (widget: DatasetKey): void => this.clear(widget);
|
|
36
|
+
private readonly handleVisibleDatasets = (datasets: DatasetKey[]): void => this.request(datasets);
|
|
37
|
+
private readonly handleClaudeParams = (changes: ClaudeParams): void => this.applyClaudeParams(changes);
|
|
38
|
+
private readonly handleClaudeRefresh = (dataset: string): void => this.refreshClaudeDataset(dataset);
|
|
39
|
+
private readonly handleClaudeLink = (name: string): void => { void name; };
|
|
40
|
+
|
|
41
|
+
constructor(options: VisualConstructorOptions | undefined) {
|
|
42
|
+
if (!options) throw new Error("Power BI did not provide visual constructor options");
|
|
43
|
+
this.host = options.host;
|
|
44
|
+
options.element.replaceChildren();
|
|
45
|
+
options.element.dataset.reportScrollRoot = "";
|
|
46
|
+
options.element.style.height = "100%";
|
|
47
|
+
options.element.style.overflowY = "auto";
|
|
48
|
+
this.root = createRoot(options.element);
|
|
49
|
+
this.render();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
update(options: VisualUpdateOptions): void {
|
|
53
|
+
this.sequence += 1;
|
|
54
|
+
this.host.eventService.renderingStarted(options);
|
|
55
|
+
try {
|
|
56
|
+
const adapter = buildBrokerAdapter(options.dataViews, this.host);
|
|
57
|
+
const completed = completeRequestedDatasets(adapter, this.visible);
|
|
58
|
+
for (const key of DATASET_KEYS) {
|
|
59
|
+
if (completed[key]) {
|
|
60
|
+
this.datasets[key] = completed[key]!;
|
|
61
|
+
this.status[key] = "fresh";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this.context = adapter.context || this.context;
|
|
65
|
+
this.warnings = adapter.warnings;
|
|
66
|
+
this.render();
|
|
67
|
+
this.host.eventService.renderingFinished(options);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
this.host.eventService.renderingFailed(options, message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
destroy(): void { this.root.unmount(); }
|
|
75
|
+
getFormattingModel(): powerbi.visuals.FormattingModel { return { cards: [] }; }
|
|
76
|
+
|
|
77
|
+
private request(datasets: DatasetKey[]): void {
|
|
78
|
+
this.visible = [...new Set(datasets)].filter((key) => DATASET_SET.has(key));
|
|
79
|
+
for (const key of this.visible) if (!this.datasets[key]) this.status[key] = "loading";
|
|
80
|
+
const filter = buildBasicFilter(BROKER_TARGET, ...this.visible);
|
|
81
|
+
this.host.applyJsonFilter(filter, "general", "selfFilter", powerbi.FilterAction.merge);
|
|
82
|
+
this.render();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private select(widget: DatasetKey, point: MetricPoint): void {
|
|
86
|
+
const value = filterValueFromLabel(point.dimensions.at(-1)?.key || point.axisKey);
|
|
87
|
+
this.selections[widget] = { widget, pointKey: point.key, label: point.label, value };
|
|
88
|
+
for (const key of dependentWidgets(widget)) this.status[key] = "stale";
|
|
89
|
+
this.host.applyJsonFilter(buildBasicFilter(FILTER_TARGETS[widget], value), "general", "filter", powerbi.FilterAction.merge);
|
|
90
|
+
this.render();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private clear(widget: DatasetKey): void {
|
|
94
|
+
delete this.selections[widget];
|
|
95
|
+
this.host.applyJsonFilter(buildBasicFilter(FILTER_TARGETS[widget], this.selections[widget]?.value ?? ""), "general", "filter", powerbi.FilterAction.remove);
|
|
96
|
+
this.render();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private applyClaudeParams(changes: ClaudeParams): void {
|
|
100
|
+
if (this.host.hostCapabilities.allowInteractions === false) return;
|
|
101
|
+
for (const [name, value] of Object.entries(changes)) {
|
|
102
|
+
const target = CLAUDE_PARAM_FILTER_TARGETS[name];
|
|
103
|
+
if (!target) continue;
|
|
104
|
+
const values = claudeFilterValues(name, value);
|
|
105
|
+
const filter = buildBasicFilter(target, ...values);
|
|
106
|
+
this.host.applyJsonFilter(filter, "general", "filter", values.length ? powerbi.FilterAction.merge : powerbi.FilterAction.remove);
|
|
107
|
+
}
|
|
108
|
+
for (const key of this.visible) this.status[key] = "stale";
|
|
109
|
+
this.request(this.visible);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private refreshClaudeDataset(dataset: string): void {
|
|
113
|
+
if (DATASET_SET.has(dataset)) this.request([dataset as DatasetKey]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private model(): DashboardModel {
|
|
117
|
+
const rowCounts = Object.fromEntries(DATASET_KEYS.map((key) => [key, this.datasets[key]?.points.length ?? 0])) as Record<DatasetKey, number>;
|
|
118
|
+
const valueSums = Object.fromEntries(DATASET_KEYS.map((key) => [key, this.datasets[key]?.points.reduce((sum, point) => sum + point.value, 0) ?? 0])) as Record<DatasetKey, number>;
|
|
119
|
+
return { datasets: this.datasets, status: this.status, warnings: this.warnings, diagnostics: {
|
|
120
|
+
dataViewCount: 0, updateSequence: this.sequence, dataUpdateSequence: this.sequence, requestSequence: 0,
|
|
121
|
+
receivedWidgets: DATASET_KEYS.filter((key) => Boolean(this.datasets[key])), receivedRowCount: Object.values(rowCounts).reduce((sum, value) => sum + value, 0),
|
|
122
|
+
widgetStatus: this.status, rowCounts, valueSums, selectionCandidates: Object.fromEntries(DATASET_KEYS.map((key) => [key, (this.datasets[key]?.points ?? []).map((point) => ({ key: point.key, label: point.label }))])) as DashboardModel["diagnostics"]["selectionCandidates"],
|
|
123
|
+
activeSelections: Object.fromEntries(Object.entries(this.selections).map(([key, value]) => [key, value?.label ?? ""])), activeRequest: this.visible,
|
|
124
|
+
context: this.context, lastRequestMs: null, events: [],
|
|
125
|
+
} };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private render(): void {
|
|
129
|
+
this.root.render(createElement(ReportApplication, {
|
|
130
|
+
model: this.model(), selections: this.selections,
|
|
131
|
+
onSelect: this.handleSelect,
|
|
132
|
+
onClear: this.handleClear,
|
|
133
|
+
onVisibleDatasetsChange: this.handleVisibleDatasets,
|
|
134
|
+
onClaudeParamsChange: this.handleClaudeParams,
|
|
135
|
+
onClaudeRefresh: this.handleClaudeRefresh,
|
|
136
|
+
onClaudeLinkChange: this.handleClaudeLink,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
:root { color-scheme: light; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
html, body, #root { margin: 0; width: 100%; min-height: 100%; }
|
|
4
|
+
body { overflow: hidden; }
|
|
5
|
+
button, input, select { font: inherit; }
|
|
6
|
+
[data-report-scroll-root] { width: 100%; min-height: 100%; overflow: visible; }
|
|
7
|
+
@import "./report-design.less";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowJs": false,
|
|
4
|
+
"esModuleInterop": true,
|
|
5
|
+
"experimentalDecorators": true,
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"lib": ["ES2022", "DOM"],
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"moduleResolution": "Bundler",
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"outDir": "./.tmp/build/",
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"target": "ES2022"
|
|
17
|
+
},
|
|
18
|
+
"files": ["./src/visual.ts", "./src/styles.d.ts"]
|
|
19
|
+
}
|
package/README.md
DELETED