diff --git a/.claude/skills/migrate-dashboard/SKILL.md b/.claude/skills/migrate-dashboard/SKILL.md new file mode 100644 index 00000000..bc4ac5dd --- /dev/null +++ b/.claude/skills/migrate-dashboard/SKILL.md @@ -0,0 +1,109 @@ +--- +name: migrate-dashboard +description: Migrate an exported platform dashboard JSON to plugin format. Use when the user pastes a dashboard JSON to convert, says "migrate this dashboard", or wants to add an exported dashboard to a plugin as default content. +model: sonnet +metadata: + author: SquaredUp + version: "0.0.1" +--- + +# Migrate Dashboard to Plugin Format + +Convert a platform-exported dashboard JSON to plugin format by normalizing hardcoded IDs to template variables, then saving as plugin default content. + +**Announce at start:** "I'm using the migrate-dashboard skill." + +--- + +## Steps + +### 1. Locate plugin + +Identify the plugin and version from context, or ask the user. + +Working folder: `plugins///` + +If only one version exists, use it without asking. + +**Done when:** working folder is set. + +--- + +### 2. Receive dashboard JSON + +Ask the user to paste the exported dashboard JSON if not already provided. + +**Done when:** valid JSON with `"_type": "layout/grid"` is in hand. + +--- + +### 3. Normalize + +Run the normalization script to replace all hardcoded platform values with template variables: + +```bash +node .claude/skills/migrate-dashboard/scripts/normalize.js [plugins///defaultContent/scopes.json] [--scope-name "Scope Name"] +``` + +Pass `scopes.json` when the dashboard has tiles with `config.variables` or `config.scope` (perspective dashboard). The script reads `config.dataStream.name` from each tile to generate the correct `{{dataStreams.[name]}}` reference. + +If `scopes.json` has multiple entries, read it first and determine which scope applies. It may be obvious from the dashboard content (e.g. tile titles referencing "Agent" or "Organization"), but if not, ask the user before running the script. Pass the chosen scope via `--scope-name "Scope Name"`. Run once per scope if the dashboard mixes tiles from multiple scopes. + +Verify the output — check that: + +- No `config-*` IDs remain (all → `{{configId}}`) +- No `space-*` IDs remain (all → `{{workspaceId}}`) +- All `config.dataStream.id` values are `{{dataStreams.[name]}}` form +- All `config.activePluginConfigIds` are `["{{configId}}"]` +- `config.scopes[]` entries with `ids_defaultScopeIds` bindings are removed +- Perspective tiles: `config.variables`, `config.scope.variable`, `config.scope.scope` are templatized + +**Done when:** no hardcoded platform IDs remain in any tile. + +--- + +### 4. Confirm title and timeframe + +Ask the user to confirm the dashboard title (suggest one derived from the content). + +Ask for timeframe. Default: `last24hours`. Options: + +`last1hour` · `last12hours` · `last24hours` · `last30days` · `thisMonth` · `thisQuarter` · `thisYear` · `lastMonth` · `lastQuarter` · `lastYear` · `none` + +**Done when:** title confirmed and timeframe chosen. + +--- + +### 5. Save + +Write to `plugins///defaultContent/.dash.json`: + +```json +{ + "name": "", + "schemaVersion": "1.5", + "timeframe": "", + "variables": [], + "dashboard": +} +``` + +For a perspective dashboard, set `"variables"` to the variable template from scopes.json, e.g.: + +```json +"variables": ["{{variables.[Algolia Index]}}"] +``` + +For a non-perspective dashboard, leave `"variables": []`. + +See [source-example.json](references/source-example.json) and [migrated-example.json](references/migrated-example.json) for a before/after reference. + +**Done when:** file written. + +--- + +### 6. Update manifest + +Ask where in `manifest.json` this dashboard should appear. Update `manifest.json` with the new entry at the specified position. + +**Done when:** `manifest.json` updated and order confirmed. diff --git a/.claude/skills/migrate-dashboard/references/migrated-example.json b/.claude/skills/migrate-dashboard/references/migrated-example.json new file mode 100644 index 00000000..3fc2edcb --- /dev/null +++ b/.claude/skills/migrate-dashboard/references/migrated-example.json @@ -0,0 +1,216 @@ +{ + "name": "Algolia Index", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": ["{{variables.[Algolia Index]}}"], + "dashboard": { + "_type": "layout/grid", + "version": 1, + "contents": [ + { + "static": false, + "w": 4, + "moved": false, + "x": 0, + "h": 4, + "i": "9b9fc2b9-c56d-47de-a4c4-d151e467a210", + "y": 6, + "z": 0, + "config": { + "timeframe": "none", + "variables": ["{{variables.[Algolia Index]}}"], + "dataStream": { + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[properties]}}" + }, + "scope": { + "variable": "{{variables.[Algolia Index]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Algolia Indices]}}" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Index Details", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "static": false, + "w": 2, + "moved": false, + "x": 0, + "h": 4, + "i": "dfa41039-1657-4142-b6a5-16ac7abffb9d", + "y": 4, + "z": 0, + "config": { + "variables": ["{{variables.[Algolia Index]}}"], + "dataStream": { + "name": "searchCount", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[searchCount]}}", + "sort": { "by": [["date_byDay", "asc"]] }, + "group": { + "by": [["date", "byDay"]], + "aggregate": [{ "type": "sum", "names": ["count"] }] + } + }, + "scope": { + "variable": "{{variables.[Algolia Index]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Algolia Indices]}}" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Search Volume", + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "yAxisLabel": "Searches", + "xAxisColumn": "date_byDay", + "showLegend": false, + "showYAxisLabel": true, + "seriesColumn": "none", + "showTrendLine": false, + "legendPosition": "bottom", + "yAxisColumn": ["count_sum"] + } + } + } + } + }, + { + "static": false, + "w": 2, + "moved": false, + "x": 2, + "h": 4, + "i": "09cc7897-4e96-44b3-9205-144fcf3ef5fb", + "y": 4, + "z": 0, + "config": { + "variables": ["{{variables.[Algolia Index]}}"], + "dataStream": { + "name": "noResultRate", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[noResultRate]}}", + "sort": { "by": [["date_byDay", "asc"]] }, + "group": { + "by": [["date", "byDay"]], + "aggregate": [{ "type": "mean", "names": ["rate"] }] + } + }, + "scope": { + "variable": "{{variables.[Algolia Index]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Algolia Indices]}}" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "No-Result Rate", + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "yAxisLabel": "No-Result Rate", + "xAxisColumn": "date_byDay", + "showLegend": false, + "showYAxisLabel": true, + "seriesColumn": "none", + "showTrendLine": false, + "legendPosition": "bottom", + "yAxisColumn": ["rate_mean"] + } + } + } + } + }, + { + "static": false, + "w": 2, + "moved": false, + "x": 0, + "h": 4, + "i": "1c816936-aafc-42dc-8096-10f0281a6e17", + "y": 0, + "z": 0, + "config": { + "variables": ["{{variables.[Algolia Index]}}"], + "dataStream": { + "name": "topSearches", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[topSearches]}}", + "sort": { "by": [["count", "desc"]] } + }, + "scope": { + "variable": "{{variables.[Algolia Index]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Algolia Indices]}}" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Top Searches", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": ["search", "count", "nbHits"], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 2, + "moved": false, + "x": 2, + "h": 4, + "i": "1727a100-9c88-41cb-b157-c0f197b1b13e", + "y": 0, + "z": 0, + "config": { + "variables": ["{{variables.[Algolia Index]}}"], + "dataStream": { + "name": "noResultSearches", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[noResultSearches]}}", + "sort": { "by": [["count", "desc"]] } + }, + "scope": { + "variable": "{{variables.[Algolia Index]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Algolia Indices]}}" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Searches With No Results", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": ["search", "count", "withFilterCount"], + "transpose": false + } + } + } + } + } + ], + "columns": 4 + } +} diff --git a/.claude/skills/migrate-dashboard/references/source-example.json b/.claude/skills/migrate-dashboard/references/source-example.json new file mode 100644 index 00000000..7dd8c0a5 --- /dev/null +++ b/.claude/skills/migrate-dashboard/references/source-example.json @@ -0,0 +1,210 @@ +{ + "_type": "layout/grid", + "version": 1, + "contents": [ + { + "static": false, + "w": 4, + "moved": false, + "x": 0, + "h": 4, + "i": "9b9fc2b9-c56d-47de-a4c4-d151e467a210", + "y": 6, + "z": 0, + "config": { + "timeframe": "none", + "variables": ["var-J4pfoHDhrn2MRUQuPqv4"], + "dataStream": { + "pluginConfigId": "config-6PK2neo9yFh0rx3z80V2", + "id": "datastream-properties" + }, + "scope": { + "variable": "var-J4pfoHDhrn2MRUQuPqv4", + "workspace": "space-m86qNQgA8Mm39fbrpiNX", + "scope": "scope-mFB0VLVFCzanCpmI07nA" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["config-6PK2neo9yFh0rx3z80V2"], + "title": "Index Details", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "static": false, + "w": 2, + "moved": false, + "x": 0, + "h": 4, + "i": "dfa41039-1657-4142-b6a5-16ac7abffb9d", + "y": 4, + "z": 0, + "config": { + "variables": ["var-J4pfoHDhrn2MRUQuPqv4"], + "dataStream": { + "name": "searchCount", + "pluginConfigId": "config-6PK2neo9yFh0rx3z80V2", + "id": "datastream-GECbenNA3dhoyzEmkVrW", + "sort": { "by": [["date_byDay", "asc"]] }, + "group": { + "by": [["date", "byDay"]], + "aggregate": [{ "type": "sum", "names": ["count"] }] + } + }, + "scope": { + "variable": "var-J4pfoHDhrn2MRUQuPqv4", + "workspace": "space-m86qNQgA8Mm39fbrpiNX", + "scope": "scope-mFB0VLVFCzanCpmI07nA" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["config-6PK2neo9yFh0rx3z80V2"], + "title": "Search Volume", + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "yAxisLabel": "Searches", + "xAxisColumn": "date_byDay", + "showLegend": false, + "showYAxisLabel": true, + "seriesColumn": "none", + "showTrendLine": false, + "legendPosition": "bottom", + "yAxisColumn": ["count_sum"] + } + } + } + } + }, + { + "static": false, + "w": 2, + "moved": false, + "x": 2, + "h": 4, + "i": "09cc7897-4e96-44b3-9205-144fcf3ef5fb", + "y": 4, + "z": 0, + "config": { + "variables": ["var-J4pfoHDhrn2MRUQuPqv4"], + "dataStream": { + "name": "noResultRate", + "pluginConfigId": "config-6PK2neo9yFh0rx3z80V2", + "id": "datastream-WZlGFhDNuEpIX3axqpcR", + "sort": { "by": [["date_byDay", "asc"]] }, + "group": { + "by": [["date", "byDay"]], + "aggregate": [{ "type": "mean", "names": ["rate"] }] + } + }, + "scope": { + "variable": "var-J4pfoHDhrn2MRUQuPqv4", + "workspace": "space-m86qNQgA8Mm39fbrpiNX", + "scope": "scope-mFB0VLVFCzanCpmI07nA" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["config-6PK2neo9yFh0rx3z80V2"], + "title": "No-Result Rate", + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "yAxisLabel": "No-Result Rate", + "xAxisColumn": "date_byDay", + "showLegend": false, + "showYAxisLabel": true, + "seriesColumn": "none", + "showTrendLine": false, + "legendPosition": "bottom", + "yAxisColumn": ["rate_mean"] + } + } + } + } + }, + { + "static": false, + "w": 2, + "moved": false, + "x": 0, + "h": 4, + "i": "1c816936-aafc-42dc-8096-10f0281a6e17", + "y": 0, + "z": 0, + "config": { + "variables": ["var-J4pfoHDhrn2MRUQuPqv4"], + "dataStream": { + "name": "topSearches", + "pluginConfigId": "config-6PK2neo9yFh0rx3z80V2", + "id": "datastream-f7fhFvXjv82JKG5Np0lc", + "sort": { "by": [["count", "desc"]] } + }, + "scope": { + "variable": "var-J4pfoHDhrn2MRUQuPqv4", + "workspace": "space-m86qNQgA8Mm39fbrpiNX", + "scope": "scope-mFB0VLVFCzanCpmI07nA" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["config-6PK2neo9yFh0rx3z80V2"], + "title": "Top Searches", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": ["search", "count", "nbHits"], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 2, + "moved": false, + "x": 2, + "h": 4, + "i": "1727a100-9c88-41cb-b157-c0f197b1b13e", + "y": 0, + "z": 0, + "config": { + "variables": ["var-J4pfoHDhrn2MRUQuPqv4"], + "dataStream": { + "name": "noResultSearches", + "pluginConfigId": "config-6PK2neo9yFh0rx3z80V2", + "id": "datastream-Y85PbGnycRrMZ6eH6bSw", + "sort": { "by": [["count", "desc"]] } + }, + "scope": { + "variable": "var-J4pfoHDhrn2MRUQuPqv4", + "workspace": "space-m86qNQgA8Mm39fbrpiNX", + "scope": "scope-mFB0VLVFCzanCpmI07nA" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["config-6PK2neo9yFh0rx3z80V2"], + "title": "Searches With No Results", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": ["search", "count", "withFilterCount"], + "transpose": false + } + } + } + } + } + ], + "columns": 4 +} diff --git a/.claude/skills/migrate-dashboard/scripts/normalize.js b/.claude/skills/migrate-dashboard/scripts/normalize.js new file mode 100644 index 00000000..f1b6ef63 --- /dev/null +++ b/.claude/skills/migrate-dashboard/scripts/normalize.js @@ -0,0 +1,116 @@ +#!/usr/bin/env node +// Usage: node normalize.js [scopes.json] [--scope-name "Scope Name"] +// Writes normalized dashboard JSON to stdout. +// Pass scopes.json for perspective dashboards (those with config.variables or config.scope). +// If scopes.json has multiple entries, --scope-name is required to select which one to apply. + +const fs = require('fs'); + +const args = process.argv.slice(2); +const scopeNameFlag = args.indexOf('--scope-name'); +const requestedScopeName = scopeNameFlag !== -1 ? args[scopeNameFlag + 1] : null; +const positional = args.filter((a, i) => { + if (a.startsWith('--')) return false; + if (scopeNameFlag !== -1 && i === scopeNameFlag + 1) return false; + return true; +}); + +const dashboardPath = positional[0]; +const scopesPath = positional[1]; + +if (!dashboardPath) { + process.stderr.write( + 'Usage: node normalize.js [scopes.json] [--scope-name "Scope Name"]\n' + ); + process.exit(1); +} + +const dashboard = JSON.parse(fs.readFileSync(dashboardPath, 'utf8')); +const allScopes = scopesPath ? JSON.parse(fs.readFileSync(scopesPath, 'utf8')) : null; + +let scopeEntry = null; +if (allScopes) { + if (allScopes.length === 1) { + scopeEntry = allScopes[0]; + } else if (requestedScopeName) { + scopeEntry = allScopes.find(s => s.name === requestedScopeName); + if (!scopeEntry) { + const available = allScopes.map(s => ` "${s.name}"`).join('\n'); + process.stderr.write( + `No scope named "${requestedScopeName}". Available:\n${available}\n` + ); + process.exit(1); + } + } else { + const available = allScopes.map(s => ` "${s.name}"`).join('\n'); + process.stderr.write( + `scopes.json has multiple entries — use --scope-name to select one:\n${available}\n` + ); + process.exit(1); + } +} + +const scopeName = scopeEntry?.name; +const variableName = scopeEntry?.variable?.name; + +function normalizeTile(tile) { + const config = structuredClone(tile.config); + if (!config) return tile; + + // Remove scope entries that pin to specific nodes via ids_defaultScopeIds + if (Array.isArray(config.scopes)) { + config.scopes = config.scopes.filter( + s => !(s.bindings && 'ids_defaultScopeIds' in s.bindings) + ); + if (config.scopes.length === 0) delete config.scopes; + } + + // Templatize dataStream + if (config.dataStream) { + if (config.dataStream.name) { + config.dataStream.id = `{{dataStreams.[${config.dataStream.name}]}}`; + } + if (config.dataStream.pluginConfigId) { + config.dataStream.pluginConfigId = '{{configId}}'; + } + } + + if (config.activePluginConfigIds) { + config.activePluginConfigIds = ['{{configId}}']; + } + + // Perspective fields + if (config.variables && variableName) { + config.variables = [`{{variables.[${variableName}]}}`]; + } + + if (config.scope) { + if (config.scope.variable && variableName) { + config.scope.variable = `{{variables.[${variableName}]}}`; + } + if (config.scope.workspace) { + config.scope.workspace = '{{workspaceId}}'; + } + if (config.scope.scope && scopeName) { + config.scope.scope = `{{scopes.[${scopeName}]}}`; + } + } + + return { ...tile, config }; +} + +// String-level pass: catch any remaining hardcoded IDs the structural pass may miss +// (e.g. inside scope.query strings) +function globalReplace(obj) { + let s = JSON.stringify(obj); + s = s.replace(/config-[A-Za-z0-9]+/g, '{{configId}}'); + s = s.replace(/space-[A-Za-z0-9]+/g, '{{workspaceId}}'); + return JSON.parse(s); +} + +const normalized = globalReplace({ + ...dashboard, + contents: (dashboard.contents || []).map(normalizeTile), +}); + +process.stdout.write(JSON.stringify(normalized, null, 2) + '\n');