@ehicoso/plugin-radar-chart 0.1.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 (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/config.d.ts +10 -0
  4. package/dist/api/RadarApi.esm.js +8 -0
  5. package/dist/api/RadarApi.esm.js.map +1 -0
  6. package/dist/api/RadarApiClient.esm.js +74 -0
  7. package/dist/api/RadarApiClient.esm.js.map +1 -0
  8. package/dist/components/EntityRadarChartCard/EntityRadarChartCard.esm.js +93 -0
  9. package/dist/components/EntityRadarChartCard/EntityRadarChartCard.esm.js.map +1 -0
  10. package/dist/components/EntityRadarChartCard/annotations.esm.js +59 -0
  11. package/dist/components/EntityRadarChartCard/annotations.esm.js.map +1 -0
  12. package/dist/components/EntityRadarChartCard/index.esm.js +6 -0
  13. package/dist/components/EntityRadarChartCard/index.esm.js.map +1 -0
  14. package/dist/components/RadarPage/ChartPreview.esm.js +81 -0
  15. package/dist/components/RadarPage/ChartPreview.esm.js.map +1 -0
  16. package/dist/components/RadarPage/ConfigForm.esm.js +167 -0
  17. package/dist/components/RadarPage/ConfigForm.esm.js.map +1 -0
  18. package/dist/components/RadarPage/RadarPage.esm.js +108 -0
  19. package/dist/components/RadarPage/RadarPage.esm.js.map +1 -0
  20. package/dist/components/RadarPage/ShareBox.esm.js +31 -0
  21. package/dist/components/RadarPage/ShareBox.esm.js.map +1 -0
  22. package/dist/components/RadarPage/index.esm.js +6 -0
  23. package/dist/components/RadarPage/index.esm.js.map +1 -0
  24. package/dist/config.esm.js +12 -0
  25. package/dist/config.esm.js.map +1 -0
  26. package/dist/index.d.ts +57 -0
  27. package/dist/index.esm.js +5 -0
  28. package/dist/index.esm.js.map +1 -0
  29. package/dist/plugin.esm.js +39 -0
  30. package/dist/plugin.esm.js.map +1 -0
  31. package/dist/routes.esm.js +8 -0
  32. package/dist/routes.esm.js.map +1 -0
  33. package/package.json +79 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrea Carmisciano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # @ehicoso/plugin-radar-chart
2
+
3
+ Frontend-only Backstage plugin that embeds KPI radar charts produced by a remote chart-rendering service. The plugin issues HTTP calls directly from the browser to a service base URL you provide — no Backstage backend plugin required.
4
+
5
+ ## Architecture
6
+
7
+ - **Frontend-only.** Ships as a `frontend-plugin`; nothing runs server-side.
8
+ - **Remote API.** All chart generation, persistence, and retrieval go to the URL set in `radarChart.baseUrl`.
9
+ - **Two surfaces:**
10
+ - `EntityRadarChartCard` — annotation-driven, read-only card on Component entities.
11
+ - `RadarPage` — full configuration UI to build and persist new charts.
12
+
13
+ ## Prerequisites
14
+
15
+ ### Configure the service base URL
16
+
17
+ The plugin will throw at runtime if `radarChart.baseUrl` is not set. Add it to your `app-config.yaml`:
18
+
19
+ ```yaml
20
+ radarChart:
21
+ baseUrl: 'https://<your-radar-chart-service>'
22
+ ```
23
+
24
+ ### CORS
25
+
26
+ The upstream service must allow CORS requests from your Backstage origin:
27
+
28
+ ```
29
+ Access-Control-Allow-Origin: https://<your-backstage-origin>
30
+ Access-Control-Allow-Methods: GET, POST, OPTIONS
31
+ Access-Control-Allow-Headers: Content-Type
32
+ ```
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ yarn add @ehicoso/plugin-radar-chart
38
+ ```
39
+
40
+ ## Usage (new frontend system)
41
+
42
+ ```typescript
43
+ // packages/app/src/App.tsx
44
+ import radarChartPlugin from '@ehicoso/plugin-radar-chart';
45
+
46
+ const app = createApp({
47
+ features: [
48
+ // ...
49
+ radarChartPlugin,
50
+ ],
51
+ });
52
+ ```
53
+
54
+ `EntityRadarChartCard` is registered with the plugin and renders on `kind:Component` entities that carry the `radar-chart/kpis` annotation. Entities without the annotation render nothing.
55
+
56
+ ## Usage (legacy classic API)
57
+
58
+ ```typescript
59
+ import {
60
+ radarChartPlugin,
61
+ RadarPage,
62
+ EntityRadarChartCard,
63
+ } from '@ehicoso/plugin-radar-chart';
64
+
65
+ // Register the plugin
66
+ const app = createApp({ plugins: [radarChartPlugin] });
67
+
68
+ // Route
69
+ <Route path="/radar" element={<RadarPage />} />
70
+
71
+ // Entity card
72
+ <EntityLayout.Route path="/" title="Overview">
73
+ <EntityRadarChartCard />
74
+ </EntityLayout.Route>
75
+ ```
76
+
77
+ ## Annotations: `EntityRadarChartCard`
78
+
79
+ ### Required: `stupid-radar-chart/kpi-url`
80
+
81
+ Absolute URL that returns a JSON object mapping KPI name → integer in `[1, 100]`. The card fetches this URL on render, so KPI values can change without editing `catalog-info.yaml`.
82
+
83
+ The endpoint must:
84
+
85
+ - respond with `Content-Type: application/json`,
86
+ - return a JSON object (not an array),
87
+ - allow CORS requests from the Backstage origin (`Access-Control-Allow-Origin`).
88
+
89
+ Unknown KPI keys are preserved (forward-compatible). The locked KPI names (`author`, `ai`, `team`, `research`, `unspecified`) default to `50` when omitted from the payload.
90
+
91
+ ### Optional
92
+
93
+ - `stupid-radar-chart/title` — overrides the card title (defaults to entity name).
94
+ - `stupid-radar-chart/show-author` — `"true"` (default) or `"false"`.
95
+
96
+ ### Example `catalog-info.yaml`
97
+
98
+ ```yaml
99
+ apiVersion: backstage.io/v1alpha1
100
+ kind: Component
101
+ metadata:
102
+ name: my-service
103
+ annotations:
104
+ stupid-radar-chart/kpi-url: 'https://kpis.example.test/my-service.json'
105
+ stupid-radar-chart/title: 'My Service KPIs'
106
+ stupid-radar-chart/show-author: 'true'
107
+ spec:
108
+ type: service
109
+ lifecycle: production
110
+ owner: team-a
111
+ ```
112
+
113
+ ### Expected payload shape
114
+
115
+ ```json
116
+ {
117
+ "author": 85,
118
+ "ai": 40,
119
+ "team": 90,
120
+ "research": 60,
121
+ "unspecified": 50,
122
+ "customKpi": 75
123
+ }
124
+ ```
125
+
126
+ Behavior:
127
+
128
+ - annotation missing → card not rendered,
129
+ - URL not parseable, fetch fails, or payload invalid → `ResponseErrorPanel` rendered inside the card,
130
+ - request in flight → `Progress` indicator inside the card.
131
+
132
+ ## `RadarPage`
133
+
134
+ Mount on a route (`/radar` by convention) to expose:
135
+
136
+ - a configuration form (title, author, deliverable type, locked KPIs, optional custom KPI),
137
+ - a live preview rendered with `react-chartjs-2`,
138
+ - a "Generate PNG & Save" action that persists the chart on the remote service, downloads the PNG, and exposes a shareable URL.
139
+
140
+ ## Configuration reference
141
+
142
+ | Key | Type | Required | Description |
143
+ | -------------------- | ------ | -------- | -------------------------------------------- |
144
+ | `radarChart.baseUrl` | string | yes | Base URL of the remote chart-rendering service. |
145
+
146
+ ## Release / CI
147
+
148
+ - `.github/workflows/ci.yml` — lint + test + build on push and PR to `main`.
149
+ - `.github/workflows/release.yml` — publishes to npm and creates a GitHub release on tags matching `v*.*.*`. Requires the `NPM_TOKEN` repository secret (npm automation token with publish access to the `@ehicoso` scope).
150
+
151
+ ## License
152
+
153
+ MIT
package/config.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface Config {
2
+ radarChart?: {
3
+ /**
4
+ * Base URL of the upstream radar chart service.
5
+ * Required: the plugin throws at runtime if this is missing.
6
+ * @visibility frontend
7
+ */
8
+ baseUrl: string;
9
+ };
10
+ }
@@ -0,0 +1,8 @@
1
+ import { createApiRef } from '@backstage/core-plugin-api';
2
+
3
+ const radarApiRef = createApiRef({
4
+ id: "plugin.radar-chart.service"
5
+ });
6
+
7
+ export { radarApiRef };
8
+ //# sourceMappingURL=RadarApi.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RadarApi.esm.js","sources":["../../src/api/RadarApi.ts"],"sourcesContent":["import { createApiRef } from '@backstage/core-plugin-api';\nimport { GenerateRequest, SavedChart, ChartConfig } from './types';\n\nexport interface RadarApi {\n generatePng(req: GenerateRequest): Promise<Blob>;\n saveChart(config: ChartConfig): Promise<{ slug: string; url: string }>;\n getChart(slug: string): Promise<SavedChart>;\n}\n\nexport const radarApiRef = createApiRef<RadarApi>({\n id: 'plugin.radar-chart.service',\n});\n"],"names":[],"mappings":";;AASO,MAAM,cAAc,YAAA,CAAuB;AAAA,EAChD,EAAA,EAAI;AACN,CAAC;;;;"}
@@ -0,0 +1,74 @@
1
+ import { ResponseError } from '@backstage/errors';
2
+ import { getBaseUrl } from '../config.esm.js';
3
+
4
+ class RadarApiClient {
5
+ baseUrl;
6
+ constructor(config) {
7
+ this.baseUrl = getBaseUrl(config);
8
+ }
9
+ async generatePng(req) {
10
+ const { title, author, deliverableType, kpis, showAuthor } = req;
11
+ const response = await fetch(`${this.baseUrl}/api/generate-radar`, {
12
+ method: "POST",
13
+ headers: { "Content-Type": "application/json" },
14
+ body: JSON.stringify({
15
+ title,
16
+ author,
17
+ deliverable_type: deliverableType,
18
+ kpis,
19
+ show_author: showAuthor
20
+ })
21
+ });
22
+ if (!response.ok) {
23
+ throw await ResponseError.fromResponse(response);
24
+ }
25
+ return response.blob();
26
+ }
27
+ async saveChart(config) {
28
+ const { title, author, deliverableType, showAuthor, lockedValues, extraKpi } = config;
29
+ const response = await fetch(`${this.baseUrl}/api/charts?out=picture`, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({
33
+ title,
34
+ author,
35
+ deliverable_type: deliverableType,
36
+ show_author: showAuthor,
37
+ locked_values: lockedValues,
38
+ extra_kpi: extraKpi
39
+ })
40
+ });
41
+ if (!response.ok) {
42
+ throw await ResponseError.fromResponse(response);
43
+ }
44
+ await response.blob();
45
+ const slug = response.headers.get("X-Chart-Slug");
46
+ if (!slug) {
47
+ throw new Error("No chart slug returned from server");
48
+ }
49
+ return {
50
+ slug,
51
+ url: `${this.baseUrl}/s/${slug}`
52
+ };
53
+ }
54
+ async getChart(slug) {
55
+ const response = await fetch(`${this.baseUrl}/api/charts/${slug}`);
56
+ if (!response.ok) {
57
+ throw await ResponseError.fromResponse(response);
58
+ }
59
+ const data = await response.json();
60
+ return {
61
+ slug: data.slug,
62
+ title: data.title,
63
+ author: data.author,
64
+ deliverableType: data.deliverableType,
65
+ showAuthor: data.showAuthor,
66
+ lockedValues: data.lockedValues,
67
+ extraKpi: data.extraKpi,
68
+ createdAt: data.createdAt
69
+ };
70
+ }
71
+ }
72
+
73
+ export { RadarApiClient };
74
+ //# sourceMappingURL=RadarApiClient.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RadarApiClient.esm.js","sources":["../../src/api/RadarApiClient.ts"],"sourcesContent":["import { ConfigApi } from '@backstage/core-plugin-api';\nimport { ResponseError } from '@backstage/errors';\nimport { RadarApi } from './RadarApi';\nimport { GenerateRequest, SavedChart, ChartConfig } from './types';\nimport { getBaseUrl } from '../config';\n\nexport class RadarApiClient implements RadarApi {\n private baseUrl: string;\n\n constructor(config: ConfigApi) {\n this.baseUrl = getBaseUrl(config);\n }\n\n async generatePng(req: GenerateRequest): Promise<Blob> {\n const { title, author, deliverableType, kpis, showAuthor } = req;\n const response = await fetch(`${this.baseUrl}/api/generate-radar`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n title,\n author,\n deliverable_type: deliverableType,\n kpis,\n show_author: showAuthor,\n }),\n });\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n return response.blob();\n }\n\n async saveChart(config: ChartConfig): Promise<{ slug: string; url: string }> {\n const { title, author, deliverableType, showAuthor, lockedValues, extraKpi } = config;\n\n const response = await fetch(`${this.baseUrl}/api/charts?out=picture`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n title,\n author,\n deliverable_type: deliverableType,\n show_author: showAuthor,\n locked_values: lockedValues,\n extra_kpi: extraKpi,\n }),\n });\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n // We read the blob to consume the response, but we don't need to use it\n // since we just need the slug from the headers\n await response.blob();\n const slug = response.headers.get('X-Chart-Slug');\n\n if (!slug) {\n throw new Error('No chart slug returned from server');\n }\n\n return {\n slug,\n url: `${this.baseUrl}/s/${slug}`,\n };\n }\n\n async getChart(slug: string): Promise<SavedChart> {\n const response = await fetch(`${this.baseUrl}/api/charts/${slug}`);\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const data = await response.json();\n\n return {\n slug: data.slug,\n title: data.title,\n author: data.author,\n deliverableType: data.deliverableType,\n showAuthor: data.showAuthor,\n lockedValues: data.lockedValues,\n extraKpi: data.extraKpi,\n createdAt: data.createdAt,\n };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useAsync } from 'react-use';
3
+ import { useEntity } from '@backstage/plugin-catalog-react';
4
+ import { InfoCard, ResponseErrorPanel, Progress } from '@backstage/core-components';
5
+ import { Chart, RadialLinearScale, LineElement, PointElement, Filler, Tooltip } from 'chart.js';
6
+ import { Radar } from 'react-chartjs-2';
7
+ import { entityHasRadarChartAnnotation, parseRadarAnnotations, AnnotationParseError, validateKpisPayload } from './annotations.esm.js';
8
+
9
+ Chart.register(RadialLinearScale, LineElement, PointElement, Filler, Tooltip);
10
+ const EntityRadarChartCard = () => {
11
+ const { entity } = useEntity();
12
+ if (!entityHasRadarChartAnnotation(entity)) {
13
+ return null;
14
+ }
15
+ let annotation;
16
+ try {
17
+ annotation = parseRadarAnnotations(entity);
18
+ } catch (e) {
19
+ const message = e instanceof AnnotationParseError ? e.message : String(e);
20
+ return /* @__PURE__ */ jsx(InfoCard, { title: "Radar Chart", children: /* @__PURE__ */ jsx(ResponseErrorPanel, { error: new Error(message) }) });
21
+ }
22
+ if (!annotation) {
23
+ return null;
24
+ }
25
+ const cardTitle = annotation.title || entity.metadata.name || "Radar Chart";
26
+ return /* @__PURE__ */ jsx(KpiChart, { kpiUrl: annotation.kpiUrl, title: cardTitle });
27
+ };
28
+ const KpiChart = ({ kpiUrl, title }) => {
29
+ const { loading, error, value: kpis } = useAsync(async () => {
30
+ const response = await fetch(kpiUrl);
31
+ if (!response.ok) {
32
+ throw new Error(`Failed to fetch KPIs (${response.status} ${response.statusText})`);
33
+ }
34
+ const payload = await response.json();
35
+ return validateKpisPayload(payload);
36
+ }, [kpiUrl]);
37
+ if (loading) {
38
+ return /* @__PURE__ */ jsx(InfoCard, { title, children: /* @__PURE__ */ jsx(Progress, {}) });
39
+ }
40
+ if (error || !kpis) {
41
+ return /* @__PURE__ */ jsx(InfoCard, { title, children: /* @__PURE__ */ jsx(ResponseErrorPanel, { error: error ?? new Error("Empty KPI payload") }) });
42
+ }
43
+ const labels = Object.keys(kpis).sort().map((k) => k.charAt(0).toUpperCase() + k.slice(1));
44
+ const values = labels.map((label) => kpis[label.toLowerCase()]);
45
+ const chartData = {
46
+ labels,
47
+ datasets: [
48
+ {
49
+ label: "KPI Values",
50
+ data: values,
51
+ backgroundColor: "rgba(15, 98, 254, 0.2)",
52
+ borderColor: "#0F62FE",
53
+ borderWidth: 2,
54
+ pointBackgroundColor: "#6b46ff",
55
+ pointBorderColor: "#0F62FE",
56
+ pointBorderWidth: 2,
57
+ pointRadius: 4
58
+ }
59
+ ]
60
+ };
61
+ const chartOptions = {
62
+ maintainAspectRatio: true,
63
+ responsive: true,
64
+ elements: { line: { tension: 0.4 } },
65
+ scales: {
66
+ r: {
67
+ min: 0,
68
+ max: 100,
69
+ grid: { color: "rgba(0, 0, 0, 0.05)" },
70
+ angleLines: { color: "rgba(0, 0, 0, 0.05)" },
71
+ pointLabels: {
72
+ color: "#0d0d12",
73
+ font: { size: 11, weight: 500 }
74
+ },
75
+ ticks: { display: false, backdropColor: "transparent" }
76
+ }
77
+ },
78
+ plugins: {
79
+ legend: { display: false },
80
+ title: { display: false },
81
+ tooltip: {
82
+ callbacks: {
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ label: (context) => `${context.label}: ${context.parsed.r}`
85
+ }
86
+ }
87
+ }
88
+ };
89
+ return /* @__PURE__ */ jsx(InfoCard, { title, children: /* @__PURE__ */ jsx("div", { style: { height: 250, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx(Radar, { data: chartData, options: chartOptions }) }) });
90
+ };
91
+
92
+ export { EntityRadarChartCard };
93
+ //# sourceMappingURL=EntityRadarChartCard.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityRadarChartCard.esm.js","sources":["../../../src/components/EntityRadarChartCard/EntityRadarChartCard.tsx"],"sourcesContent":["import React from 'react';\nimport { useAsync } from 'react-use';\nimport { useEntity } from '@backstage/plugin-catalog-react';\nimport { ResponseErrorPanel, InfoCard, Progress } from '@backstage/core-components';\nimport {\n Chart as ChartJS,\n RadialLinearScale,\n LineElement,\n PointElement,\n Filler,\n Tooltip,\n} from 'chart.js';\nimport { Radar } from 'react-chartjs-2';\nimport {\n parseRadarAnnotations,\n validateKpisPayload,\n AnnotationParseError,\n entityHasRadarChartAnnotation,\n} from './annotations';\n\nChartJS.register(RadialLinearScale, LineElement, PointElement, Filler, Tooltip);\n\nexport const EntityRadarChartCard: React.FC = () => {\n const { entity } = useEntity();\n\n if (!entityHasRadarChartAnnotation(entity)) {\n return null;\n }\n\n let annotation;\n try {\n annotation = parseRadarAnnotations(entity);\n } catch (e) {\n const message = e instanceof AnnotationParseError ? e.message : String(e);\n return (\n <InfoCard title=\"Radar Chart\">\n <ResponseErrorPanel error={new Error(message)} />\n </InfoCard>\n );\n }\n\n if (!annotation) {\n return null;\n }\n\n const cardTitle = annotation.title || entity.metadata.name || 'Radar Chart';\n\n return <KpiChart kpiUrl={annotation.kpiUrl} title={cardTitle} />;\n};\n\nconst KpiChart: React.FC<{ kpiUrl: string; title: string }> = ({ kpiUrl, title }) => {\n const { loading, error, value: kpis } = useAsync(async () => {\n const response = await fetch(kpiUrl);\n if (!response.ok) {\n throw new Error(`Failed to fetch KPIs (${response.status} ${response.statusText})`);\n }\n const payload = await response.json();\n return validateKpisPayload(payload);\n }, [kpiUrl]);\n\n if (loading) {\n return (\n <InfoCard title={title}>\n <Progress />\n </InfoCard>\n );\n }\n\n if (error || !kpis) {\n return (\n <InfoCard title={title}>\n <ResponseErrorPanel error={error ?? new Error('Empty KPI payload')} />\n </InfoCard>\n );\n }\n\n const labels = Object.keys(kpis)\n .sort()\n .map(k => k.charAt(0).toUpperCase() + k.slice(1));\n const values = labels.map(label => kpis[label.toLowerCase()]);\n\n const chartData = {\n labels,\n datasets: [\n {\n label: 'KPI Values',\n data: values,\n backgroundColor: 'rgba(15, 98, 254, 0.2)',\n borderColor: '#0F62FE',\n borderWidth: 2,\n pointBackgroundColor: '#6b46ff',\n pointBorderColor: '#0F62FE',\n pointBorderWidth: 2,\n pointRadius: 4,\n },\n ],\n };\n\n const chartOptions = {\n maintainAspectRatio: true,\n responsive: true,\n elements: { line: { tension: 0.4 } },\n scales: {\n r: {\n min: 0,\n max: 100,\n grid: { color: 'rgba(0, 0, 0, 0.05)' },\n angleLines: { color: 'rgba(0, 0, 0, 0.05)' },\n pointLabels: {\n color: '#0d0d12',\n font: { size: 11, weight: 500 },\n },\n ticks: { display: false, backdropColor: 'transparent' },\n },\n },\n plugins: {\n legend: { display: false },\n title: { display: false },\n tooltip: {\n callbacks: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n label: (context: any) => `${context.label}: ${context.parsed.r}`,\n },\n },\n },\n };\n\n return (\n <InfoCard title={title}>\n <div style={{ height: 250, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>\n <Radar data={chartData} options={chartOptions} />\n </div>\n </InfoCard>\n );\n};\n"],"names":["ChartJS"],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ const RADAR_CHART_KPI_URL_ANNOTATION = "stupid-radar-chart/kpi-url";
2
+ const RADAR_CHART_TITLE_ANNOTATION = "stupid-radar-chart/title";
3
+ const RADAR_CHART_SHOW_AUTHOR_ANNOTATION = "stupid-radar-chart/show-author";
4
+ const LOCKED_KPI_NAMES = ["author", "ai", "team", "research", "unspecified"];
5
+ const DEFAULT_KPI_VALUE = 50;
6
+ class AnnotationParseError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "AnnotationParseError";
10
+ }
11
+ }
12
+ function entityHasRadarChartAnnotation(entity) {
13
+ const annotations = entity.metadata.annotations || {};
14
+ return RADAR_CHART_KPI_URL_ANNOTATION in annotations;
15
+ }
16
+ function parseRadarAnnotations(entity) {
17
+ const annotations = entity.metadata.annotations || {};
18
+ const rawUrl = annotations[RADAR_CHART_KPI_URL_ANNOTATION];
19
+ if (!rawUrl) {
20
+ return null;
21
+ }
22
+ const kpiUrl = rawUrl.trim();
23
+ try {
24
+ new URL(kpiUrl);
25
+ } catch {
26
+ throw new AnnotationParseError(
27
+ `${RADAR_CHART_KPI_URL_ANNOTATION} must be a valid absolute URL (got "${rawUrl}")`
28
+ );
29
+ }
30
+ const title = annotations[RADAR_CHART_TITLE_ANNOTATION];
31
+ const showAuthorStr = annotations[RADAR_CHART_SHOW_AUTHOR_ANNOTATION];
32
+ const showAuthor = showAuthorStr !== "false";
33
+ return { kpiUrl, title, showAuthor };
34
+ }
35
+ function validateKpisPayload(payload) {
36
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
37
+ throw new AnnotationParseError(
38
+ `KPI payload must be a JSON object mapping name to number (1..100)`
39
+ );
40
+ }
41
+ const kpis = {};
42
+ for (const [name, value] of Object.entries(payload)) {
43
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1 || value > 100) {
44
+ throw new AnnotationParseError(
45
+ `KPI '${name}' has invalid value ${String(value)}. Must be a number 1..100.`
46
+ );
47
+ }
48
+ kpis[name] = value;
49
+ }
50
+ for (const lockedName of LOCKED_KPI_NAMES) {
51
+ if (!(lockedName in kpis)) {
52
+ kpis[lockedName] = DEFAULT_KPI_VALUE;
53
+ }
54
+ }
55
+ return kpis;
56
+ }
57
+
58
+ export { AnnotationParseError, RADAR_CHART_KPI_URL_ANNOTATION, RADAR_CHART_SHOW_AUTHOR_ANNOTATION, RADAR_CHART_TITLE_ANNOTATION, entityHasRadarChartAnnotation, parseRadarAnnotations, validateKpisPayload };
59
+ //# sourceMappingURL=annotations.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"annotations.esm.js","sources":["../../../src/components/EntityRadarChartCard/annotations.ts"],"sourcesContent":["import { Entity } from '@backstage/catalog-model';\n\nexport const RADAR_CHART_KPI_URL_ANNOTATION = 'stupid-radar-chart/kpi-url';\nexport const RADAR_CHART_TITLE_ANNOTATION = 'stupid-radar-chart/title';\nexport const RADAR_CHART_SHOW_AUTHOR_ANNOTATION = 'stupid-radar-chart/show-author';\n\nconst LOCKED_KPI_NAMES = ['author', 'ai', 'team', 'research', 'unspecified'];\nconst DEFAULT_KPI_VALUE = 50;\n\nexport interface RadarAnnotationData {\n kpiUrl: string;\n title?: string;\n showAuthor: boolean;\n}\n\nexport class AnnotationParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AnnotationParseError';\n }\n}\n\nexport function entityHasRadarChartAnnotation(entity: Entity): boolean {\n const annotations = entity.metadata.annotations || {};\n return RADAR_CHART_KPI_URL_ANNOTATION in annotations;\n}\n\nexport function parseRadarAnnotations(entity: Entity): RadarAnnotationData | null {\n const annotations = entity.metadata.annotations || {};\n\n const rawUrl = annotations[RADAR_CHART_KPI_URL_ANNOTATION];\n if (!rawUrl) {\n return null;\n }\n\n const kpiUrl = rawUrl.trim();\n try {\n // eslint-disable-next-line no-new\n new URL(kpiUrl);\n } catch {\n throw new AnnotationParseError(\n `${RADAR_CHART_KPI_URL_ANNOTATION} must be a valid absolute URL (got \"${rawUrl}\")`,\n );\n }\n\n const title = annotations[RADAR_CHART_TITLE_ANNOTATION];\n const showAuthorStr = annotations[RADAR_CHART_SHOW_AUTHOR_ANNOTATION];\n const showAuthor = showAuthorStr !== 'false';\n\n return { kpiUrl, title, showAuthor };\n}\n\nexport function validateKpisPayload(payload: unknown): Record<string, number> {\n if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\n throw new AnnotationParseError(\n `KPI payload must be a JSON object mapping name to number (1..100)`,\n );\n }\n\n const kpis: Record<string, number> = {};\n for (const [name, value] of Object.entries(payload as Record<string, unknown>)) {\n if (typeof value !== 'number' || !Number.isFinite(value) || value < 1 || value > 100) {\n throw new AnnotationParseError(\n `KPI '${name}' has invalid value ${String(value)}. Must be a number 1..100.`,\n );\n }\n kpis[name] = value;\n }\n\n for (const lockedName of LOCKED_KPI_NAMES) {\n if (!(lockedName in kpis)) {\n kpis[lockedName] = DEFAULT_KPI_VALUE;\n }\n }\n\n return kpis;\n}\n"],"names":[],"mappings":"AAEO,MAAM,8BAAA,GAAiC;AACvC,MAAM,4BAAA,GAA+B;AACrC,MAAM,kCAAA,GAAqC;AAElD,MAAM,mBAAmB,CAAC,QAAA,EAAU,IAAA,EAAM,MAAA,EAAQ,YAAY,aAAa,CAAA;AAC3E,MAAM,iBAAA,GAAoB,EAAA;AAQnB,MAAM,6BAA6B,KAAA,CAAM;AAAA,EAC9C,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,sBAAA;AAAA,EACd;AACF;AAEO,SAAS,8BAA8B,MAAA,EAAyB;AACrE,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,CAAS,WAAA,IAAe,EAAC;AACpD,EAAA,OAAO,8BAAA,IAAkC,WAAA;AAC3C;AAEO,SAAS,sBAAsB,MAAA,EAA4C;AAChF,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,CAAS,WAAA,IAAe,EAAC;AAEpD,EAAA,MAAM,MAAA,GAAS,YAAY,8BAA8B,CAAA;AACzD,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,MAAA,GAAS,OAAO,IAAA,EAAK;AAC3B,EAAA,IAAI;AAEF,IAAA,IAAI,IAAI,MAAM,CAAA;AAAA,EAChB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,oBAAA;AAAA,MACR,CAAA,EAAG,8BAA8B,CAAA,oCAAA,EAAuC,MAAM,CAAA,EAAA;AAAA,KAChF;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,YAAY,4BAA4B,CAAA;AACtD,EAAA,MAAM,aAAA,GAAgB,YAAY,kCAAkC,CAAA;AACpE,EAAA,MAAM,aAAa,aAAA,KAAkB,OAAA;AAErC,EAAA,OAAO,EAAE,MAAA,EAAQ,KAAA,EAAO,UAAA,EAAW;AACrC;AAEO,SAAS,oBAAoB,OAAA,EAA0C;AAC5E,EAAA,IAAI,CAAC,WAAW,OAAO,OAAA,KAAY,YAAY,KAAA,CAAM,OAAA,CAAQ,OAAO,CAAA,EAAG;AACrE,IAAA,MAAM,IAAI,oBAAA;AAAA,MACR,CAAA,iEAAA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,OAA+B,EAAC;AACtC,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAkC,CAAA,EAAG;AAC9E,IAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,CAAC,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,IAAK,KAAA,GAAQ,CAAA,IAAK,KAAA,GAAQ,GAAA,EAAK;AACpF,MAAA,MAAM,IAAI,oBAAA;AAAA,QACR,CAAA,KAAA,EAAQ,IAAI,CAAA,oBAAA,EAAuB,MAAA,CAAO,KAAK,CAAC,CAAA,0BAAA;AAAA,OAClD;AAAA,IACF;AACA,IAAA,IAAA,CAAK,IAAI,CAAA,GAAI,KAAA;AAAA,EACf;AAEA,EAAA,KAAA,MAAW,cAAc,gBAAA,EAAkB;AACzC,IAAA,IAAI,EAAE,cAAc,IAAA,CAAA,EAAO;AACzB,MAAA,IAAA,CAAK,UAAU,CAAA,GAAI,iBAAA;AAAA,IACrB;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;;;;"}
@@ -0,0 +1,6 @@
1
+ import { EntityRadarChartCard } from './EntityRadarChartCard.esm.js';
2
+
3
+
4
+
5
+ export { EntityRadarChartCard };
6
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
@@ -0,0 +1,81 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useMemo } from 'react';
3
+ import { Chart, RadialLinearScale, LineElement, PointElement, Filler, Tooltip, Title } from 'chart.js';
4
+ import { Radar } from 'react-chartjs-2';
5
+ import { Box } from '@material-ui/core';
6
+
7
+ Chart.register(RadialLinearScale, LineElement, PointElement, Filler, Tooltip, Title);
8
+ const LOCKED_KPI_NAMES = ["author", "ai", "team", "research", "unspecified"];
9
+ const ChartPreview = ({
10
+ title,
11
+ author,
12
+ showAuthor,
13
+ lockedValues,
14
+ extraKpi
15
+ }) => {
16
+ const labels = useMemo(() => {
17
+ const locked = LOCKED_KPI_NAMES.map((k) => k.charAt(0).toUpperCase() + k.slice(1));
18
+ const extra = extraKpi ? [extraKpi.name.charAt(0).toUpperCase() + extraKpi.name.slice(1)] : [];
19
+ return [...locked, ...extra];
20
+ }, [extraKpi]);
21
+ const values = useMemo(() => {
22
+ const locked = LOCKED_KPI_NAMES.map((name) => lockedValues[name] ?? 50);
23
+ const extra = extraKpi ? [extraKpi.value] : [];
24
+ return [...locked, ...extra];
25
+ }, [lockedValues, extraKpi]);
26
+ const data = useMemo(
27
+ () => ({
28
+ labels,
29
+ datasets: [
30
+ {
31
+ label: "KPI Values",
32
+ data: values,
33
+ backgroundColor: "rgba(15, 98, 254, 0.2)",
34
+ borderColor: "#0F62FE",
35
+ borderWidth: 2,
36
+ pointBackgroundColor: "#6b46ff",
37
+ pointBorderColor: "#0F62FE",
38
+ pointBorderWidth: 2,
39
+ pointRadius: 5
40
+ }
41
+ ]
42
+ }),
43
+ [labels, values]
44
+ );
45
+ const options = useMemo(
46
+ () => ({
47
+ maintainAspectRatio: false,
48
+ elements: { line: { tension: 0.4 } },
49
+ scales: {
50
+ r: {
51
+ min: 0,
52
+ max: 100,
53
+ grid: { color: "rgba(0, 0, 0, 0.05)" },
54
+ angleLines: { color: "rgba(0, 0, 0, 0.05)" },
55
+ pointLabels: {
56
+ color: "#0d0d12",
57
+ font: { size: 13, weight: 500 }
58
+ },
59
+ ticks: { display: false, backdropColor: "transparent" }
60
+ }
61
+ },
62
+ plugins: {
63
+ legend: {
64
+ labels: { color: "#0d0d12", font: { weight: 500 } }
65
+ },
66
+ title: {
67
+ display: true,
68
+ text: showAuthor ? `${title}
69
+ by ${author}` : title,
70
+ color: "#0d0d12",
71
+ font: { size: 16, weight: 700 }
72
+ }
73
+ }
74
+ }),
75
+ [title, author, showAuthor]
76
+ );
77
+ return /* @__PURE__ */ jsx(Box, { style: { height: 350, width: "100%" }, children: /* @__PURE__ */ jsx(Radar, { data, options }) });
78
+ };
79
+
80
+ export { ChartPreview };
81
+ //# sourceMappingURL=ChartPreview.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartPreview.esm.js","sources":["../../../src/components/RadarPage/ChartPreview.tsx"],"sourcesContent":["import React, { useMemo } from 'react';\nimport {\n Chart as ChartJS,\n RadialLinearScale,\n LineElement,\n PointElement,\n Filler,\n Tooltip,\n Title,\n ChartDataset,\n ChartOptions,\n} from 'chart.js';\nimport { Radar } from 'react-chartjs-2';\nimport { Box } from '@material-ui/core';\n\nChartJS.register(RadialLinearScale, LineElement, PointElement, Filler, Tooltip, Title);\n\nexport interface ChartPreviewProps {\n title: string;\n author: string;\n showAuthor: boolean;\n lockedValues: Record<string, number>;\n extraKpi: { name: string; value: number } | null;\n}\n\nconst LOCKED_KPI_NAMES = ['author', 'ai', 'team', 'research', 'unspecified'];\n\nexport const ChartPreview: React.FC<ChartPreviewProps> = ({\n title,\n author,\n showAuthor,\n lockedValues,\n extraKpi,\n}) => {\n const labels = useMemo(() => {\n const locked = LOCKED_KPI_NAMES.map(k => k.charAt(0).toUpperCase() + k.slice(1));\n const extra = extraKpi ? [extraKpi.name.charAt(0).toUpperCase() + extraKpi.name.slice(1)] : [];\n return [...locked, ...extra];\n }, [extraKpi]);\n\n const values = useMemo(() => {\n const locked = LOCKED_KPI_NAMES.map(name => lockedValues[name] ?? 50);\n const extra = extraKpi ? [extraKpi.value] : [];\n return [...locked, ...extra];\n }, [lockedValues, extraKpi]);\n\n const data = useMemo(\n () => ({\n labels,\n datasets: [\n {\n label: 'KPI Values',\n data: values,\n backgroundColor: 'rgba(15, 98, 254, 0.2)',\n borderColor: '#0F62FE',\n borderWidth: 2,\n pointBackgroundColor: '#6b46ff',\n pointBorderColor: '#0F62FE',\n pointBorderWidth: 2,\n pointRadius: 5,\n } as ChartDataset<'radar', number[]>,\n ],\n }),\n [labels, values]\n );\n\n const options = useMemo(\n () =>\n ({\n maintainAspectRatio: false,\n elements: { line: { tension: 0.4 } },\n scales: {\n r: {\n min: 0,\n max: 100,\n grid: { color: 'rgba(0, 0, 0, 0.05)' },\n angleLines: { color: 'rgba(0, 0, 0, 0.05)' },\n pointLabels: {\n color: '#0d0d12',\n font: { size: 13, weight: 500 },\n },\n ticks: { display: false, backdropColor: 'transparent' },\n },\n },\n plugins: {\n legend: {\n labels: { color: '#0d0d12', font: { weight: 500 } },\n },\n title: {\n display: true,\n text: showAuthor ? `${title}\\nby ${author}` : title,\n color: '#0d0d12',\n font: { size: 16, weight: 700 },\n },\n },\n } as ChartOptions<'radar'>),\n [title, author, showAuthor]\n );\n\n return (\n <Box style={{ height: 350, width: '100%' }}>\n <Radar data={data} options={options} />\n </Box>\n );\n};\n"],"names":["ChartJS"],"mappings":"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"}
@@ -0,0 +1,167 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useCallback } from 'react';
3
+ import { Paper, Box, Typography, Button, Grid, TextField, Select, FormControlLabel, Switch, Slider } from '@material-ui/core';
4
+
5
+ const LOCKED_KPI_NAMES = ["author", "ai", "team", "research", "unspecified"];
6
+ const ConfigForm = ({ config, onChange, onReset }) => {
7
+ const handleFieldChange = useCallback(
8
+ (key, value) => {
9
+ onChange({ ...config, [key]: value });
10
+ },
11
+ [config, onChange]
12
+ );
13
+ const handleLockedKpiChange = useCallback(
14
+ (name, value) => {
15
+ onChange({
16
+ ...config,
17
+ lockedValues: { ...config.lockedValues, [name]: value }
18
+ });
19
+ },
20
+ [config, onChange]
21
+ );
22
+ const handleExtraKpiNameChange = useCallback(
23
+ (name) => {
24
+ if (config.extraKpi) {
25
+ onChange({
26
+ ...config,
27
+ extraKpi: { ...config.extraKpi, name }
28
+ });
29
+ }
30
+ },
31
+ [config, onChange]
32
+ );
33
+ const handleExtraKpiValueChange = useCallback(
34
+ (value) => {
35
+ if (config.extraKpi) {
36
+ onChange({
37
+ ...config,
38
+ extraKpi: { ...config.extraKpi, value }
39
+ });
40
+ }
41
+ },
42
+ [config, onChange]
43
+ );
44
+ const addExtraKpi = useCallback(() => {
45
+ onChange({ ...config, extraKpi: { name: "custom", value: 50 } });
46
+ }, [config, onChange]);
47
+ const removeExtraKpi = useCallback(() => {
48
+ onChange({ ...config, extraKpi: null });
49
+ }, [config, onChange]);
50
+ return /* @__PURE__ */ jsxs(Paper, { style: { padding: 24 }, children: [
51
+ /* @__PURE__ */ jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 2, children: [
52
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", children: "Configuration" }),
53
+ /* @__PURE__ */ jsx(Button, { onClick: onReset, size: "small", children: "Reset" })
54
+ ] }),
55
+ /* @__PURE__ */ jsxs(Grid, { container: true, spacing: 3, children: [
56
+ /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(
57
+ TextField,
58
+ {
59
+ fullWidth: true,
60
+ label: "Project Title",
61
+ value: config.title,
62
+ onChange: (e) => handleFieldChange("title", e.target.value),
63
+ variant: "outlined",
64
+ size: "small"
65
+ }
66
+ ) }),
67
+ /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(
68
+ TextField,
69
+ {
70
+ fullWidth: true,
71
+ label: "Author",
72
+ value: config.author,
73
+ onChange: (e) => handleFieldChange("author", e.target.value),
74
+ variant: "outlined",
75
+ size: "small"
76
+ }
77
+ ) }),
78
+ /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsxs(
79
+ Select,
80
+ {
81
+ native: true,
82
+ fullWidth: true,
83
+ value: config.deliverableType,
84
+ onChange: (e) => handleFieldChange("deliverableType", e.target.value),
85
+ children: [
86
+ /* @__PURE__ */ jsx("option", { value: "slideshow", children: "Slide Deck" }),
87
+ /* @__PURE__ */ jsx("option", { value: "code", children: "Code" }),
88
+ /* @__PURE__ */ jsx("option", { value: "workbook", children: "Workbook" }),
89
+ /* @__PURE__ */ jsx("option", { value: "other", children: "Other" })
90
+ ]
91
+ }
92
+ ) }),
93
+ /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(
94
+ FormControlLabel,
95
+ {
96
+ control: /* @__PURE__ */ jsx(
97
+ Switch,
98
+ {
99
+ checked: config.showAuthor,
100
+ onChange: (e) => handleFieldChange("showAuthor", e.target.checked)
101
+ }
102
+ ),
103
+ label: "Show author name"
104
+ }
105
+ ) }),
106
+ /* @__PURE__ */ jsxs(Grid, { item: true, xs: 12, children: [
107
+ /* @__PURE__ */ jsxs(Typography, { variant: "subtitle1", gutterBottom: true, children: [
108
+ "KPIs (",
109
+ LOCKED_KPI_NAMES.length + (config.extraKpi ? 1 : 0),
110
+ ")"
111
+ ] }),
112
+ LOCKED_KPI_NAMES.map((name) => /* @__PURE__ */ jsxs(Box, { marginBottom: 3, children: [
113
+ /* @__PURE__ */ jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 1, children: [
114
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", style: { textTransform: "capitalize" }, children: name }),
115
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", style: { fontWeight: "bold" }, children: config.lockedValues[name] ?? 50 })
116
+ ] }),
117
+ /* @__PURE__ */ jsx(
118
+ Slider,
119
+ {
120
+ min: 1,
121
+ max: 100,
122
+ value: config.lockedValues[name] ?? 50,
123
+ onChange: (_, value) => handleLockedKpiChange(name, value),
124
+ marks: [
125
+ { value: 1, label: "1" },
126
+ { value: 100, label: "100" }
127
+ ]
128
+ }
129
+ )
130
+ ] }, name)),
131
+ config.extraKpi && /* @__PURE__ */ jsxs(Box, { marginBottom: 3, paddingTop: 2, borderTop: "1px dashed #ccc", children: [
132
+ /* @__PURE__ */ jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 1, children: [
133
+ /* @__PURE__ */ jsx(
134
+ TextField,
135
+ {
136
+ size: "small",
137
+ value: config.extraKpi.name,
138
+ onChange: (e) => handleExtraKpiNameChange(e.target.value),
139
+ variant: "outlined",
140
+ style: { flex: 1, marginRight: 8 }
141
+ }
142
+ ),
143
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", style: { fontWeight: "bold", marginRight: 8 }, children: config.extraKpi.value }),
144
+ /* @__PURE__ */ jsx(Button, { onClick: removeExtraKpi, size: "small", color: "secondary", children: "Remove" })
145
+ ] }),
146
+ /* @__PURE__ */ jsx(
147
+ Slider,
148
+ {
149
+ min: 1,
150
+ max: 100,
151
+ value: config.extraKpi.value,
152
+ onChange: (_, value) => handleExtraKpiValueChange(value),
153
+ marks: [
154
+ { value: 1, label: "1" },
155
+ { value: 100, label: "100" }
156
+ ]
157
+ }
158
+ )
159
+ ] }),
160
+ !config.extraKpi && /* @__PURE__ */ jsx(Box, { marginTop: 2, display: "flex", children: /* @__PURE__ */ jsx(Button, { variant: "outlined", onClick: addExtraKpi, fullWidth: true, children: "+ Add Custom KPI" }) })
161
+ ] })
162
+ ] })
163
+ ] });
164
+ };
165
+
166
+ export { ConfigForm };
167
+ //# sourceMappingURL=ConfigForm.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfigForm.esm.js","sources":["../../../src/components/RadarPage/ConfigForm.tsx"],"sourcesContent":["import React, { useCallback } from 'react';\nimport {\n Box,\n Button,\n FormControlLabel,\n Grid,\n Select,\n Slider,\n Switch,\n TextField,\n Typography,\n Paper,\n} from '@material-ui/core';\nimport { ChartConfig } from '../../api/types';\n\nconst LOCKED_KPI_NAMES = ['author', 'ai', 'team', 'research', 'unspecified'];\n\nexport interface ConfigFormProps {\n config: ChartConfig;\n onChange: (config: ChartConfig) => void;\n onReset: () => void;\n}\n\nexport const ConfigForm: React.FC<ConfigFormProps> = ({ config, onChange, onReset }) => {\n const handleFieldChange = useCallback(\n <K extends keyof ChartConfig>(key: K, value: ChartConfig[K]) => {\n onChange({ ...config, [key]: value });\n },\n [config, onChange]\n );\n\n const handleLockedKpiChange = useCallback(\n (name: string, value: number) => {\n onChange({\n ...config,\n lockedValues: { ...config.lockedValues, [name]: value },\n });\n },\n [config, onChange]\n );\n\n const handleExtraKpiNameChange = useCallback(\n (name: string) => {\n if (config.extraKpi) {\n onChange({\n ...config,\n extraKpi: { ...config.extraKpi, name },\n });\n }\n },\n [config, onChange]\n );\n\n const handleExtraKpiValueChange = useCallback(\n (value: number) => {\n if (config.extraKpi) {\n onChange({\n ...config,\n extraKpi: { ...config.extraKpi, value },\n });\n }\n },\n [config, onChange]\n );\n\n const addExtraKpi = useCallback(() => {\n onChange({ ...config, extraKpi: { name: 'custom', value: 50 } });\n }, [config, onChange]);\n\n const removeExtraKpi = useCallback(() => {\n onChange({ ...config, extraKpi: null });\n }, [config, onChange]);\n\n return (\n <Paper style={{ padding: 24 }}>\n <Box display=\"flex\" justifyContent=\"space-between\" alignItems=\"center\" marginBottom={2}>\n <Typography variant=\"h6\">Configuration</Typography>\n <Button onClick={onReset} size=\"small\">\n Reset\n </Button>\n </Box>\n\n <Grid container spacing={3}>\n <Grid item xs={12}>\n <TextField\n fullWidth\n label=\"Project Title\"\n value={config.title}\n onChange={e => handleFieldChange('title', e.target.value)}\n variant=\"outlined\"\n size=\"small\"\n />\n </Grid>\n\n <Grid item xs={12}>\n <TextField\n fullWidth\n label=\"Author\"\n value={config.author}\n onChange={e => handleFieldChange('author', e.target.value)}\n variant=\"outlined\"\n size=\"small\"\n />\n </Grid>\n\n <Grid item xs={12}>\n <Select\n native\n fullWidth\n value={config.deliverableType}\n onChange={e => handleFieldChange('deliverableType', e.target.value as string)}\n >\n <option value=\"slideshow\">Slide Deck</option>\n <option value=\"code\">Code</option>\n <option value=\"workbook\">Workbook</option>\n <option value=\"other\">Other</option>\n </Select>\n </Grid>\n\n <Grid item xs={12}>\n <FormControlLabel\n control={\n <Switch\n checked={config.showAuthor}\n onChange={e => handleFieldChange('showAuthor', e.target.checked)}\n />\n }\n label=\"Show author name\"\n />\n </Grid>\n\n <Grid item xs={12}>\n <Typography variant=\"subtitle1\" gutterBottom>\n KPIs ({LOCKED_KPI_NAMES.length + (config.extraKpi ? 1 : 0)})\n </Typography>\n\n {LOCKED_KPI_NAMES.map(name => (\n <Box key={name} marginBottom={3}>\n <Box display=\"flex\" justifyContent=\"space-between\" alignItems=\"center\" marginBottom={1}>\n <Typography variant=\"body2\" style={{ textTransform: 'capitalize' }}>\n {name}\n </Typography>\n <Typography variant=\"body2\" style={{ fontWeight: 'bold' }}>\n {config.lockedValues[name] ?? 50}\n </Typography>\n </Box>\n <Slider\n min={1}\n max={100}\n value={config.lockedValues[name] ?? 50}\n onChange={(_, value) => handleLockedKpiChange(name, value as number)}\n marks={[\n { value: 1, label: '1' },\n { value: 100, label: '100' },\n ]}\n />\n </Box>\n ))}\n\n {config.extraKpi && (\n <Box marginBottom={3} paddingTop={2} borderTop=\"1px dashed #ccc\">\n <Box display=\"flex\" justifyContent=\"space-between\" alignItems=\"center\" marginBottom={1}>\n <TextField\n size=\"small\"\n value={config.extraKpi.name}\n onChange={e => handleExtraKpiNameChange(e.target.value)}\n variant=\"outlined\"\n style={{ flex: 1, marginRight: 8 }}\n />\n <Typography variant=\"body2\" style={{ fontWeight: 'bold', marginRight: 8 }}>\n {config.extraKpi.value}\n </Typography>\n <Button onClick={removeExtraKpi} size=\"small\" color=\"secondary\">\n Remove\n </Button>\n </Box>\n <Slider\n min={1}\n max={100}\n value={config.extraKpi.value}\n onChange={(_, value) => handleExtraKpiValueChange(value as number)}\n marks={[\n { value: 1, label: '1' },\n { value: 100, label: '100' },\n ]}\n />\n </Box>\n )}\n\n {!config.extraKpi && (\n <Box marginTop={2} display=\"flex\">\n <Button variant=\"outlined\" onClick={addExtraKpi} fullWidth>\n + Add Custom KPI\n </Button>\n </Box>\n )}\n </Grid>\n </Grid>\n </Paper>\n );\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useState, useCallback } from 'react';
3
+ import { useApi, configApiRef } from '@backstage/core-plugin-api';
4
+ import { Page, Header, Content } from '@backstage/core-components';
5
+ import { Grid, Box, Button, CircularProgress } from '@material-ui/core';
6
+ import { radarApiRef } from '../../api/RadarApi.esm.js';
7
+ import { getBaseUrl } from '../../config.esm.js';
8
+ import { ConfigForm } from './ConfigForm.esm.js';
9
+ import { ChartPreview } from './ChartPreview.esm.js';
10
+ import { ShareBox } from './ShareBox.esm.js';
11
+
12
+ const DEFAULT_CONFIG = {
13
+ title: "Project Radar",
14
+ author: "Team",
15
+ deliverableType: "other",
16
+ showAuthor: true,
17
+ extraKpi: null,
18
+ lockedValues: {
19
+ author: 50,
20
+ ai: 50,
21
+ team: 50,
22
+ research: 50,
23
+ unspecified: 50
24
+ }
25
+ };
26
+ const RadarPage = () => {
27
+ const radarApi = useApi(radarApiRef);
28
+ const configApi = useApi(configApiRef);
29
+ const baseUrl = getBaseUrl(configApi);
30
+ const [config, setConfig] = useState(DEFAULT_CONFIG);
31
+ const [saving, setSaving] = useState(false);
32
+ const [slug, setSlug] = useState(null);
33
+ const handleReset = useCallback(() => {
34
+ setConfig({ ...DEFAULT_CONFIG });
35
+ }, []);
36
+ const handleGenerateAndSave = useCallback(async () => {
37
+ setSaving(true);
38
+ try {
39
+ const result = await radarApi.saveChart(config);
40
+ setSlug(result.slug);
41
+ const kpis = { ...config.lockedValues };
42
+ if (config.extraKpi) {
43
+ kpis[config.extraKpi.name] = config.extraKpi.value;
44
+ }
45
+ const blob = await radarApi.generatePng({
46
+ title: config.title,
47
+ author: config.author,
48
+ deliverableType: config.deliverableType,
49
+ kpis,
50
+ showAuthor: config.showAuthor
51
+ });
52
+ const url = URL.createObjectURL(blob);
53
+ const link = document.createElement("a");
54
+ link.download = `radar-${config.author.replace(/\s+/g, "_")}-${Date.now()}.png`;
55
+ link.href = url;
56
+ link.click();
57
+ URL.revokeObjectURL(url);
58
+ } catch (e) {
59
+ alert(`Failed to generate and save chart: ${e instanceof Error ? e.message : String(e)}`);
60
+ } finally {
61
+ setSaving(false);
62
+ }
63
+ }, [config, radarApi]);
64
+ return /* @__PURE__ */ jsxs(Page, { themeId: "tool", children: [
65
+ /* @__PURE__ */ jsx(Header, { title: "Radar Chart Generator", subtitle: "Create KPI radar charts from your data" }),
66
+ /* @__PURE__ */ jsx(Content, { children: /* @__PURE__ */ jsxs(Grid, { container: true, spacing: 3, children: [
67
+ /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, md: 6, children: /* @__PURE__ */ jsx(ConfigForm, { config, onChange: setConfig, onReset: handleReset }) }),
68
+ /* @__PURE__ */ jsxs(Grid, { item: true, xs: 12, md: 6, children: [
69
+ /* @__PURE__ */ jsx(
70
+ Box,
71
+ {
72
+ style: {
73
+ padding: 24,
74
+ border: "1px solid #e0e0e0",
75
+ borderRadius: 4,
76
+ backgroundColor: "#fafafa"
77
+ },
78
+ children: /* @__PURE__ */ jsx(
79
+ ChartPreview,
80
+ {
81
+ title: config.title,
82
+ author: config.author,
83
+ showAuthor: config.showAuthor,
84
+ lockedValues: config.lockedValues,
85
+ extraKpi: config.extraKpi
86
+ }
87
+ )
88
+ }
89
+ ),
90
+ /* @__PURE__ */ jsx(Box, { marginTop: 2, children: /* @__PURE__ */ jsx(
91
+ Button,
92
+ {
93
+ fullWidth: true,
94
+ variant: "contained",
95
+ color: "primary",
96
+ onClick: handleGenerateAndSave,
97
+ disabled: saving,
98
+ children: saving ? /* @__PURE__ */ jsx(CircularProgress, { size: 24 }) : "Generate PNG & Save"
99
+ }
100
+ ) }),
101
+ slug && /* @__PURE__ */ jsx(ShareBox, { slug, baseUrl })
102
+ ] })
103
+ ] }) })
104
+ ] });
105
+ };
106
+
107
+ export { RadarPage };
108
+ //# sourceMappingURL=RadarPage.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RadarPage.esm.js","sources":["../../../src/components/RadarPage/RadarPage.tsx"],"sourcesContent":["import React, { useState, useCallback } from 'react';\nimport { useApi, configApiRef } from '@backstage/core-plugin-api';\nimport { Page, Header, Content } from '@backstage/core-components';\nimport { Box, Button, Grid, CircularProgress } from '@material-ui/core';\nimport { radarApiRef } from '../../api/RadarApi';\nimport { ChartConfig } from '../../api/types';\nimport { getBaseUrl } from '../../config';\nimport { ConfigForm } from './ConfigForm';\nimport { ChartPreview } from './ChartPreview';\nimport { ShareBox } from './ShareBox';\n\nconst DEFAULT_CONFIG: ChartConfig = {\n title: 'Project Radar',\n author: 'Team',\n deliverableType: 'other',\n showAuthor: true,\n extraKpi: null,\n lockedValues: {\n author: 50,\n ai: 50,\n team: 50,\n research: 50,\n unspecified: 50,\n },\n};\n\nexport const RadarPage: React.FC = () => {\n const radarApi = useApi(radarApiRef);\n const configApi = useApi(configApiRef);\n const baseUrl = getBaseUrl(configApi);\n\n const [config, setConfig] = useState<ChartConfig>(DEFAULT_CONFIG);\n const [saving, setSaving] = useState(false);\n const [slug, setSlug] = useState<string | null>(null);\n\n const handleReset = useCallback(() => {\n setConfig({ ...DEFAULT_CONFIG });\n }, []);\n\n const handleGenerateAndSave = useCallback(async () => {\n setSaving(true);\n try {\n const result = await radarApi.saveChart(config);\n setSlug(result.slug);\n\n // Trigger PNG download\n const kpis: Record<string, number> = { ...config.lockedValues };\n if (config.extraKpi) {\n kpis[config.extraKpi.name] = config.extraKpi.value;\n }\n\n const blob = await radarApi.generatePng({\n title: config.title,\n author: config.author,\n deliverableType: config.deliverableType,\n kpis,\n showAuthor: config.showAuthor,\n });\n\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.download = `radar-${config.author.replace(/\\s+/g, '_')}-${Date.now()}.png`;\n link.href = url;\n link.click();\n URL.revokeObjectURL(url);\n } catch (e) {\n alert(`Failed to generate and save chart: ${e instanceof Error ? e.message : String(e)}`);\n } finally {\n setSaving(false);\n }\n }, [config, radarApi]);\n\n return (\n <Page themeId=\"tool\">\n <Header title=\"Radar Chart Generator\" subtitle=\"Create KPI radar charts from your data\" />\n <Content>\n <Grid container spacing={3}>\n <Grid item xs={12} md={6}>\n <ConfigForm config={config} onChange={setConfig} onReset={handleReset} />\n </Grid>\n <Grid item xs={12} md={6}>\n <Box\n style={{\n padding: 24,\n border: '1px solid #e0e0e0',\n borderRadius: 4,\n backgroundColor: '#fafafa',\n }}\n >\n <ChartPreview\n title={config.title}\n author={config.author}\n showAuthor={config.showAuthor}\n lockedValues={config.lockedValues}\n extraKpi={config.extraKpi}\n />\n </Box>\n\n <Box marginTop={2}>\n <Button\n fullWidth\n variant=\"contained\"\n color=\"primary\"\n onClick={handleGenerateAndSave}\n disabled={saving}\n >\n {saving ? <CircularProgress size={24} /> : 'Generate PNG & Save'}\n </Button>\n </Box>\n\n {slug && <ShareBox slug={slug} baseUrl={baseUrl} />}\n </Grid>\n </Grid>\n </Content>\n </Page>\n );\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useCallback } from 'react';
3
+ import { Paper, Typography, Box, TextField, Button } from '@material-ui/core';
4
+
5
+ const ShareBox = ({ slug, baseUrl }) => {
6
+ const url = `${baseUrl}/s/${slug}`;
7
+ const copyToClipboard = useCallback(() => {
8
+ navigator.clipboard.writeText(url);
9
+ }, [url]);
10
+ return /* @__PURE__ */ jsxs(Paper, { style: { padding: 16, marginTop: 16 }, children: [
11
+ /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: "Share this chart" }),
12
+ /* @__PURE__ */ jsxs(Box, { display: "flex", marginTop: 1, children: [
13
+ /* @__PURE__ */ jsx(
14
+ TextField,
15
+ {
16
+ fullWidth: true,
17
+ size: "small",
18
+ value: url,
19
+ variant: "outlined",
20
+ InputProps: { readOnly: true },
21
+ onClick: (e) => e.currentTarget.parentElement?.select?.(),
22
+ style: { marginRight: 8 }
23
+ }
24
+ ),
25
+ /* @__PURE__ */ jsx(Button, { variant: "contained", color: "primary", onClick: copyToClipboard, style: { whiteSpace: "nowrap" }, children: "Copy" })
26
+ ] })
27
+ ] });
28
+ };
29
+
30
+ export { ShareBox };
31
+ //# sourceMappingURL=ShareBox.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShareBox.esm.js","sources":["../../../src/components/RadarPage/ShareBox.tsx"],"sourcesContent":["import React, { useCallback } from 'react';\nimport { Box, Button, TextField, Paper, Typography } from '@material-ui/core';\n\nexport interface ShareBoxProps {\n slug: string;\n baseUrl: string;\n}\n\nexport const ShareBox: React.FC<ShareBoxProps> = ({ slug, baseUrl }) => {\n const url = `${baseUrl}/s/${slug}`;\n\n const copyToClipboard = useCallback(() => {\n navigator.clipboard.writeText(url);\n }, [url]);\n\n return (\n <Paper style={{ padding: 16, marginTop: 16 }}>\n <Typography variant=\"subtitle2\" gutterBottom>\n Share this chart\n </Typography>\n <Box display=\"flex\" marginTop={1}>\n <TextField\n fullWidth\n size=\"small\"\n value={url}\n variant=\"outlined\"\n InputProps={{ readOnly: true }}\n onClick={e => (e.currentTarget.parentElement as HTMLInputElement)?.select?.()}\n style={{ marginRight: 8 }}\n />\n <Button variant=\"contained\" color=\"primary\" onClick={copyToClipboard} style={{ whiteSpace: 'nowrap' }}>\n Copy\n </Button>\n </Box>\n </Paper>\n );\n};\n"],"names":[],"mappings":";;;;AAQO,MAAM,QAAA,GAAoC,CAAC,EAAE,IAAA,EAAM,SAAQ,KAAM;AACtE,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,GAAA,EAAM,IAAI,CAAA,CAAA;AAEhC,EAAA,MAAM,eAAA,GAAkB,YAAY,MAAM;AACxC,IAAA,SAAA,CAAU,SAAA,CAAU,UAAU,GAAG,CAAA;AAAA,EACnC,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAER,EAAA,uBACE,IAAA,CAAC,SAAM,KAAA,EAAO,EAAE,SAAS,EAAA,EAAI,SAAA,EAAW,IAAG,EACzC,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,UAAA,EAAA,EAAW,OAAA,EAAQ,WAAA,EAAY,YAAA,EAAY,MAAC,QAAA,EAAA,kBAAA,EAE7C,CAAA;AAAA,oBACA,IAAA,CAAC,GAAA,EAAA,EAAI,OAAA,EAAQ,MAAA,EAAO,WAAW,CAAA,EAC7B,QAAA,EAAA;AAAA,sBAAA,GAAA;AAAA,QAAC,SAAA;AAAA,QAAA;AAAA,UACC,SAAA,EAAS,IAAA;AAAA,UACT,IAAA,EAAK,OAAA;AAAA,UACL,KAAA,EAAO,GAAA;AAAA,UACP,OAAA,EAAQ,UAAA;AAAA,UACR,UAAA,EAAY,EAAE,QAAA,EAAU,IAAA,EAAK;AAAA,UAC7B,OAAA,EAAS,CAAA,CAAA,KAAM,CAAA,CAAE,aAAA,CAAc,eAAoC,MAAA,IAAS;AAAA,UAC5E,KAAA,EAAO,EAAE,WAAA,EAAa,CAAA;AAAE;AAAA,OAC1B;AAAA,sBACA,GAAA,CAAC,MAAA,EAAA,EAAO,OAAA,EAAQ,WAAA,EAAY,KAAA,EAAM,SAAA,EAAU,OAAA,EAAS,eAAA,EAAiB,KAAA,EAAO,EAAE,UAAA,EAAY,QAAA,IAAY,QAAA,EAAA,MAAA,EAEvG;AAAA,KAAA,EACF;AAAA,GAAA,EACF,CAAA;AAEJ;;;;"}
@@ -0,0 +1,6 @@
1
+ import { RadarPage } from './RadarPage.esm.js';
2
+
3
+
4
+
5
+ export { RadarPage };
6
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
@@ -0,0 +1,12 @@
1
+ function getBaseUrl(config) {
2
+ const baseUrl = config.getOptionalString("radarChart.baseUrl");
3
+ if (!baseUrl) {
4
+ throw new Error(
5
+ "radarChart.baseUrl is not configured. Set it in app-config.yaml under `radarChart.baseUrl`."
6
+ );
7
+ }
8
+ return baseUrl.replace(/\/+$/, "");
9
+ }
10
+
11
+ export { getBaseUrl };
12
+ //# sourceMappingURL=config.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.esm.js","sources":["../src/config.ts"],"sourcesContent":["import { ConfigApi } from '@backstage/core-plugin-api';\n\nexport function getBaseUrl(config: ConfigApi): string {\n const baseUrl = config.getOptionalString('radarChart.baseUrl');\n if (!baseUrl) {\n throw new Error(\n 'radarChart.baseUrl is not configured. Set it in app-config.yaml under `radarChart.baseUrl`.',\n );\n }\n return baseUrl.replace(/\\/+$/, '');\n}\n"],"names":[],"mappings":"AAEO,SAAS,WAAW,MAAA,EAA2B;AACpD,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,iBAAA,CAAkB,oBAAoB,CAAA;AAC7D,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACnC;;;;"}
@@ -0,0 +1,57 @@
1
+ import * as _backstage_core_plugin_api from '@backstage/core-plugin-api';
2
+ import React from 'react';
3
+
4
+ declare const radarChartPlugin: _backstage_core_plugin_api.BackstagePlugin<{
5
+ root: _backstage_core_plugin_api.RouteRef<undefined>;
6
+ }, {}, {}>;
7
+ declare const RadarPage$1: (props: any) => JSX.Element | null;
8
+ declare const EntityRadarChartCard$1: unknown;
9
+
10
+ interface ChartConfig {
11
+ title: string;
12
+ author: string;
13
+ deliverableType: string;
14
+ showAuthor: boolean;
15
+ extraKpi: {
16
+ name: string;
17
+ value: number;
18
+ } | null;
19
+ lockedValues: Record<string, number>;
20
+ }
21
+ interface GenerateRequest {
22
+ title?: string;
23
+ author?: string;
24
+ deliverableType?: string;
25
+ kpis: Record<string, number>;
26
+ showAuthor?: boolean;
27
+ }
28
+ interface SavedChart {
29
+ slug: string;
30
+ title: string;
31
+ author: string;
32
+ deliverableType: string;
33
+ showAuthor: boolean;
34
+ lockedValues: Record<string, number>;
35
+ extraKpi: {
36
+ name: string;
37
+ value: number;
38
+ } | null;
39
+ createdAt: string;
40
+ }
41
+
42
+ interface RadarApi {
43
+ generatePng(req: GenerateRequest): Promise<Blob>;
44
+ saveChart(config: ChartConfig): Promise<{
45
+ slug: string;
46
+ url: string;
47
+ }>;
48
+ getChart(slug: string): Promise<SavedChart>;
49
+ }
50
+ declare const radarApiRef: _backstage_core_plugin_api.ApiRef<RadarApi>;
51
+
52
+ declare const RadarPage: React.FC;
53
+
54
+ declare const EntityRadarChartCard: React.FC;
55
+
56
+ export { EntityRadarChartCard$1 as EntityRadarChartCard, EntityRadarChartCard as EntityRadarChartCardComponent, RadarPage$1 as RadarPage, RadarPage as RadarPageComponent, radarChartPlugin as plugin, radarApiRef, radarChartPlugin };
57
+ export type { ChartConfig, GenerateRequest, RadarApi, SavedChart };
@@ -0,0 +1,5 @@
1
+ export { EntityRadarChartCard, RadarPage, radarChartPlugin as plugin, radarChartPlugin } from './plugin.esm.js';
2
+ export { radarApiRef } from './api/RadarApi.esm.js';
3
+ export { RadarPage as RadarPageComponent } from './components/RadarPage/RadarPage.esm.js';
4
+ export { EntityRadarChartCard as EntityRadarChartCardComponent } from './components/EntityRadarChartCard/EntityRadarChartCard.esm.js';
5
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
@@ -0,0 +1,39 @@
1
+ import { createPlugin, createApiFactory, configApiRef, createRoutableExtension } from '@backstage/core-plugin-api';
2
+ import { radarApiRef } from './api/RadarApi.esm.js';
3
+ import { RadarApiClient } from './api/RadarApiClient.esm.js';
4
+ import { rootRouteRef } from './routes.esm.js';
5
+
6
+ const radarChartPlugin = createPlugin({
7
+ id: "radar-chart",
8
+ routes: {
9
+ root: rootRouteRef
10
+ },
11
+ apis: [
12
+ createApiFactory({
13
+ api: radarApiRef,
14
+ deps: { configApi: configApiRef },
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ factory: ({ configApi }) => new RadarApiClient(configApi)
17
+ })
18
+ ]
19
+ });
20
+ const RadarPage = radarChartPlugin.provide(
21
+ createRoutableExtension({
22
+ name: "RadarPage",
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ component: () => import('./components/RadarPage/index.esm.js').then((m) => m.RadarPage),
25
+ mountPoint: rootRouteRef
26
+ })
27
+ );
28
+ const EntityRadarChartCard = radarChartPlugin.provide(
29
+ createRoutableExtension({
30
+ name: "EntityRadarChartCard",
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ component: () => import('./components/EntityRadarChartCard/index.esm.js').then((m) => m.EntityRadarChartCard),
33
+ mountPoint: rootRouteRef
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ })
36
+ );
37
+
38
+ export { EntityRadarChartCard, RadarPage, radarChartPlugin };
39
+ //# sourceMappingURL=plugin.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n createPlugin,\n createRoutableExtension,\n createApiFactory,\n configApiRef,\n} from '@backstage/core-plugin-api';\nimport { radarApiRef } from './api/RadarApi';\nimport { RadarApiClient } from './api/RadarApiClient';\nimport { rootRouteRef } from './routes';\n\nexport const radarChartPlugin = createPlugin({\n id: 'radar-chart',\n routes: {\n root: rootRouteRef,\n },\n apis: [\n createApiFactory({\n api: radarApiRef,\n deps: { configApi: configApiRef },\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n factory: ({ configApi }: any) => new RadarApiClient(configApi),\n }),\n ],\n});\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const RadarPage = radarChartPlugin.provide(\n createRoutableExtension({\n name: 'RadarPage',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n component: () => import('./components/RadarPage').then(m => m.RadarPage) as any,\n mountPoint: rootRouteRef,\n })\n);\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const EntityRadarChartCard = radarChartPlugin.provide(\n createRoutableExtension({\n name: 'EntityRadarChartCard',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n component: () => import('./components/EntityRadarChartCard').then(m => m.EntityRadarChartCard) as any,\n mountPoint: rootRouteRef,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n }) as any\n);\n"],"names":[],"mappings":";;;;;AAUO,MAAM,mBAAmB,YAAA,CAAa;AAAA,EAC3C,EAAA,EAAI,aAAA;AAAA,EACJ,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA,GACR;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,gBAAA,CAAiB;AAAA,MACf,GAAA,EAAK,WAAA;AAAA,MACL,IAAA,EAAM,EAAE,SAAA,EAAW,YAAA,EAAa;AAAA;AAAA,MAEhC,SAAS,CAAC,EAAE,WAAU,KAAW,IAAI,eAAe,SAAS;AAAA,KAC9D;AAAA;AAEL,CAAC;AAGM,MAAM,YAAY,gBAAA,CAAiB,OAAA;AAAA,EACxC,uBAAA,CAAwB;AAAA,IACtB,IAAA,EAAM,WAAA;AAAA;AAAA,IAEN,SAAA,EAAW,MAAM,OAAO,qCAAwB,EAAE,IAAA,CAAK,CAAA,CAAA,KAAK,EAAE,SAAS,CAAA;AAAA,IACvE,UAAA,EAAY;AAAA,GACb;AACH;AAGO,MAAM,uBAAuB,gBAAA,CAAiB,OAAA;AAAA,EACnD,uBAAA,CAAwB;AAAA,IACtB,IAAA,EAAM,sBAAA;AAAA;AAAA,IAEN,SAAA,EAAW,MAAM,OAAO,gDAAmC,EAAE,IAAA,CAAK,CAAA,CAAA,KAAK,EAAE,oBAAoB,CAAA;AAAA,IAC7F,UAAA,EAAY;AAAA;AAAA,GAEb;AACH;;;;"}
@@ -0,0 +1,8 @@
1
+ import { createRouteRef } from '@backstage/core-plugin-api';
2
+
3
+ const rootRouteRef = createRouteRef({
4
+ id: "radar-chart"
5
+ });
6
+
7
+ export { rootRouteRef };
8
+ //# sourceMappingURL=routes.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.esm.js","sources":["../src/routes.ts"],"sourcesContent":["import { createRouteRef } from '@backstage/core-plugin-api';\n\nexport const rootRouteRef = createRouteRef({\n id: 'radar-chart',\n});\n"],"names":[],"mappings":";;AAEO,MAAM,eAAe,cAAA,CAAe;AAAA,EACzC,EAAA,EAAI;AACN,CAAC;;;;"}
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@ehicoso/plugin-radar-chart",
3
+ "version": "0.1.0",
4
+ "description": "Backstage frontend plugin — generate KPI radar charts from any Backstage instance via a remote chart-rendering service.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/acarmisc/stupid-radar-chart-backstage-plugin.git"
8
+ },
9
+ "homepage": "https://github.com/acarmisc/stupid-radar-chart-backstage-plugin#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/acarmisc/stupid-radar-chart-backstage-plugin/issues"
12
+ },
13
+ "main": "dist/index.cjs.js",
14
+ "module": "dist/index.esm.js",
15
+ "types": "dist/index.d.ts",
16
+ "files": [
17
+ "dist/**/*",
18
+ "config.d.ts",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "main": "dist/index.cjs.js",
25
+ "module": "dist/index.esm.js",
26
+ "types": "dist/index.d.ts"
27
+ },
28
+ "backstage": {
29
+ "role": "frontend-plugin",
30
+ "pluginId": "radar-chart",
31
+ "pluginPackages": [
32
+ "@ehicoso/plugin-radar-chart"
33
+ ]
34
+ },
35
+ "configSchema": "config.d.ts",
36
+ "scripts": {
37
+ "start": "backstage-cli package start",
38
+ "tsc": "tsc",
39
+ "prebuild": "tsc",
40
+ "build": "backstage-cli package build",
41
+ "lint": "backstage-cli package lint",
42
+ "test": "backstage-cli package test",
43
+ "clean": "backstage-cli package clean",
44
+ "prepack": "backstage-cli package prepack",
45
+ "postpack": "backstage-cli package postpack"
46
+ },
47
+ "dependencies": {
48
+ "@backstage/core-components": "^0.15.0",
49
+ "@backstage/core-plugin-api": "^1.10.0",
50
+ "@backstage/errors": "^1.2.0",
51
+ "@backstage/frontend-plugin-api": "^0.9.0",
52
+ "@backstage/plugin-catalog-react": "^1.14.0",
53
+ "@material-ui/core": "^4.12.0",
54
+ "@material-ui/icons": "^4.11.0",
55
+ "chart.js": "^4.4.0",
56
+ "react-chartjs-2": "^5.2.0",
57
+ "react-use": "^17.5.0"
58
+ },
59
+ "peerDependencies": {
60
+ "react": "^17 || ^18",
61
+ "react-dom": "^17 || ^18"
62
+ },
63
+ "devDependencies": {
64
+ "@backstage/cli": "^0.28.0",
65
+ "@backstage/core-app-api": "^1.15.0",
66
+ "@backstage/dev-utils": "^1.1.0",
67
+ "@backstage/test-utils": "^1.7.0",
68
+ "@testing-library/jest-dom": "^6.0.0",
69
+ "@testing-library/react": "^14.0.0",
70
+ "@types/react": "^18.2.0",
71
+ "@typescript-eslint/eslint-plugin": "^8.60.0",
72
+ "@typescript-eslint/parser": "^8.60.0",
73
+ "eslint": "^10.4.0",
74
+ "eslint-plugin-react": "^7.37.5",
75
+ "react": "^18.2.0",
76
+ "react-dom": "^18.2.0",
77
+ "react-router-dom": "^6.30.3"
78
+ }
79
+ }