@karimov-labs/backstage-plugin-devxp 1.0.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/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # @karimov-labs/backstage-plugin-devxp
2
+
3
+ A Backstage frontend plugin that provides a **Developer Intelligence** dashboard for managing masked developer identities. It lets platform teams upload a list of real developer names, store SHA-256–based pseudonyms, and look up or verify masked identities — all without exposing real names to third-party analytics tools.
4
+
5
+ This plugin is the UI counterpart to [`@karimov-labs/backstage-plugin-devxp-backend`](https://www.npmjs.com/package/@karimov-labs/backstage-plugin-devxp-backend), which must be installed alongside it.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Dashboard tab** — shows configuration health (salt, API token, endpoint, project ID) and total mapping count, plus an inline unmask tester
12
+ - **Settings tab** — upload a plain-text CSV of real developer names to generate and store masked hashes; browse and delete individual mappings
13
+ - Hashing algorithm matches the [dev-xp-analyzer](https://github.com/karimov-labs/dev-xp-analyzer) tool: `SHA-256(salt + realName)`, first 16 hex chars
14
+
15
+ ---
16
+
17
+ ## Requirements
18
+
19
+ | Dependency | Version |
20
+ |---|---|
21
+ | Backstage | >= 1.30 |
22
+ | `@karimov-labs/backstage-plugin-devxp-backend` | `^1.0.0` |
23
+ | React | `^18` |
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ### 1. Install the packages
30
+
31
+ ```bash
32
+ # yarn (Backstage default)
33
+ yarn workspace app add @karimov-labs/backstage-plugin-devxp
34
+ yarn workspace backend add @karimov-labs/backstage-plugin-devxp-backend
35
+
36
+ # npm
37
+ npm install @karimov-labs/backstage-plugin-devxp
38
+ npm install @karimov-labs/backstage-plugin-devxp-backend
39
+ ```
40
+
41
+ ---
42
+
43
+ ### 2. Add the frontend plugin to your app
44
+
45
+ Edit `packages/app/src/App.tsx`:
46
+
47
+ ```tsx
48
+ import devxpPlugin from '@karimov-labs/backstage-plugin-devxp';
49
+
50
+ export default createApp({
51
+ features: [
52
+ // ... other plugins
53
+ devxpPlugin,
54
+ ],
55
+ });
56
+ ```
57
+
58
+ ---
59
+
60
+ ### 3. Add a sidebar navigation item
61
+
62
+ In your sidebar component (e.g. `packages/app/src/modules/nav/Sidebar.tsx`), add:
63
+
64
+ ```tsx
65
+ import AssessmentIcon from '@material-ui/icons/Assessment';
66
+
67
+ // inside your sidebar menu group:
68
+ <SidebarItem icon={AssessmentIcon} to="/devxp" text="Dev Intelligence" />
69
+ ```
70
+
71
+ ---
72
+
73
+ ### 4. Register the backend plugin
74
+
75
+ Edit `packages/backend/src/index.ts`:
76
+
77
+ ```ts
78
+ backend.add(import('@karimov-labs/backstage-plugin-devxp-backend'));
79
+ ```
80
+
81
+ ---
82
+
83
+ ### 5. Configure app-config.yaml
84
+
85
+ ```yaml
86
+ devxp:
87
+ # Salt used for SHA-256 hashing — must match the salt used in dev-xp-analyzer
88
+ salt: ${DEVXP_SALT}
89
+
90
+ # Whether developer names are masked in the analytics tool
91
+ masked: true
92
+
93
+ # dev-xp-analyzer API credentials (optional — only needed if calling the analyzer API)
94
+ apiToken: ${DEVXP_API_TOKEN}
95
+ apiEndpoint: ${DEVXP_API_ENDPOINT}
96
+ projectId: ${DEVXP_PROJECT_ID}
97
+ ```
98
+
99
+ Set the corresponding environment variables before starting Backstage:
100
+
101
+ ```bash
102
+ export DEVXP_SALT="your-secret-salt"
103
+ export DEVXP_API_TOKEN="your-api-token" # optional
104
+ export DEVXP_API_ENDPOINT="https://..." # optional
105
+ export DEVXP_PROJECT_ID="your-project-id" # optional
106
+ ```
107
+
108
+ > **Security note:** The salt and API token are consumed exclusively by the backend and are never sent to the browser.
109
+
110
+ ---
111
+
112
+ ## Usage
113
+
114
+ Navigate to `/devxp` in your Backstage instance.
115
+
116
+ ### Dashboard tab
117
+
118
+ | Section | Description |
119
+ |---|---|
120
+ | Configuration Status | Green/red indicators for salt, API token, endpoint, and project ID |
121
+ | Developer Mappings | Count of stored masked ↔ real name pairs |
122
+ | Unmask Tester | Enter a 16-character hex masked name and press Enter to look up the real name |
123
+
124
+ ### Settings tab
125
+
126
+ | Section | Description |
127
+ |---|---|
128
+ | CSV Upload | Upload a plain-text file with one real developer name per line; the plugin hashes each and stores the mapping |
129
+ | Mappings Table | Lists all stored pairs (masked hash → real name) with per-row delete buttons |
130
+
131
+ ---
132
+
133
+ ## API
134
+
135
+ The frontend communicates with the backend via the Backstage discovery API at `/api/devxp/`. See the [`@karimov-labs/backstage-plugin-devxp-backend` README](https://www.npmjs.com/package/@karimov-labs/backstage-plugin-devxp-backend) for full endpoint documentation.
136
+
137
+ ---
138
+
139
+ ## License
140
+
141
+ Apache-2.0
@@ -0,0 +1,72 @@
1
+ import { createApiRef } from '@backstage/core-plugin-api';
2
+
3
+ createApiRef({
4
+ id: "plugin.devxp.api"
5
+ });
6
+ class DevxpClient {
7
+ fetchApi;
8
+ discoveryApi;
9
+ constructor(options) {
10
+ this.fetchApi = options.fetchApi;
11
+ this.discoveryApi = options.discoveryApi;
12
+ }
13
+ async baseUrl() {
14
+ return this.discoveryApi.getBaseUrl("devxp");
15
+ }
16
+ async getConfig() {
17
+ const url = `${await this.baseUrl()}/config`;
18
+ const response = await this.fetchApi.fetch(url);
19
+ if (!response.ok) throw new Error(`Failed to fetch config: ${response.statusText}`);
20
+ return response.json();
21
+ }
22
+ async getMappings() {
23
+ const url = `${await this.baseUrl()}/mappings`;
24
+ const response = await this.fetchApi.fetch(url);
25
+ if (!response.ok) throw new Error(`Failed to fetch mappings: ${response.statusText}`);
26
+ return response.json();
27
+ }
28
+ async uploadCsv(csvContent) {
29
+ const url = `${await this.baseUrl()}/mappings/upload`;
30
+ const response = await this.fetchApi.fetch(url, {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify({ csvContent })
34
+ });
35
+ const data = await response.json();
36
+ if (!response.ok) return { message: data.error || "Upload failed", count: 0, error: data.error };
37
+ return data;
38
+ }
39
+ async unmask(maskedName) {
40
+ const url = `${await this.baseUrl()}/unmask`;
41
+ const response = await this.fetchApi.fetch(url, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify({ maskedName })
45
+ });
46
+ if (!response.ok) throw new Error(`Failed to unmask: ${response.statusText}`);
47
+ return response.json();
48
+ }
49
+ async hash(realName) {
50
+ const url = `${await this.baseUrl()}/hash`;
51
+ const response = await this.fetchApi.fetch(url, {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({ realName })
55
+ });
56
+ if (!response.ok) throw new Error(`Failed to hash: ${response.statusText}`);
57
+ return response.json();
58
+ }
59
+ async deleteMapping(maskedName) {
60
+ const url = `${await this.baseUrl()}/mappings/delete`;
61
+ const response = await this.fetchApi.fetch(url, {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify({ maskedName })
65
+ });
66
+ if (!response.ok) throw new Error(`Failed to delete mapping: ${response.statusText}`);
67
+ return response.json();
68
+ }
69
+ }
70
+
71
+ export { DevxpClient };
72
+ //# sourceMappingURL=api.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.esm.js","sources":["../src/api.ts"],"sourcesContent":["import { createApiRef } from '@backstage/core-plugin-api';\nimport type {\n DevxpConfig,\n DeveloperMapping,\n UnmaskResult,\n HashResult,\n UploadResult,\n} from './types';\n\nexport interface DevxpApi {\n getConfig(): Promise<DevxpConfig>;\n getMappings(): Promise<{ mappings: DeveloperMapping[] }>;\n uploadCsv(csvContent: string): Promise<UploadResult>;\n unmask(maskedName: string): Promise<UnmaskResult>;\n hash(realName: string): Promise<HashResult>;\n deleteMapping(maskedName: string): Promise<{ message: string }>;\n}\n\nexport const devxpApiRef = createApiRef<DevxpApi>({\n id: 'plugin.devxp.api',\n});\n\nexport class DevxpClient implements DevxpApi {\n private readonly fetchApi: { fetch: typeof fetch };\n private readonly discoveryApi: { getBaseUrl: (pluginId: string) => Promise<string> };\n\n constructor(options: {\n fetchApi: { fetch: typeof fetch };\n discoveryApi: { getBaseUrl: (pluginId: string) => Promise<string> };\n }) {\n this.fetchApi = options.fetchApi;\n this.discoveryApi = options.discoveryApi;\n }\n\n private async baseUrl(): Promise<string> {\n return this.discoveryApi.getBaseUrl('devxp');\n }\n\n async getConfig(): Promise<DevxpConfig> {\n const url = `${await this.baseUrl()}/config`;\n const response = await this.fetchApi.fetch(url);\n if (!response.ok) throw new Error(`Failed to fetch config: ${response.statusText}`);\n return response.json();\n }\n\n async getMappings(): Promise<{ mappings: DeveloperMapping[] }> {\n const url = `${await this.baseUrl()}/mappings`;\n const response = await this.fetchApi.fetch(url);\n if (!response.ok) throw new Error(`Failed to fetch mappings: ${response.statusText}`);\n return response.json();\n }\n\n async uploadCsv(csvContent: string): Promise<UploadResult> {\n const url = `${await this.baseUrl()}/mappings/upload`;\n const response = await this.fetchApi.fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ csvContent }),\n });\n const data = await response.json();\n if (!response.ok) return { message: data.error || 'Upload failed', count: 0, error: data.error };\n return data;\n }\n\n async unmask(maskedName: string): Promise<UnmaskResult> {\n const url = `${await this.baseUrl()}/unmask`;\n const response = await this.fetchApi.fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ maskedName }),\n });\n if (!response.ok) throw new Error(`Failed to unmask: ${response.statusText}`);\n return response.json();\n }\n\n async hash(realName: string): Promise<HashResult> {\n const url = `${await this.baseUrl()}/hash`;\n const response = await this.fetchApi.fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ realName }),\n });\n if (!response.ok) throw new Error(`Failed to hash: ${response.statusText}`);\n return response.json();\n }\n\n async deleteMapping(maskedName: string): Promise<{ message: string }> {\n const url = `${await this.baseUrl()}/mappings/delete`;\n const response = await this.fetchApi.fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ maskedName }),\n });\n if (!response.ok) throw new Error(`Failed to delete mapping: ${response.statusText}`);\n return response.json();\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,158 @@
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+ import { Typography, Grid, Box, TextField, Button, Chip } from '@material-ui/core';
3
+ import { makeStyles } from '@material-ui/core/styles';
4
+ import CheckCircleIcon from '@material-ui/icons/CheckCircle';
5
+ import ErrorIcon from '@material-ui/icons/Error';
6
+ import { InfoCard } from '@backstage/core-components';
7
+
8
+ const useStyles = makeStyles((theme) => ({
9
+ configGrid: {
10
+ marginBottom: theme.spacing(3)
11
+ },
12
+ statusChip: {
13
+ marginLeft: theme.spacing(1)
14
+ },
15
+ unmaskResult: {
16
+ marginTop: theme.spacing(2),
17
+ padding: theme.spacing(2),
18
+ backgroundColor: theme.palette.background.default,
19
+ borderRadius: theme.shape.borderRadius
20
+ },
21
+ resultLabel: {
22
+ color: theme.palette.text.secondary,
23
+ marginBottom: theme.spacing(0.5)
24
+ },
25
+ resultValue: {
26
+ fontFamily: "monospace",
27
+ fontSize: "1.1em"
28
+ }
29
+ }));
30
+ const DashboardContent = ({ api }) => {
31
+ const classes = useStyles();
32
+ const [config, setConfig] = useState(null);
33
+ const [loading, setLoading] = useState(true);
34
+ const [maskedInput, setMaskedInput] = useState("");
35
+ const [unmaskResult, setUnmaskResult] = useState(null);
36
+ const [unmaskError, setUnmaskError] = useState("");
37
+ const loadConfig = useCallback(async () => {
38
+ try {
39
+ setLoading(true);
40
+ const cfg = await api.getConfig();
41
+ setConfig(cfg);
42
+ } catch (e) {
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, [api]);
47
+ useEffect(() => {
48
+ loadConfig();
49
+ }, [loadConfig]);
50
+ const handleUnmask = async () => {
51
+ if (!maskedInput.trim()) return;
52
+ setUnmaskError("");
53
+ setUnmaskResult(null);
54
+ try {
55
+ const result = await api.unmask(maskedInput.trim());
56
+ setUnmaskResult(result);
57
+ } catch (e) {
58
+ setUnmaskError(e.message || "Failed to unmask");
59
+ }
60
+ };
61
+ const handleKeyDown = (e) => {
62
+ if (e.key === "Enter") handleUnmask();
63
+ };
64
+ if (loading) {
65
+ return /* @__PURE__ */ React.createElement(Typography, null, "Loading configuration...");
66
+ }
67
+ return /* @__PURE__ */ React.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Configuration Status" }, config ? /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(
68
+ ConfigItem,
69
+ {
70
+ label: "Mode",
71
+ value: config.masked ? "Masked" : "Unmasked",
72
+ ok: true
73
+ }
74
+ ), /* @__PURE__ */ React.createElement(
75
+ ConfigItem,
76
+ {
77
+ label: "Salt",
78
+ value: config.saltConfigured ? "Configured" : "Not configured",
79
+ ok: config.saltConfigured
80
+ }
81
+ ), /* @__PURE__ */ React.createElement(
82
+ ConfigItem,
83
+ {
84
+ label: "API Endpoint",
85
+ value: config.apiEndpointConfigured ? "Configured" : "Not configured",
86
+ ok: config.apiEndpointConfigured
87
+ }
88
+ ), /* @__PURE__ */ React.createElement(
89
+ ConfigItem,
90
+ {
91
+ label: "API Token",
92
+ value: config.apiTokenConfigured ? "Configured" : "Not configured",
93
+ ok: config.apiTokenConfigured
94
+ }
95
+ ), /* @__PURE__ */ React.createElement(
96
+ ConfigItem,
97
+ {
98
+ label: "Project ID",
99
+ value: config.projectIdConfigured ? "Configured" : "Not configured",
100
+ ok: config.projectIdConfigured
101
+ }
102
+ )) : /* @__PURE__ */ React.createElement(Typography, { color: "error" }, "Unable to load configuration. Is the backend plugin installed?"))), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Developer Mappings" }, /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center" }, /* @__PURE__ */ React.createElement(Typography, { variant: "h3" }, config?.mappingCount ?? 0), /* @__PURE__ */ React.createElement(
103
+ Typography,
104
+ {
105
+ variant: "body1",
106
+ style: { marginLeft: 8 },
107
+ color: "textSecondary"
108
+ },
109
+ "developer name mappings stored"
110
+ )), /* @__PURE__ */ React.createElement(Box, { mt: 2 }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "Upload developer names in the Settings tab to populate the mapping database. The system will compute SHA-256 hashes using the configured salt value.")))), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Unmask Developer Name" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary", paragraph: true }, "Enter a masked developer username (16-character hex hash) to look up the original name from the mapping database."), /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "flex-start" }, /* @__PURE__ */ React.createElement(
111
+ TextField,
112
+ {
113
+ label: "Masked username",
114
+ value: maskedInput,
115
+ onChange: (e) => setMaskedInput(e.target.value),
116
+ onKeyDown: handleKeyDown,
117
+ variant: "outlined",
118
+ size: "small",
119
+ placeholder: "e.g. a1b2c3d4e5f67890",
120
+ style: { minWidth: 300 },
121
+ inputProps: { maxLength: 16 }
122
+ }
123
+ ), /* @__PURE__ */ React.createElement(
124
+ Button,
125
+ {
126
+ variant: "contained",
127
+ color: "primary",
128
+ onClick: handleUnmask,
129
+ disabled: !maskedInput.trim(),
130
+ style: { marginLeft: 12, height: 40 }
131
+ },
132
+ "Unmask"
133
+ )), unmaskResult && /* @__PURE__ */ React.createElement(Box, { className: classes.unmaskResult }, /* @__PURE__ */ React.createElement(Typography, { className: classes.resultLabel, variant: "body2" }, "Masked:"), /* @__PURE__ */ React.createElement(Typography, { className: classes.resultValue }, unmaskResult.maskedName), /* @__PURE__ */ React.createElement(Box, { mt: 1 }, /* @__PURE__ */ React.createElement(Typography, { className: classes.resultLabel, variant: "body2" }, "Real Name:"), /* @__PURE__ */ React.createElement(Typography, { className: classes.resultValue }, unmaskResult.realName ? /* @__PURE__ */ React.createElement(
134
+ Chip,
135
+ {
136
+ label: unmaskResult.realName,
137
+ color: "primary",
138
+ variant: "outlined"
139
+ }
140
+ ) : /* @__PURE__ */ React.createElement(
141
+ Chip,
142
+ {
143
+ label: "No mapping found",
144
+ color: "default",
145
+ variant: "outlined"
146
+ }
147
+ )))), unmaskError && /* @__PURE__ */ React.createElement(Box, { mt: 2 }, /* @__PURE__ */ React.createElement(Typography, { color: "error" }, unmaskError)))));
148
+ };
149
+ function ConfigItem({
150
+ label,
151
+ value,
152
+ ok
153
+ }) {
154
+ return /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", mb: 1 }, ok ? /* @__PURE__ */ React.createElement(CheckCircleIcon, { style: { color: "#4caf50", marginRight: 8 }, fontSize: "small" }) : /* @__PURE__ */ React.createElement(ErrorIcon, { style: { color: "#f44336", marginRight: 8 }, fontSize: "small" }), /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, /* @__PURE__ */ React.createElement("strong", null, label, ":"), " ", value));
155
+ }
156
+
157
+ export { DashboardContent };
158
+ //# sourceMappingURL=DashboardContent.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DashboardContent.esm.js","sources":["../../src/components/DashboardContent.tsx"],"sourcesContent":["import React, { useState, useEffect, useCallback } from 'react';\nimport {\n Typography,\n TextField,\n Button,\n Grid,\n Box,\n Chip,\n} from '@material-ui/core';\nimport { makeStyles } from '@material-ui/core/styles';\nimport CheckCircleIcon from '@material-ui/icons/CheckCircle';\nimport ErrorIcon from '@material-ui/icons/Error';\nimport { InfoCard } from '@backstage/core-components';\nimport type { DevxpApi } from '../api';\nimport type { DevxpConfig, UnmaskResult } from '../types';\n\nconst useStyles = makeStyles(theme => ({\n configGrid: {\n marginBottom: theme.spacing(3),\n },\n statusChip: {\n marginLeft: theme.spacing(1),\n },\n unmaskResult: {\n marginTop: theme.spacing(2),\n padding: theme.spacing(2),\n backgroundColor: theme.palette.background.default,\n borderRadius: theme.shape.borderRadius,\n },\n resultLabel: {\n color: theme.palette.text.secondary,\n marginBottom: theme.spacing(0.5),\n },\n resultValue: {\n fontFamily: 'monospace',\n fontSize: '1.1em',\n },\n}));\n\ninterface DashboardContentProps {\n api: DevxpApi;\n}\n\nexport const DashboardContent = ({ api }: DashboardContentProps) => {\n const classes = useStyles();\n const [config, setConfig] = useState<DevxpConfig | null>(null);\n const [loading, setLoading] = useState(true);\n const [maskedInput, setMaskedInput] = useState('');\n const [unmaskResult, setUnmaskResult] = useState<UnmaskResult | null>(null);\n const [unmaskError, setUnmaskError] = useState('');\n\n const loadConfig = useCallback(async () => {\n try {\n setLoading(true);\n const cfg = await api.getConfig();\n setConfig(cfg);\n } catch (e) {\n // Config not available\n } finally {\n setLoading(false);\n }\n }, [api]);\n\n useEffect(() => {\n loadConfig();\n }, [loadConfig]);\n\n const handleUnmask = async () => {\n if (!maskedInput.trim()) return;\n setUnmaskError('');\n setUnmaskResult(null);\n try {\n const result = await api.unmask(maskedInput.trim());\n setUnmaskResult(result);\n } catch (e: any) {\n setUnmaskError(e.message || 'Failed to unmask');\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') handleUnmask();\n };\n\n if (loading) {\n return <Typography>Loading configuration...</Typography>;\n }\n\n return (\n <Grid container spacing={3}>\n {/* Configuration Status */}\n <Grid item xs={12} md={6}>\n <InfoCard title=\"Configuration Status\">\n {config ? (\n <Box>\n <ConfigItem\n label=\"Mode\"\n value={config.masked ? 'Masked' : 'Unmasked'}\n ok\n />\n <ConfigItem\n label=\"Salt\"\n value={config.saltConfigured ? 'Configured' : 'Not configured'}\n ok={config.saltConfigured}\n />\n <ConfigItem\n label=\"API Endpoint\"\n value={config.apiEndpointConfigured ? 'Configured' : 'Not configured'}\n ok={config.apiEndpointConfigured}\n />\n <ConfigItem\n label=\"API Token\"\n value={config.apiTokenConfigured ? 'Configured' : 'Not configured'}\n ok={config.apiTokenConfigured}\n />\n <ConfigItem\n label=\"Project ID\"\n value={config.projectIdConfigured ? 'Configured' : 'Not configured'}\n ok={config.projectIdConfigured}\n />\n </Box>\n ) : (\n <Typography color=\"error\">\n Unable to load configuration. Is the backend plugin installed?\n </Typography>\n )}\n </InfoCard>\n </Grid>\n\n {/* Statistics */}\n <Grid item xs={12} md={6}>\n <InfoCard title=\"Developer Mappings\">\n <Box display=\"flex\" alignItems=\"center\">\n <Typography variant=\"h3\">\n {config?.mappingCount ?? 0}\n </Typography>\n <Typography\n variant=\"body1\"\n style={{ marginLeft: 8 }}\n color=\"textSecondary\"\n >\n developer name mappings stored\n </Typography>\n </Box>\n <Box mt={2}>\n <Typography variant=\"body2\" color=\"textSecondary\">\n Upload developer names in the Settings tab to populate the mapping\n database. The system will compute SHA-256 hashes using the\n configured salt value.\n </Typography>\n </Box>\n </InfoCard>\n </Grid>\n\n {/* Unmask Tester */}\n <Grid item xs={12}>\n <InfoCard title=\"Unmask Developer Name\">\n <Typography variant=\"body2\" color=\"textSecondary\" paragraph>\n Enter a masked developer username (16-character hex hash) to look up\n the original name from the mapping database.\n </Typography>\n <Box display=\"flex\" alignItems=\"flex-start\">\n <TextField\n label=\"Masked username\"\n value={maskedInput}\n onChange={e => setMaskedInput(e.target.value)}\n onKeyDown={handleKeyDown}\n variant=\"outlined\"\n size=\"small\"\n placeholder=\"e.g. a1b2c3d4e5f67890\"\n style={{ minWidth: 300 }}\n inputProps={{ maxLength: 16 }}\n />\n <Button\n variant=\"contained\"\n color=\"primary\"\n onClick={handleUnmask}\n disabled={!maskedInput.trim()}\n style={{ marginLeft: 12, height: 40 }}\n >\n Unmask\n </Button>\n </Box>\n {unmaskResult && (\n <Box className={classes.unmaskResult}>\n <Typography className={classes.resultLabel} variant=\"body2\">\n Masked:\n </Typography>\n <Typography className={classes.resultValue}>\n {unmaskResult.maskedName}\n </Typography>\n <Box mt={1}>\n <Typography className={classes.resultLabel} variant=\"body2\">\n Real Name:\n </Typography>\n <Typography className={classes.resultValue}>\n {unmaskResult.realName ? (\n <Chip\n label={unmaskResult.realName}\n color=\"primary\"\n variant=\"outlined\"\n />\n ) : (\n <Chip\n label=\"No mapping found\"\n color=\"default\"\n variant=\"outlined\"\n />\n )}\n </Typography>\n </Box>\n </Box>\n )}\n {unmaskError && (\n <Box mt={2}>\n <Typography color=\"error\">{unmaskError}</Typography>\n </Box>\n )}\n </InfoCard>\n </Grid>\n </Grid>\n );\n};\n\nfunction ConfigItem({\n label,\n value,\n ok,\n}: {\n label: string;\n value: string;\n ok: boolean;\n}) {\n return (\n <Box display=\"flex\" alignItems=\"center\" mb={1}>\n {ok ? (\n <CheckCircleIcon style={{ color: '#4caf50', marginRight: 8 }} fontSize=\"small\" />\n ) : (\n <ErrorIcon style={{ color: '#f44336', marginRight: 8 }} fontSize=\"small\" />\n )}\n <Typography variant=\"body2\">\n <strong>{label}:</strong> {value}\n </Typography>\n </Box>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { Page, Header, Content } from '@backstage/core-components';
3
+ import { useApi, fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
4
+ import { Tabs, Tab, Box } from '@material-ui/core';
5
+ import { DevxpClient } from '../api.esm.js';
6
+ import { DashboardContent } from './DashboardContent.esm.js';
7
+ import { SettingsContent } from './SettingsContent.esm.js';
8
+
9
+ const DevxpPage = () => {
10
+ const [tab, setTab] = useState(0);
11
+ const fetchApi = useApi(fetchApiRef);
12
+ const discoveryApi = useApi(discoveryApiRef);
13
+ const api = useMemo(
14
+ () => new DevxpClient({ fetchApi, discoveryApi }),
15
+ [fetchApi, discoveryApi]
16
+ );
17
+ return /* @__PURE__ */ React.createElement(Page, { themeId: "tool" }, /* @__PURE__ */ React.createElement(
18
+ Header,
19
+ {
20
+ title: "Developer Intelligence",
21
+ subtitle: "DevXP Analytics & Developer Name Mapping"
22
+ }
23
+ ), /* @__PURE__ */ React.createElement(Content, null, /* @__PURE__ */ React.createElement(
24
+ Tabs,
25
+ {
26
+ value: tab,
27
+ onChange: (_, v) => setTab(v),
28
+ indicatorColor: "primary",
29
+ textColor: "primary"
30
+ },
31
+ /* @__PURE__ */ React.createElement(Tab, { label: "Dashboard" }),
32
+ /* @__PURE__ */ React.createElement(Tab, { label: "Settings" })
33
+ ), /* @__PURE__ */ React.createElement(Box, { mt: 3 }, tab === 0 && /* @__PURE__ */ React.createElement(DashboardContent, { api }), tab === 1 && /* @__PURE__ */ React.createElement(SettingsContent, { api }))));
34
+ };
35
+
36
+ export { DevxpPage };
37
+ //# sourceMappingURL=DevxpPage.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DevxpPage.esm.js","sources":["../../src/components/DevxpPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { Header, Page, Content } from '@backstage/core-components';\nimport {\n useApi,\n fetchApiRef,\n discoveryApiRef,\n} from '@backstage/core-plugin-api';\nimport { Tabs, Tab, Box } from '@material-ui/core';\nimport { DevxpClient } from '../api';\nimport { DashboardContent } from './DashboardContent';\nimport { SettingsContent } from './SettingsContent';\n\nexport const DevxpPage = () => {\n const [tab, setTab] = useState(0);\n const fetchApi = useApi(fetchApiRef);\n const discoveryApi = useApi(discoveryApiRef);\n\n const api = useMemo(\n () => new DevxpClient({ fetchApi, discoveryApi }),\n [fetchApi, discoveryApi],\n );\n\n return (\n <Page themeId=\"tool\">\n <Header\n title=\"Developer Intelligence\"\n subtitle=\"DevXP Analytics & Developer Name Mapping\"\n />\n <Content>\n <Tabs\n value={tab}\n onChange={(_, v) => setTab(v)}\n indicatorColor=\"primary\"\n textColor=\"primary\"\n >\n <Tab label=\"Dashboard\" />\n <Tab label=\"Settings\" />\n </Tabs>\n <Box mt={3}>\n {tab === 0 && <DashboardContent api={api} />}\n {tab === 1 && <SettingsContent api={api} />}\n </Box>\n </Content>\n </Page>\n );\n};\n"],"names":[],"mappings":";;;;;;;;AAYO,MAAM,YAAY,MAAM;AAC7B,EAAA,MAAM,CAAC,GAAA,EAAK,MAAM,CAAA,GAAI,SAAS,CAAC,CAAA;AAChC,EAAA,MAAM,QAAA,GAAW,OAAO,WAAW,CAAA;AACnC,EAAA,MAAM,YAAA,GAAe,OAAO,eAAe,CAAA;AAE3C,EAAA,MAAM,GAAA,GAAM,OAAA;AAAA,IACV,MAAM,IAAI,WAAA,CAAY,EAAE,QAAA,EAAU,cAAc,CAAA;AAAA,IAChD,CAAC,UAAU,YAAY;AAAA,GACzB;AAEA,EAAA,uBACE,KAAA,CAAA,aAAA,CAAC,IAAA,EAAA,EAAK,OAAA,EAAQ,MAAA,EAAA,kBACZ,KAAA,CAAA,aAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAM,wBAAA;AAAA,MACN,QAAA,EAAS;AAAA;AAAA,GACX,sCACC,OAAA,EAAA,IAAA,kBACC,KAAA,CAAA,aAAA;AAAA,IAAC,IAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAO,GAAA;AAAA,MACP,QAAA,EAAU,CAAC,CAAA,EAAG,CAAA,KAAM,OAAO,CAAC,CAAA;AAAA,MAC5B,cAAA,EAAe,SAAA;AAAA,MACf,SAAA,EAAU;AAAA,KAAA;AAAA,oBAEV,KAAA,CAAA,aAAA,CAAC,GAAA,EAAA,EAAI,KAAA,EAAM,WAAA,EAAY,CAAA;AAAA,oBACvB,KAAA,CAAA,aAAA,CAAC,GAAA,EAAA,EAAI,KAAA,EAAM,UAAA,EAAW;AAAA,qBAExB,KAAA,CAAA,aAAA,CAAC,GAAA,EAAA,EAAI,IAAI,CAAA,EAAA,EACN,GAAA,KAAQ,qBAAK,KAAA,CAAA,aAAA,CAAC,gBAAA,EAAA,EAAiB,GAAA,EAAU,CAAA,EACzC,QAAQ,CAAA,oBAAK,KAAA,CAAA,aAAA,CAAC,mBAAgB,GAAA,EAAU,CAC3C,CACF,CACF,CAAA;AAEJ;;;;"}
@@ -0,0 +1,142 @@
1
+ import React, { useRef, useState, useCallback, useEffect } from 'react';
2
+ import { Grid, Typography, Box, Button, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, IconButton } from '@material-ui/core';
3
+ import { makeStyles } from '@material-ui/core/styles';
4
+ import DeleteIcon from '@material-ui/icons/Delete';
5
+ import CloudUploadIcon from '@material-ui/icons/CloudUpload';
6
+ import { InfoCard } from '@backstage/core-components';
7
+
8
+ const useStyles = makeStyles((theme) => ({
9
+ fileInput: {
10
+ display: "none"
11
+ },
12
+ fileName: {
13
+ marginLeft: theme.spacing(2),
14
+ color: theme.palette.text.secondary
15
+ },
16
+ maskedCell: {
17
+ fontFamily: "monospace",
18
+ fontSize: "0.9em"
19
+ },
20
+ statusMessage: {
21
+ marginTop: theme.spacing(2),
22
+ padding: theme.spacing(1.5),
23
+ borderRadius: theme.shape.borderRadius
24
+ },
25
+ success: {
26
+ backgroundColor: "#e8f5e9",
27
+ color: "#2e7d32"
28
+ },
29
+ error: {
30
+ backgroundColor: "#ffebee",
31
+ color: "#c62828"
32
+ },
33
+ emptyState: {
34
+ textAlign: "center",
35
+ padding: theme.spacing(4),
36
+ color: theme.palette.text.secondary
37
+ }
38
+ }));
39
+ const SettingsContent = ({ api }) => {
40
+ const classes = useStyles();
41
+ const fileInputRef = useRef(null);
42
+ const [mappings, setMappings] = useState([]);
43
+ const [loading, setLoading] = useState(true);
44
+ const [selectedFileName, setSelectedFileName] = useState("");
45
+ const [fileContent, setFileContent] = useState("");
46
+ const [uploading, setUploading] = useState(false);
47
+ const [statusMessage, setStatusMessage] = useState(null);
48
+ const loadMappings = useCallback(async () => {
49
+ try {
50
+ setLoading(true);
51
+ const result = await api.getMappings();
52
+ setMappings(result.mappings);
53
+ } catch {
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ }, [api]);
58
+ useEffect(() => {
59
+ loadMappings();
60
+ }, [loadMappings]);
61
+ const handleFileSelect = (event) => {
62
+ const file = event.target.files?.[0];
63
+ if (!file) return;
64
+ setSelectedFileName(file.name);
65
+ setStatusMessage(null);
66
+ const reader = new FileReader();
67
+ reader.onload = (e) => {
68
+ setFileContent(e.target?.result);
69
+ };
70
+ reader.readAsText(file);
71
+ };
72
+ const handleUpload = async () => {
73
+ if (!fileContent) return;
74
+ setUploading(true);
75
+ setStatusMessage(null);
76
+ try {
77
+ const result = await api.uploadCsv(fileContent);
78
+ if (result.error) {
79
+ setStatusMessage({ text: result.error, isError: true });
80
+ } else {
81
+ setStatusMessage({ text: result.message, isError: false });
82
+ setSelectedFileName("");
83
+ setFileContent("");
84
+ if (fileInputRef.current) fileInputRef.current.value = "";
85
+ await loadMappings();
86
+ }
87
+ } catch (e) {
88
+ setStatusMessage({
89
+ text: e.message || "Upload failed",
90
+ isError: true
91
+ });
92
+ } finally {
93
+ setUploading(false);
94
+ }
95
+ };
96
+ const handleDelete = async (maskedName) => {
97
+ try {
98
+ await api.deleteMapping(maskedName);
99
+ await loadMappings();
100
+ } catch {
101
+ }
102
+ };
103
+ return /* @__PURE__ */ React.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Upload Developer Names" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary", paragraph: true }, "Upload a CSV file with developer names (one name per line). The system will compute SHA-256 hashes using the configured salt and store the masked-to-real name mappings. Duplicate names will be updated."), /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center" }, /* @__PURE__ */ React.createElement(
104
+ "input",
105
+ {
106
+ ref: fileInputRef,
107
+ type: "file",
108
+ accept: ".csv,.txt",
109
+ className: classes.fileInput,
110
+ onChange: handleFileSelect,
111
+ id: "devxp-csv-upload"
112
+ }
113
+ ), /* @__PURE__ */ React.createElement("label", { htmlFor: "devxp-csv-upload" }, /* @__PURE__ */ React.createElement(Button, { variant: "outlined", component: "span" }, "Choose CSV File")), selectedFileName && /* @__PURE__ */ React.createElement(Typography, { className: classes.fileName, variant: "body2" }, selectedFileName), /* @__PURE__ */ React.createElement(
114
+ Button,
115
+ {
116
+ variant: "contained",
117
+ color: "primary",
118
+ onClick: handleUpload,
119
+ disabled: !fileContent || uploading,
120
+ startIcon: /* @__PURE__ */ React.createElement(CloudUploadIcon, null),
121
+ style: { marginLeft: 16 }
122
+ },
123
+ uploading ? "Processing..." : "Upload & Process"
124
+ )), statusMessage && /* @__PURE__ */ React.createElement(
125
+ Box,
126
+ {
127
+ className: `${classes.statusMessage} ${statusMessage.isError ? classes.error : classes.success}`
128
+ },
129
+ /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, statusMessage.text)
130
+ ))), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Developer Mappings" }, loading ? /* @__PURE__ */ React.createElement(Typography, null, "Loading mappings...") : mappings.length === 0 ? /* @__PURE__ */ React.createElement(Box, { className: classes.emptyState }, /* @__PURE__ */ React.createElement(Typography, { variant: "body1" }, "No developer mappings yet. Upload a CSV file above to get started.")) : /* @__PURE__ */ React.createElement(TableContainer, { component: Paper, variant: "outlined" }, /* @__PURE__ */ React.createElement(Table, { size: "small" }, /* @__PURE__ */ React.createElement(TableHead, null, /* @__PURE__ */ React.createElement(TableRow, null, /* @__PURE__ */ React.createElement(TableCell, null, "Masked Name"), /* @__PURE__ */ React.createElement(TableCell, null, "Real Name"), /* @__PURE__ */ React.createElement(TableCell, null, "Created"), /* @__PURE__ */ React.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React.createElement(TableBody, null, mappings.map((m) => /* @__PURE__ */ React.createElement(TableRow, { key: m.masked_name }, /* @__PURE__ */ React.createElement(TableCell, { className: classes.maskedCell }, m.masked_name), /* @__PURE__ */ React.createElement(TableCell, null, m.real_name), /* @__PURE__ */ React.createElement(TableCell, null, new Date(m.created_at).toLocaleDateString()), /* @__PURE__ */ React.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React.createElement(
131
+ IconButton,
132
+ {
133
+ size: "small",
134
+ onClick: () => handleDelete(m.masked_name),
135
+ "aria-label": "Delete mapping"
136
+ },
137
+ /* @__PURE__ */ React.createElement(DeleteIcon, { fontSize: "small" })
138
+ ))))))))));
139
+ };
140
+
141
+ export { SettingsContent };
142
+ //# sourceMappingURL=SettingsContent.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SettingsContent.esm.js","sources":["../../src/components/SettingsContent.tsx"],"sourcesContent":["import React, { useState, useEffect, useCallback, useRef } from 'react';\nimport {\n Typography,\n Button,\n Box,\n Table,\n TableBody,\n TableCell,\n TableContainer,\n TableHead,\n TableRow,\n Paper,\n IconButton,\n Grid,\n} from '@material-ui/core';\nimport { makeStyles } from '@material-ui/core/styles';\nimport DeleteIcon from '@material-ui/icons/Delete';\nimport CloudUploadIcon from '@material-ui/icons/CloudUpload';\nimport { InfoCard } from '@backstage/core-components';\nimport type { DevxpApi } from '../api';\nimport type { DeveloperMapping } from '../types';\n\nconst useStyles = makeStyles(theme => ({\n fileInput: {\n display: 'none',\n },\n fileName: {\n marginLeft: theme.spacing(2),\n color: theme.palette.text.secondary,\n },\n maskedCell: {\n fontFamily: 'monospace',\n fontSize: '0.9em',\n },\n statusMessage: {\n marginTop: theme.spacing(2),\n padding: theme.spacing(1.5),\n borderRadius: theme.shape.borderRadius,\n },\n success: {\n backgroundColor: '#e8f5e9',\n color: '#2e7d32',\n },\n error: {\n backgroundColor: '#ffebee',\n color: '#c62828',\n },\n emptyState: {\n textAlign: 'center',\n padding: theme.spacing(4),\n color: theme.palette.text.secondary,\n },\n}));\n\ninterface SettingsContentProps {\n api: DevxpApi;\n}\n\nexport const SettingsContent = ({ api }: SettingsContentProps) => {\n const classes = useStyles();\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [mappings, setMappings] = useState<DeveloperMapping[]>([]);\n const [loading, setLoading] = useState(true);\n const [selectedFileName, setSelectedFileName] = useState('');\n const [fileContent, setFileContent] = useState('');\n const [uploading, setUploading] = useState(false);\n const [statusMessage, setStatusMessage] = useState<{\n text: string;\n isError: boolean;\n } | null>(null);\n\n const loadMappings = useCallback(async () => {\n try {\n setLoading(true);\n const result = await api.getMappings();\n setMappings(result.mappings);\n } catch {\n // Failed to load mappings\n } finally {\n setLoading(false);\n }\n }, [api]);\n\n useEffect(() => {\n loadMappings();\n }, [loadMappings]);\n\n const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n if (!file) return;\n\n setSelectedFileName(file.name);\n setStatusMessage(null);\n\n const reader = new FileReader();\n reader.onload = e => {\n setFileContent(e.target?.result as string);\n };\n reader.readAsText(file);\n };\n\n const handleUpload = async () => {\n if (!fileContent) return;\n\n setUploading(true);\n setStatusMessage(null);\n try {\n const result = await api.uploadCsv(fileContent);\n if (result.error) {\n setStatusMessage({ text: result.error, isError: true });\n } else {\n setStatusMessage({ text: result.message, isError: false });\n setSelectedFileName('');\n setFileContent('');\n if (fileInputRef.current) fileInputRef.current.value = '';\n await loadMappings();\n }\n } catch (e: any) {\n setStatusMessage({\n text: e.message || 'Upload failed',\n isError: true,\n });\n } finally {\n setUploading(false);\n }\n };\n\n const handleDelete = async (maskedName: string) => {\n try {\n await api.deleteMapping(maskedName);\n await loadMappings();\n } catch {\n // Delete failed\n }\n };\n\n return (\n <Grid container spacing={3}>\n {/* CSV Upload Section */}\n <Grid item xs={12}>\n <InfoCard title=\"Upload Developer Names\">\n <Typography variant=\"body2\" color=\"textSecondary\" paragraph>\n Upload a CSV file with developer names (one name per line). The\n system will compute SHA-256 hashes using the configured salt and\n store the masked-to-real name mappings. Duplicate names will be\n updated.\n </Typography>\n <Box display=\"flex\" alignItems=\"center\">\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\".csv,.txt\"\n className={classes.fileInput}\n onChange={handleFileSelect}\n id=\"devxp-csv-upload\"\n />\n <label htmlFor=\"devxp-csv-upload\">\n <Button variant=\"outlined\" component=\"span\">\n Choose CSV File\n </Button>\n </label>\n {selectedFileName && (\n <Typography className={classes.fileName} variant=\"body2\">\n {selectedFileName}\n </Typography>\n )}\n <Button\n variant=\"contained\"\n color=\"primary\"\n onClick={handleUpload}\n disabled={!fileContent || uploading}\n startIcon={<CloudUploadIcon />}\n style={{ marginLeft: 16 }}\n >\n {uploading ? 'Processing...' : 'Upload & Process'}\n </Button>\n </Box>\n {statusMessage && (\n <Box\n className={`${classes.statusMessage} ${statusMessage.isError ? classes.error : classes.success}`}\n >\n <Typography variant=\"body2\">{statusMessage.text}</Typography>\n </Box>\n )}\n </InfoCard>\n </Grid>\n\n {/* Mappings Table */}\n <Grid item xs={12}>\n <InfoCard title=\"Developer Mappings\">\n {loading ? (\n <Typography>Loading mappings...</Typography>\n ) : mappings.length === 0 ? (\n <Box className={classes.emptyState}>\n <Typography variant=\"body1\">\n No developer mappings yet. Upload a CSV file above to get\n started.\n </Typography>\n </Box>\n ) : (\n <TableContainer component={Paper} variant=\"outlined\">\n <Table size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Masked Name</TableCell>\n <TableCell>Real Name</TableCell>\n <TableCell>Created</TableCell>\n <TableCell align=\"right\">Actions</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {mappings.map(m => (\n <TableRow key={m.masked_name}>\n <TableCell className={classes.maskedCell}>\n {m.masked_name}\n </TableCell>\n <TableCell>{m.real_name}</TableCell>\n <TableCell>\n {new Date(m.created_at).toLocaleDateString()}\n </TableCell>\n <TableCell align=\"right\">\n <IconButton\n size=\"small\"\n onClick={() => handleDelete(m.masked_name)}\n aria-label=\"Delete mapping\"\n >\n <DeleteIcon fontSize=\"small\" />\n </IconButton>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </TableContainer>\n )}\n </InfoCard>\n </Grid>\n </Grid>\n );\n};\n"],"names":[],"mappings":";;;;;;;AAsBA,MAAM,SAAA,GAAY,WAAW,CAAA,KAAA,MAAU;AAAA,EACrC,SAAA,EAAW;AAAA,IACT,OAAA,EAAS;AAAA,GACX;AAAA,EACA,QAAA,EAAU;AAAA,IACR,UAAA,EAAY,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IAC3B,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK;AAAA,GAC5B;AAAA,EACA,UAAA,EAAY;AAAA,IACV,UAAA,EAAY,WAAA;AAAA,IACZ,QAAA,EAAU;AAAA,GACZ;AAAA,EACA,aAAA,EAAe;AAAA,IACb,SAAA,EAAW,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IAC1B,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAAA,IAC1B,YAAA,EAAc,MAAM,KAAA,CAAM;AAAA,GAC5B;AAAA,EACA,OAAA,EAAS;AAAA,IACP,eAAA,EAAiB,SAAA;AAAA,IACjB,KAAA,EAAO;AAAA,GACT;AAAA,EACA,KAAA,EAAO;AAAA,IACL,eAAA,EAAiB,SAAA;AAAA,IACjB,KAAA,EAAO;AAAA,GACT;AAAA,EACA,UAAA,EAAY;AAAA,IACV,SAAA,EAAW,QAAA;AAAA,IACX,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACxB,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK;AAAA;AAE9B,CAAA,CAAE,CAAA;AAMK,MAAM,eAAA,GAAkB,CAAC,EAAE,GAAA,EAAI,KAA4B;AAChE,EAAA,MAAM,UAAU,SAAA,EAAU;AAC1B,EAAA,MAAM,YAAA,GAAe,OAAyB,IAAI,CAAA;AAClD,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAI,QAAA,CAA6B,EAAE,CAAA;AAC/D,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,IAAI,CAAA;AAC3C,EAAA,MAAM,CAAC,gBAAA,EAAkB,mBAAmB,CAAA,GAAI,SAAS,EAAE,CAAA;AAC3D,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAI,SAAS,EAAE,CAAA;AACjD,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAS,KAAK,CAAA;AAChD,EAAA,MAAM,CAAC,aAAA,EAAe,gBAAgB,CAAA,GAAI,SAGhC,IAAI,CAAA;AAEd,EAAA,MAAM,YAAA,GAAe,YAAY,YAAY;AAC3C,IAAA,IAAI;AACF,MAAA,UAAA,CAAW,IAAI,CAAA;AACf,MAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,WAAA,EAAY;AACrC,MAAA,WAAA,CAAY,OAAO,QAAQ,CAAA;AAAA,IAC7B,CAAA,CAAA,MAAQ;AAAA,IAER,CAAA,SAAE;AACA,MAAA,UAAA,CAAW,KAAK,CAAA;AAAA,IAClB;AAAA,EACF,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAER,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,YAAA,EAAa;AAAA,EACf,CAAA,EAAG,CAAC,YAAY,CAAC,CAAA;AAEjB,EAAA,MAAM,gBAAA,GAAmB,CAAC,KAAA,KAA+C;AACvE,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,MAAA,CAAO,KAAA,GAAQ,CAAC,CAAA;AACnC,IAAA,IAAI,CAAC,IAAA,EAAM;AAEX,IAAA,mBAAA,CAAoB,KAAK,IAAI,CAAA;AAC7B,IAAA,gBAAA,CAAiB,IAAI,CAAA;AAErB,IAAA,MAAM,MAAA,GAAS,IAAI,UAAA,EAAW;AAC9B,IAAA,MAAA,CAAO,SAAS,CAAA,CAAA,KAAK;AACnB,MAAA,cAAA,CAAe,CAAA,CAAE,QAAQ,MAAgB,CAAA;AAAA,IAC3C,CAAA;AACA,IAAA,MAAA,CAAO,WAAW,IAAI,CAAA;AAAA,EACxB,CAAA;AAEA,EAAA,MAAM,eAAe,YAAY;AAC/B,IAAA,IAAI,CAAC,WAAA,EAAa;AAElB,IAAA,YAAA,CAAa,IAAI,CAAA;AACjB,IAAA,gBAAA,CAAiB,IAAI,CAAA;AACrB,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,SAAA,CAAU,WAAW,CAAA;AAC9C,MAAA,IAAI,OAAO,KAAA,EAAO;AAChB,QAAA,gBAAA,CAAiB,EAAE,IAAA,EAAM,MAAA,CAAO,KAAA,EAAO,OAAA,EAAS,MAAM,CAAA;AAAA,MACxD,CAAA,MAAO;AACL,QAAA,gBAAA,CAAiB,EAAE,IAAA,EAAM,MAAA,CAAO,OAAA,EAAS,OAAA,EAAS,OAAO,CAAA;AACzD,QAAA,mBAAA,CAAoB,EAAE,CAAA;AACtB,QAAA,cAAA,CAAe,EAAE,CAAA;AACjB,QAAA,IAAI,YAAA,CAAa,OAAA,EAAS,YAAA,CAAa,OAAA,CAAQ,KAAA,GAAQ,EAAA;AACvD,QAAA,MAAM,YAAA,EAAa;AAAA,MACrB;AAAA,IACF,SAAS,CAAA,EAAQ;AACf,MAAA,gBAAA,CAAiB;AAAA,QACf,IAAA,EAAM,EAAE,OAAA,IAAW,eAAA;AAAA,QACnB,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH,CAAA,SAAE;AACA,MAAA,YAAA,CAAa,KAAK,CAAA;AAAA,IACpB;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,YAAA,GAAe,OAAO,UAAA,KAAuB;AACjD,IAAA,IAAI;AACF,MAAA,MAAM,GAAA,CAAI,cAAc,UAAU,CAAA;AAClC,MAAA,MAAM,YAAA,EAAa;AAAA,IACrB,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,CAAA;AAEA,EAAA,uBACE,KAAA,CAAA,aAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAS,IAAA,EAAC,OAAA,EAAS,CAAA,EAAA,kBAEvB,KAAA,CAAA,aAAA,CAAC,IAAA,EAAA,EAAK,IAAA,EAAI,IAAA,EAAC,EAAA,EAAI,EAAA,EAAA,sCACZ,QAAA,EAAA,EAAS,KAAA,EAAM,wBAAA,EAAA,kBACd,KAAA,CAAA,aAAA,CAAC,UAAA,EAAA,EAAW,OAAA,EAAQ,OAAA,EAAQ,KAAA,EAAM,iBAAgB,SAAA,EAAS,IAAA,EAAA,EAAC,2MAK5D,CAAA,kBACA,KAAA,CAAA,aAAA,CAAC,GAAA,EAAA,EAAI,OAAA,EAAQ,MAAA,EAAO,YAAW,QAAA,EAAA,kBAC7B,KAAA,CAAA,aAAA;AAAA,IAAC,OAAA;AAAA,IAAA;AAAA,MACC,GAAA,EAAK,YAAA;AAAA,MACL,IAAA,EAAK,MAAA;AAAA,MACL,MAAA,EAAO,WAAA;AAAA,MACP,WAAW,OAAA,CAAQ,SAAA;AAAA,MACnB,QAAA,EAAU,gBAAA;AAAA,MACV,EAAA,EAAG;AAAA;AAAA,GACL,sCACC,OAAA,EAAA,EAAM,OAAA,EAAQ,sCACb,KAAA,CAAA,aAAA,CAAC,MAAA,EAAA,EAAO,OAAA,EAAQ,UAAA,EAAW,SAAA,EAAU,MAAA,EAAA,EAAO,iBAE5C,CACF,CAAA,EACC,gBAAA,oBACC,KAAA,CAAA,aAAA,CAAC,UAAA,EAAA,EAAW,SAAA,EAAW,QAAQ,QAAA,EAAU,OAAA,EAAQ,OAAA,EAAA,EAC9C,gBACH,CAAA,kBAEF,KAAA,CAAA,aAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,OAAA,EAAQ,WAAA;AAAA,MACR,KAAA,EAAM,SAAA;AAAA,MACN,OAAA,EAAS,YAAA;AAAA,MACT,QAAA,EAAU,CAAC,WAAA,IAAe,SAAA;AAAA,MAC1B,SAAA,sCAAY,eAAA,EAAA,IAAgB,CAAA;AAAA,MAC5B,KAAA,EAAO,EAAE,UAAA,EAAY,EAAA;AAAG,KAAA;AAAA,IAEvB,YAAY,eAAA,GAAkB;AAAA,GAEnC,GACC,aAAA,oBACC,KAAA,CAAA,aAAA;AAAA,IAAC,GAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAW,CAAA,EAAG,OAAA,CAAQ,aAAa,CAAA,CAAA,EAAI,cAAc,OAAA,GAAU,OAAA,CAAQ,KAAA,GAAQ,OAAA,CAAQ,OAAO,CAAA;AAAA,KAAA;AAAA,oBAE9F,KAAA,CAAA,aAAA,CAAC,UAAA,EAAA,EAAW,OAAA,EAAQ,OAAA,EAAA,EAAS,cAAc,IAAK;AAAA,GAGtD,CACF,CAAA,kBAGA,KAAA,CAAA,aAAA,CAAC,QAAK,IAAA,EAAI,IAAA,EAAC,EAAA,EAAI,EAAA,EAAA,kBACb,KAAA,CAAA,aAAA,CAAC,QAAA,EAAA,EAAS,OAAM,oBAAA,EAAA,EACb,OAAA,uCACE,UAAA,EAAA,IAAA,EAAW,qBAAmB,IAC7B,QAAA,CAAS,MAAA,KAAW,CAAA,mBACtB,KAAA,CAAA,aAAA,CAAC,GAAA,EAAA,EAAI,SAAA,EAAW,QAAQ,UAAA,EAAA,kBACtB,KAAA,CAAA,aAAA,CAAC,cAAW,OAAA,EAAQ,OAAA,EAAA,EAAQ,oEAG5B,CACF,CAAA,mBAEA,KAAA,CAAA,aAAA,CAAC,cAAA,EAAA,EAAe,SAAA,EAAW,KAAA,EAAO,SAAQ,UAAA,EAAA,kBACxC,KAAA,CAAA,aAAA,CAAC,KAAA,EAAA,EAAM,IAAA,EAAK,OAAA,EAAA,kBACV,KAAA,CAAA,aAAA,CAAC,iCACC,KAAA,CAAA,aAAA,CAAC,QAAA,EAAA,IAAA,kBACC,KAAA,CAAA,aAAA,CAAC,SAAA,EAAA,IAAA,EAAU,aAAW,CAAA,sCACrB,SAAA,EAAA,IAAA,EAAU,WAAS,mBACpB,KAAA,CAAA,aAAA,CAAC,SAAA,EAAA,IAAA,EAAU,SAAO,CAAA,kBAClB,KAAA,CAAA,aAAA,CAAC,SAAA,EAAA,EAAU,KAAA,EAAM,OAAA,EAAA,EAAQ,SAAO,CAClC,CACF,CAAA,kBACA,KAAA,CAAA,aAAA,CAAC,SAAA,EAAA,IAAA,EACE,QAAA,CAAS,GAAA,CAAI,uBACZ,KAAA,CAAA,aAAA,CAAC,QAAA,EAAA,EAAS,GAAA,EAAK,CAAA,CAAE,WAAA,EAAA,kBACf,KAAA,CAAA,aAAA,CAAC,aAAU,SAAA,EAAW,OAAA,CAAQ,cAC3B,CAAA,CAAE,WACL,mBACA,KAAA,CAAA,aAAA,CAAC,SAAA,EAAA,IAAA,EAAW,CAAA,CAAE,SAAU,CAAA,kBACxB,KAAA,CAAA,aAAA,CAAC,iBACE,IAAI,IAAA,CAAK,CAAA,CAAE,UAAU,CAAA,CAAE,kBAAA,EAC1B,CAAA,kBACA,KAAA,CAAA,aAAA,CAAC,SAAA,EAAA,EAAU,KAAA,EAAM,OAAA,EAAA,kBACf,KAAA,CAAA,aAAA;AAAA,IAAC,UAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,OAAA;AAAA,MACL,OAAA,EAAS,MAAM,YAAA,CAAa,CAAA,CAAE,WAAW,CAAA;AAAA,MACzC,YAAA,EAAW;AAAA,KAAA;AAAA,oBAEX,KAAA,CAAA,aAAA,CAAC,UAAA,EAAA,EAAW,QAAA,EAAS,OAAA,EAAQ;AAAA,GAEjC,CACF,CACD,CACH,CACF,CACF,CAEJ,CACF,CACF,CAAA;AAEJ;;;;"}
@@ -0,0 +1,50 @@
1
+ import * as react from 'react';
2
+ import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+
5
+ declare const _default: _backstage_frontend_plugin_api.OverridableFrontendPlugin<{}, {}, {
6
+ "page:devxp": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
7
+ kind: "page";
8
+ name: undefined;
9
+ config: {
10
+ path: string | undefined;
11
+ title: string | undefined;
12
+ };
13
+ configInput: {
14
+ title?: string | undefined;
15
+ path?: string | undefined;
16
+ };
17
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
18
+ optional: true;
19
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.title", {
20
+ optional: true;
21
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
22
+ optional: true;
23
+ }>;
24
+ inputs: {
25
+ pages: _backstage_frontend_plugin_api.ExtensionInput<_backstage_frontend_plugin_api.ConfigurableExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
26
+ optional: true;
27
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.title", {
28
+ optional: true;
29
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
30
+ optional: true;
31
+ }>, {
32
+ singleton: false;
33
+ optional: false;
34
+ internal: false;
35
+ }>;
36
+ };
37
+ params: {
38
+ path: string;
39
+ title?: string;
40
+ icon?: _backstage_frontend_plugin_api.IconElement;
41
+ loader?: () => Promise<react.JSX.Element>;
42
+ routeRef?: _backstage_frontend_plugin_api.RouteRef;
43
+ noHeader?: boolean;
44
+ };
45
+ }>;
46
+ }>;
47
+
48
+ declare const DevxpPage: () => react_jsx_runtime.JSX.Element;
49
+
50
+ export { DevxpPage, _default as default };
@@ -0,0 +1,3 @@
1
+ export { default } from './plugin.esm.js';
2
+ export { DevxpPage } from './components/DevxpPage.esm.js';
3
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,16 @@
1
+ import { PageBlueprint, createFrontendPlugin } from '@backstage/frontend-plugin-api';
2
+
3
+ const devxpPage = PageBlueprint.make({
4
+ params: {
5
+ path: "/devxp",
6
+ title: "Dev Intelligence",
7
+ loader: () => import('./components/DevxpPage.esm.js').then((m) => /* @__PURE__ */ React.createElement(m.DevxpPage, null))
8
+ }
9
+ });
10
+ var plugin = createFrontendPlugin({
11
+ pluginId: "devxp",
12
+ extensions: [devxpPage]
13
+ });
14
+
15
+ export { plugin as default };
16
+ //# sourceMappingURL=plugin.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["import { createFrontendPlugin, PageBlueprint } from '@backstage/frontend-plugin-api';\n\nconst devxpPage = PageBlueprint.make({\n params: {\n path: '/devxp',\n title: 'Dev Intelligence',\n loader: () =>\n import('./components/DevxpPage').then(m => <m.DevxpPage />),\n },\n});\n\nexport default createFrontendPlugin({\n pluginId: 'devxp',\n extensions: [devxpPage],\n});\n"],"names":[],"mappings":";;AAEA,MAAM,SAAA,GAAY,cAAc,IAAA,CAAK;AAAA,EACnC,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM,QAAA;AAAA,IACN,KAAA,EAAO,kBAAA;AAAA,IACP,MAAA,EAAQ,MACN,OAAO,+BAAwB,CAAA,CAAE,IAAA,CAAK,CAAA,CAAA,qBAAK,KAAA,CAAA,aAAA,CAAC,CAAA,CAAE,SAAA,EAAF,IAAY,CAAE;AAAA;AAEhE,CAAC,CAAA;AAED,aAAe,oBAAA,CAAqB;AAAA,EAClC,QAAA,EAAU,OAAA;AAAA,EACV,UAAA,EAAY,CAAC,SAAS;AACxB,CAAC,CAAA;;;;"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@karimov-labs/backstage-plugin-devxp",
3
+ "version": "1.0.0",
4
+ "description": "Backstage frontend plugin for developer intelligence — masked identity management, CSV upload, and unmask tooling.",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "license": "Apache-2.0",
8
+ "author": "karimov-labs",
9
+ "homepage": "https://github.com/karimov-labs/backstage-plugin-devxp#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/karimov-labs/backstage-plugin-devxp.git",
13
+ "directory": "plugins/devxp"
14
+ },
15
+ "keywords": [
16
+ "backstage",
17
+ "plugin",
18
+ "developer-intelligence",
19
+ "devxp",
20
+ "identity-masking"
21
+ ],
22
+ "backstage": {
23
+ "role": "frontend-plugin",
24
+ "pluginId": "devxp"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "main": "dist/index.cjs.js",
29
+ "module": "dist/index.esm.js",
30
+ "types": "dist/index.d.ts"
31
+ },
32
+ "scripts": {
33
+ "start": "backstage-cli package start",
34
+ "build": "backstage-cli package build",
35
+ "lint": "backstage-cli package lint",
36
+ "test": "backstage-cli package test",
37
+ "clean": "backstage-cli package clean"
38
+ },
39
+ "dependencies": {
40
+ "@backstage/core-components": "^0.18.8",
41
+ "@backstage/core-plugin-api": "^1.12.4",
42
+ "@backstage/frontend-plugin-api": "^0.15.0",
43
+ "@material-ui/core": "^4.12.2",
44
+ "@material-ui/icons": "^4.9.1",
45
+ "@material-ui/lab": "^4.0.0-alpha.61",
46
+ "react": "^18.0.2",
47
+ "react-dom": "^18.0.2"
48
+ },
49
+ "devDependencies": {
50
+ "@backstage/cli": "^0.36.0",
51
+ "@types/react": "*",
52
+ "@types/react-dom": "*"
53
+ },
54
+ "peerDependencies": {
55
+ "react": "^18.0.2",
56
+ "react-dom": "^18.0.2"
57
+ },
58
+ "files": [
59
+ "dist"
60
+ ]
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default } from './plugin';
2
+ export { DevxpPage } from './components/DevxpPage';