@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.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/config.d.ts +10 -0
- package/dist/api/RadarApi.esm.js +8 -0
- package/dist/api/RadarApi.esm.js.map +1 -0
- package/dist/api/RadarApiClient.esm.js +74 -0
- package/dist/api/RadarApiClient.esm.js.map +1 -0
- package/dist/components/EntityRadarChartCard/EntityRadarChartCard.esm.js +93 -0
- package/dist/components/EntityRadarChartCard/EntityRadarChartCard.esm.js.map +1 -0
- package/dist/components/EntityRadarChartCard/annotations.esm.js +59 -0
- package/dist/components/EntityRadarChartCard/annotations.esm.js.map +1 -0
- package/dist/components/EntityRadarChartCard/index.esm.js +6 -0
- package/dist/components/EntityRadarChartCard/index.esm.js.map +1 -0
- package/dist/components/RadarPage/ChartPreview.esm.js +81 -0
- package/dist/components/RadarPage/ChartPreview.esm.js.map +1 -0
- package/dist/components/RadarPage/ConfigForm.esm.js +167 -0
- package/dist/components/RadarPage/ConfigForm.esm.js.map +1 -0
- package/dist/components/RadarPage/RadarPage.esm.js +108 -0
- package/dist/components/RadarPage/RadarPage.esm.js.map +1 -0
- package/dist/components/RadarPage/ShareBox.esm.js +31 -0
- package/dist/components/RadarPage/ShareBox.esm.js.map +1 -0
- package/dist/components/RadarPage/index.esm.js +6 -0
- package/dist/components/RadarPage/index.esm.js.map +1 -0
- package/dist/config.esm.js +12 -0
- package/dist/config.esm.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.esm.js +5 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/plugin.esm.js +39 -0
- package/dist/plugin.esm.js.map +1 -0
- package/dist/routes.esm.js +8 -0
- package/dist/routes.esm.js.map +1 -0
- 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 @@
|
|
|
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":";;;AAMO,MAAM,cAAA,CAAmC;AAAA,EACtC,OAAA;AAAA,EAER,YAAY,MAAA,EAAmB;AAC7B,IAAA,IAAA,CAAK,OAAA,GAAU,WAAW,MAAM,CAAA;AAAA,EAClC;AAAA,EAEA,MAAM,YAAY,GAAA,EAAqC;AACrD,IAAA,MAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,eAAA,EAAiB,IAAA,EAAM,YAAW,GAAI,GAAA;AAC7D,IAAA,MAAM,WAAW,MAAM,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,OAAO,CAAA,mBAAA,CAAA,EAAuB;AAAA,MACjE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACnB,KAAA;AAAA,QACA,MAAA;AAAA,QACA,gBAAA,EAAkB,eAAA;AAAA,QAClB,IAAA;AAAA,QACA,WAAA,EAAa;AAAA,OACd;AAAA,KACF,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,MAAM,aAAA,CAAc,YAAA,CAAa,QAAQ,CAAA;AAAA,IACjD;AAEA,IAAA,OAAO,SAAS,IAAA,EAAK;AAAA,EACvB;AAAA,EAEA,MAAM,UAAU,MAAA,EAA6D;AAC3E,IAAA,MAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,iBAAiB,UAAA,EAAY,YAAA,EAAc,UAAS,GAAI,MAAA;AAE/E,IAAA,MAAM,WAAW,MAAM,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,OAAO,CAAA,uBAAA,CAAA,EAA2B;AAAA,MACrE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACnB,KAAA;AAAA,QACA,MAAA;AAAA,QACA,gBAAA,EAAkB,eAAA;AAAA,QAClB,WAAA,EAAa,UAAA;AAAA,QACb,aAAA,EAAe,YAAA;AAAA,QACf,SAAA,EAAW;AAAA,OACZ;AAAA,KACF,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,MAAM,aAAA,CAAc,YAAA,CAAa,QAAQ,CAAA;AAAA,IACjD;AAIA,IAAA,MAAM,SAAS,IAAA,EAAK;AACpB,IAAA,MAAM,IAAA,GAAO,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,cAAc,CAAA;AAEhD,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,IAAI,MAAM,oCAAoC,CAAA;AAAA,IACtD;AAEA,IAAA,OAAO;AAAA,MACL,IAAA;AAAA,MACA,GAAA,EAAK,CAAA,EAAG,IAAA,CAAK,OAAO,MAAM,IAAI,CAAA;AAAA,KAChC;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,IAAA,EAAmC;AAChD,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,CAAA,EAAG,KAAK,OAAO,CAAA,YAAA,EAAe,IAAI,CAAA,CAAE,CAAA;AAEjE,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,MAAM,aAAA,CAAc,YAAA,CAAa,QAAQ,CAAA;AAAA,IACjD;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAEjC,IAAA,OAAO;AAAA,MACL,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,iBAAiB,IAAA,CAAK,eAAA;AAAA,MACtB,YAAY,IAAA,CAAK,UAAA;AAAA,MACjB,cAAc,IAAA,CAAK,YAAA;AAAA,MACnB,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,WAAW,IAAA,CAAK;AAAA,KAClB;AAAA,EACF;AACF;;;;"}
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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;;;;"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|