@karimov-labs/backstage-plugin-devxp 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -10
- package/dist/api.esm.js +51 -0
- package/dist/api.esm.js.map +1 -1
- package/dist/components/DashboardContent.esm.js +55 -16
- package/dist/components/DashboardContent.esm.js.map +1 -1
- package/dist/components/DevxpPage.esm.js +3 -3
- package/dist/components/DevxpPage.esm.js.map +1 -1
- package/dist/components/GithubSyncSection.esm.js +310 -0
- package/dist/components/GithubSyncSection.esm.js.map +1 -0
- package/dist/components/SettingsContent.esm.js +9 -8
- package/dist/components/SettingsContent.esm.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/plugin.esm.js +2 -1
- package/dist/plugin.esm.js.map +1 -1
- package/package.json +5 -7
- package/src/index.ts +0 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @karimov-labs/backstage-plugin-devxp
|
|
2
2
|
|
|
3
|
-
A Backstage frontend plugin that provides a **Developer Intelligence** dashboard for managing masked developer identities. It lets platform teams
|
|
3
|
+
A Backstage frontend plugin that provides a **Developer Intelligence** dashboard for managing masked developer identities. It lets platform teams populate a real-name → masked-hash mapping database via CSV upload or automatic GitHub organization sync, and look up or verify masked identities — all without exposing real names to third-party analytics tools.
|
|
4
4
|
|
|
5
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
6
|
|
|
@@ -10,9 +10,20 @@ Built for use with the [DevXP](https://devxp.net) developer analytics platform.
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **Dashboard tab**
|
|
14
|
-
-
|
|
15
|
-
-
|
|
13
|
+
- **Dashboard tab**
|
|
14
|
+
- Configuration health indicators (salt, API token, endpoint, project ID)
|
|
15
|
+
- Total mapping count
|
|
16
|
+
- GitHub Auto-Sync Configurations table — shows all registered GitHub Apps with status and last-sync timestamp
|
|
17
|
+
- Inline unmask tester — enter a 16-char hex hash, get the real name back instantly
|
|
18
|
+
- Triggers automatic GitHub member sync on page load (throttled to once per 24 hours server-side)
|
|
19
|
+
- **Settings tab**
|
|
20
|
+
- **GitHub Organization Auto-Sync** — register a GitHub App (github.com or GitHub Enterprise Server) to automatically pull all organization members and store their hashed identities
|
|
21
|
+
- Collapsible setup guide with a pre-configured GitHub App creation link
|
|
22
|
+
- Per-config activate/deactivate toggle and manual sync button
|
|
23
|
+
- Full CRUD: register, activate, deactivate, delete
|
|
24
|
+
- **CSV Upload** — upload a plain-text file with one real developer name per line to bulk-import mappings
|
|
25
|
+
- **Mappings Table** — browse all stored pairs (masked hash → real name) with per-row delete
|
|
26
|
+
- Hashing algorithm matches [dev-xp-analyzer](https://github.com/karimov-labs/dev-xp-analyzer): `SHA-256(salt + realName)`, first 16 hex chars
|
|
16
27
|
|
|
17
28
|
---
|
|
18
29
|
|
|
@@ -21,7 +32,7 @@ Built for use with the [DevXP](https://devxp.net) developer analytics platform.
|
|
|
21
32
|
| Dependency | Version |
|
|
22
33
|
|---|---|
|
|
23
34
|
| Backstage | >= 1.30 |
|
|
24
|
-
| `@karimov-labs/backstage-plugin-devxp-backend` | `^1.
|
|
35
|
+
| `@karimov-labs/backstage-plugin-devxp-backend` | `^1.1.0` |
|
|
25
36
|
| React | `^18` |
|
|
26
37
|
|
|
27
38
|
---
|
|
@@ -107,7 +118,7 @@ export DEVXP_API_ENDPOINT="https://..." # optional
|
|
|
107
118
|
export DEVXP_PROJECT_ID="your-project-id" # optional
|
|
108
119
|
```
|
|
109
120
|
|
|
110
|
-
> **Security note:** The salt
|
|
121
|
+
> **Security note:** The salt, API token, and GitHub App private keys are consumed exclusively by the backend and are never sent to the browser.
|
|
111
122
|
|
|
112
123
|
---
|
|
113
124
|
|
|
@@ -120,15 +131,48 @@ Navigate to `/devxp` in your Backstage instance.
|
|
|
120
131
|
| Section | Description |
|
|
121
132
|
|---|---|
|
|
122
133
|
| Configuration Status | Green/red indicators for salt, API token, endpoint, and project ID |
|
|
123
|
-
| Developer Mappings |
|
|
124
|
-
|
|
|
134
|
+
| Developer Mappings | Total count of stored masked ↔ real name pairs |
|
|
135
|
+
| GitHub Auto-Sync Configurations | Read-only list of all registered GitHub Apps — organization, hostname, client ID, active status, and last-sync time |
|
|
136
|
+
| Unmask Tester | Enter a 16-character hex masked name and press Enter (or click Unmask) to look up the real name |
|
|
137
|
+
|
|
138
|
+
The Dashboard also silently triggers a background auto-sync of all active GitHub configurations when the page is first loaded. The server throttles this to at most once every 24 hours per backend process.
|
|
125
139
|
|
|
126
140
|
### Settings tab
|
|
127
141
|
|
|
142
|
+
#### GitHub Organization Auto-Sync
|
|
143
|
+
|
|
144
|
+
Register one or more GitHub Apps to automatically pull your organization's member list and store hashed mappings.
|
|
145
|
+
|
|
128
146
|
| Section | Description |
|
|
129
147
|
|---|---|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
148
|
+
| Setup guide | Collapsible step-by-step instructions with a pre-configured GitHub App creation link (permissions pre-filled) |
|
|
149
|
+
| Register GitHub App | Form to add a new sync configuration (see below) |
|
|
150
|
+
| Configuration table | Lists all registered apps; click the status chip to toggle active/inactive; use the sync icon to trigger an immediate sync; use the delete icon to remove |
|
|
151
|
+
|
|
152
|
+
**Registration form fields:**
|
|
153
|
+
|
|
154
|
+
| Field | Description |
|
|
155
|
+
|---|---|
|
|
156
|
+
| GitHub Org Name | The login name of your GitHub organization (e.g. `acme-corp`) |
|
|
157
|
+
| GitHub Hostname | `github.com` for GitHub.com, or your GitHub Enterprise Server hostname (e.g. `github.acme.com`) |
|
|
158
|
+
| App Client ID | Shown on the GitHub App's General settings tab (e.g. `Iv1.a1b2c3d4e5f67890`) |
|
|
159
|
+
| App Private Key (PEM) | Full contents of the `.pem` file downloaded from the GitHub App's Private keys section |
|
|
160
|
+
|
|
161
|
+
**Setting up a GitHub App (quick start):**
|
|
162
|
+
|
|
163
|
+
1. Click **"Click here to pre-configure and create your GitHub App"** in the setup guide — this opens GitHub with the app name and `Members: Read-only` permission pre-filled.
|
|
164
|
+
2. Scroll to *Organization permissions* and confirm **Members → Read-only** is checked.
|
|
165
|
+
3. After creating the app, copy the **Client ID** from the General tab.
|
|
166
|
+
4. Scroll to *Private keys* and click **Generate a private key**; paste the downloaded `.pem` contents into the form.
|
|
167
|
+
5. [Install the app](https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app) in your GitHub organization.
|
|
168
|
+
|
|
169
|
+
#### CSV Upload
|
|
170
|
+
|
|
171
|
+
Upload a plain-text file with one real developer name per line. The plugin hashes each name with `SHA-256(salt + name)` and stores the mapping. A header row of `name` (case-insensitive) is automatically skipped.
|
|
172
|
+
|
|
173
|
+
#### Developer Mappings Table
|
|
174
|
+
|
|
175
|
+
Lists all stored hash → real-name pairs with per-row delete buttons. This table reflects both CSV-imported and GitHub-synced entries.
|
|
132
176
|
|
|
133
177
|
---
|
|
134
178
|
|
package/dist/api.esm.js
CHANGED
|
@@ -66,6 +66,57 @@ class DevxpClient {
|
|
|
66
66
|
if (!response.ok) throw new Error(`Failed to delete mapping: ${response.statusText}`);
|
|
67
67
|
return response.json();
|
|
68
68
|
}
|
|
69
|
+
// ─── GitHub Sync ─────────────────────────────────────────────────────────────
|
|
70
|
+
async getGithubSyncConfigs() {
|
|
71
|
+
const url = `${await this.baseUrl()}/github-sync`;
|
|
72
|
+
const response = await this.fetchApi.fetch(url);
|
|
73
|
+
if (!response.ok) throw new Error(`Failed to fetch GitHub sync configs: ${response.statusText}`);
|
|
74
|
+
return response.json();
|
|
75
|
+
}
|
|
76
|
+
async createGithubSyncConfig(orgName, githubHostname, appClientId, appPrivateKey) {
|
|
77
|
+
const url = `${await this.baseUrl()}/github-sync`;
|
|
78
|
+
const response = await this.fetchApi.fetch(url, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/json" },
|
|
81
|
+
body: JSON.stringify({ orgName, githubHostname, appClientId, appPrivateKey })
|
|
82
|
+
});
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
if (!response.ok) throw new Error(data.error || "Failed to create GitHub sync config");
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
async toggleGithubSyncConfig(id, active) {
|
|
88
|
+
const url = `${await this.baseUrl()}/github-sync/${id}/toggle`;
|
|
89
|
+
const response = await this.fetchApi.fetch(url, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ active })
|
|
93
|
+
});
|
|
94
|
+
const data = await response.json();
|
|
95
|
+
if (!response.ok) throw new Error(data.error || "Failed to toggle GitHub sync config");
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
async deleteGithubSyncConfig(id) {
|
|
99
|
+
const url = `${await this.baseUrl()}/github-sync/${id}`;
|
|
100
|
+
const response = await this.fetchApi.fetch(url, {
|
|
101
|
+
method: "DELETE"
|
|
102
|
+
});
|
|
103
|
+
const data = await response.json();
|
|
104
|
+
if (!response.ok) throw new Error(data.error || "Failed to delete GitHub sync config");
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
async syncGithubConfig(id) {
|
|
108
|
+
const url = `${await this.baseUrl()}/github-sync/${id}/sync`;
|
|
109
|
+
const response = await this.fetchApi.fetch(url, { method: "POST" });
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
if (!response.ok) return { message: data.error || "Sync failed", count: 0, orgName: "", error: data.error };
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
async triggerAutoSync() {
|
|
115
|
+
const url = `${await this.baseUrl()}/github-sync/auto`;
|
|
116
|
+
const response = await this.fetchApi.fetch(url, { method: "POST" });
|
|
117
|
+
if (!response.ok) return { message: "Auto-sync trigger failed" };
|
|
118
|
+
return response.json();
|
|
119
|
+
}
|
|
69
120
|
}
|
|
70
121
|
|
|
71
122
|
export { DevxpClient };
|
package/dist/api.esm.js.map
CHANGED
|
@@ -1 +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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"}
|
|
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 GithubSyncConfig,\n GithubSyncResult,\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 // GitHub sync\n getGithubSyncConfigs(): Promise<{ configs: GithubSyncConfig[] }>;\n createGithubSyncConfig(orgName: string, githubHostname: string, appClientId: string, appPrivateKey: string): Promise<{ id: number; message: string }>;\n toggleGithubSyncConfig(id: number, active: boolean): Promise<{ message: string }>;\n deleteGithubSyncConfig(id: number): Promise<{ message: string }>;\n syncGithubConfig(id: number): Promise<GithubSyncResult>;\n triggerAutoSync(): 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 // ─── GitHub Sync ─────────────────────────────────────────────────────────────\n\n async getGithubSyncConfigs(): Promise<{ configs: GithubSyncConfig[] }> {\n const url = `${await this.baseUrl()}/github-sync`;\n const response = await this.fetchApi.fetch(url);\n if (!response.ok) throw new Error(`Failed to fetch GitHub sync configs: ${response.statusText}`);\n return response.json();\n }\n\n async createGithubSyncConfig(\n orgName: string,\n githubHostname: string,\n appClientId: string,\n appPrivateKey: string,\n ): Promise<{ id: number; message: string }> {\n const url = `${await this.baseUrl()}/github-sync`;\n const response = await this.fetchApi.fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgName, githubHostname, appClientId, appPrivateKey }),\n });\n const data = await response.json();\n if (!response.ok) throw new Error(data.error || 'Failed to create GitHub sync config');\n return data;\n }\n\n async toggleGithubSyncConfig(id: number, active: boolean): Promise<{ message: string }> {\n const url = `${await this.baseUrl()}/github-sync/${id}/toggle`;\n const response = await this.fetchApi.fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ active }),\n });\n const data = await response.json();\n if (!response.ok) throw new Error(data.error || 'Failed to toggle GitHub sync config');\n return data;\n }\n\n async deleteGithubSyncConfig(id: number): Promise<{ message: string }> {\n const url = `${await this.baseUrl()}/github-sync/${id}`;\n const response = await this.fetchApi.fetch(url, {\n method: 'DELETE',\n });\n const data = await response.json();\n if (!response.ok) throw new Error(data.error || 'Failed to delete GitHub sync config');\n return data;\n }\n\n async syncGithubConfig(id: number): Promise<GithubSyncResult> {\n const url = `${await this.baseUrl()}/github-sync/${id}/sync`;\n const response = await this.fetchApi.fetch(url, { method: 'POST' });\n const data = await response.json();\n if (!response.ok) return { message: data.error || 'Sync failed', count: 0, orgName: '', error: data.error };\n return data;\n }\n\n async triggerAutoSync(): Promise<{ message: string }> {\n const url = `${await this.baseUrl()}/github-sync/auto`;\n const response = await this.fetchApi.fetch(url, { method: 'POST' });\n if (!response.ok) return { message: 'Auto-sync trigger failed' };\n return response.json();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Typography, Grid, Box,
|
|
1
|
+
import React__default, { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { Typography, Grid, Box, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, Chip, TextField, Button } from '@material-ui/core';
|
|
3
3
|
import { makeStyles } from '@material-ui/core/styles';
|
|
4
4
|
import CheckCircleIcon from '@material-ui/icons/CheckCircle';
|
|
5
5
|
import ErrorIcon from '@material-ui/icons/Error';
|
|
@@ -25,6 +25,23 @@ const useStyles = makeStyles((theme) => ({
|
|
|
25
25
|
resultValue: {
|
|
26
26
|
fontFamily: "monospace",
|
|
27
27
|
fontSize: "1.1em"
|
|
28
|
+
},
|
|
29
|
+
activeChip: {
|
|
30
|
+
backgroundColor: "#e8f5e9",
|
|
31
|
+
color: "#2e7d32"
|
|
32
|
+
},
|
|
33
|
+
inactiveChip: {
|
|
34
|
+
backgroundColor: "#f5f5f5",
|
|
35
|
+
color: "#757575"
|
|
36
|
+
},
|
|
37
|
+
lastSynced: {
|
|
38
|
+
fontSize: "0.75rem",
|
|
39
|
+
color: theme.palette.text.secondary
|
|
40
|
+
},
|
|
41
|
+
emptySync: {
|
|
42
|
+
padding: theme.spacing(2),
|
|
43
|
+
color: theme.palette.text.secondary,
|
|
44
|
+
fontStyle: "italic"
|
|
28
45
|
}
|
|
29
46
|
}));
|
|
30
47
|
const DashboardContent = ({ api }) => {
|
|
@@ -34,6 +51,8 @@ const DashboardContent = ({ api }) => {
|
|
|
34
51
|
const [maskedInput, setMaskedInput] = useState("");
|
|
35
52
|
const [unmaskResult, setUnmaskResult] = useState(null);
|
|
36
53
|
const [unmaskError, setUnmaskError] = useState("");
|
|
54
|
+
const [syncConfigs, setSyncConfigs] = useState([]);
|
|
55
|
+
const [syncConfigsLoading, setSyncConfigsLoading] = useState(true);
|
|
37
56
|
const loadConfig = useCallback(async () => {
|
|
38
57
|
try {
|
|
39
58
|
setLoading(true);
|
|
@@ -44,9 +63,22 @@ const DashboardContent = ({ api }) => {
|
|
|
44
63
|
setLoading(false);
|
|
45
64
|
}
|
|
46
65
|
}, [api]);
|
|
66
|
+
const loadSyncConfigs = useCallback(async () => {
|
|
67
|
+
try {
|
|
68
|
+
setSyncConfigsLoading(true);
|
|
69
|
+
const result = await api.getGithubSyncConfigs();
|
|
70
|
+
setSyncConfigs(result.configs);
|
|
71
|
+
} catch {
|
|
72
|
+
} finally {
|
|
73
|
+
setSyncConfigsLoading(false);
|
|
74
|
+
}
|
|
75
|
+
}, [api]);
|
|
47
76
|
useEffect(() => {
|
|
48
77
|
loadConfig();
|
|
49
|
-
|
|
78
|
+
loadSyncConfigs();
|
|
79
|
+
api.triggerAutoSync().catch(() => {
|
|
80
|
+
});
|
|
81
|
+
}, [loadConfig, loadSyncConfigs, api]);
|
|
50
82
|
const handleUnmask = async () => {
|
|
51
83
|
if (!maskedInput.trim()) return;
|
|
52
84
|
setUnmaskError("");
|
|
@@ -62,44 +94,44 @@ const DashboardContent = ({ api }) => {
|
|
|
62
94
|
if (e.key === "Enter") handleUnmask();
|
|
63
95
|
};
|
|
64
96
|
if (loading) {
|
|
65
|
-
return /* @__PURE__ */
|
|
97
|
+
return /* @__PURE__ */ React__default.createElement(Typography, null, "Loading configuration...");
|
|
66
98
|
}
|
|
67
|
-
return /* @__PURE__ */
|
|
99
|
+
return /* @__PURE__ */ React__default.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React__default.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React__default.createElement(InfoCard, { title: "Configuration Status" }, config ? /* @__PURE__ */ React__default.createElement(Box, null, /* @__PURE__ */ React__default.createElement(
|
|
68
100
|
ConfigItem,
|
|
69
101
|
{
|
|
70
102
|
label: "Mode",
|
|
71
103
|
value: config.masked ? "Masked" : "Unmasked",
|
|
72
104
|
ok: true
|
|
73
105
|
}
|
|
74
|
-
), /* @__PURE__ */
|
|
106
|
+
), /* @__PURE__ */ React__default.createElement(
|
|
75
107
|
ConfigItem,
|
|
76
108
|
{
|
|
77
109
|
label: "Salt",
|
|
78
110
|
value: config.saltConfigured ? "Configured" : "Not configured",
|
|
79
111
|
ok: config.saltConfigured
|
|
80
112
|
}
|
|
81
|
-
), /* @__PURE__ */
|
|
113
|
+
), /* @__PURE__ */ React__default.createElement(
|
|
82
114
|
ConfigItem,
|
|
83
115
|
{
|
|
84
116
|
label: "API Endpoint",
|
|
85
117
|
value: config.apiEndpointConfigured ? "Configured" : "Not configured",
|
|
86
118
|
ok: config.apiEndpointConfigured
|
|
87
119
|
}
|
|
88
|
-
), /* @__PURE__ */
|
|
120
|
+
), /* @__PURE__ */ React__default.createElement(
|
|
89
121
|
ConfigItem,
|
|
90
122
|
{
|
|
91
123
|
label: "API Token",
|
|
92
124
|
value: config.apiTokenConfigured ? "Configured" : "Not configured",
|
|
93
125
|
ok: config.apiTokenConfigured
|
|
94
126
|
}
|
|
95
|
-
), /* @__PURE__ */
|
|
127
|
+
), /* @__PURE__ */ React__default.createElement(
|
|
96
128
|
ConfigItem,
|
|
97
129
|
{
|
|
98
130
|
label: "Project ID",
|
|
99
131
|
value: config.projectIdConfigured ? "Configured" : "Not configured",
|
|
100
132
|
ok: config.projectIdConfigured
|
|
101
133
|
}
|
|
102
|
-
)) : /* @__PURE__ */
|
|
134
|
+
)) : /* @__PURE__ */ React__default.createElement(Typography, { color: "error" }, "Unable to load configuration. Is the backend plugin installed?"))), /* @__PURE__ */ React__default.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React__default.createElement(InfoCard, { title: "Developer Mappings" }, /* @__PURE__ */ React__default.createElement(Box, { display: "flex", alignItems: "center" }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "h3" }, config?.mappingCount ?? 0), /* @__PURE__ */ React__default.createElement(
|
|
103
135
|
Typography,
|
|
104
136
|
{
|
|
105
137
|
variant: "body1",
|
|
@@ -107,7 +139,14 @@ const DashboardContent = ({ api }) => {
|
|
|
107
139
|
color: "textSecondary"
|
|
108
140
|
},
|
|
109
141
|
"developer name mappings stored"
|
|
110
|
-
)), /* @__PURE__ */
|
|
142
|
+
)), /* @__PURE__ */ React__default.createElement(Box, { mt: 2 }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2", color: "textSecondary" }, "Upload developer names via CSV or configure GitHub auto-sync in the Settings tab to populate the mapping database.")))), /* @__PURE__ */ React__default.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React__default.createElement(InfoCard, { title: "GitHub Auto-Sync Configurations" }, syncConfigsLoading ? /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2", color: "textSecondary" }, "Loading sync configurations...") : syncConfigs.length === 0 ? /* @__PURE__ */ React__default.createElement(Typography, { className: classes.emptySync, variant: "body2" }, "No GitHub sync configurations. Go to Settings to register a GitHub App for automatic member syncing.") : /* @__PURE__ */ React__default.createElement(TableContainer, { component: Paper, variant: "outlined" }, /* @__PURE__ */ React__default.createElement(Table, { size: "small" }, /* @__PURE__ */ React__default.createElement(TableHead, null, /* @__PURE__ */ React__default.createElement(TableRow, null, /* @__PURE__ */ React__default.createElement(TableCell, null, "Organization"), /* @__PURE__ */ React__default.createElement(TableCell, null, "GitHub Host"), /* @__PURE__ */ React__default.createElement(TableCell, null, "Client ID"), /* @__PURE__ */ React__default.createElement(TableCell, null, "Status"), /* @__PURE__ */ React__default.createElement(TableCell, null, "Last Synced"), /* @__PURE__ */ React__default.createElement(TableCell, null, "Registered"))), /* @__PURE__ */ React__default.createElement(TableBody, null, syncConfigs.map((cfg) => /* @__PURE__ */ React__default.createElement(TableRow, { key: cfg.id }, /* @__PURE__ */ React__default.createElement(TableCell, null, /* @__PURE__ */ React__default.createElement("strong", null, cfg.org_name)), /* @__PURE__ */ React__default.createElement(TableCell, { style: { fontSize: "0.85em" } }, cfg.github_hostname ?? "github.com"), /* @__PURE__ */ React__default.createElement(TableCell, { style: { fontFamily: "monospace", fontSize: "0.85em" } }, cfg.app_client_id), /* @__PURE__ */ React__default.createElement(TableCell, null, /* @__PURE__ */ React__default.createElement(
|
|
143
|
+
Chip,
|
|
144
|
+
{
|
|
145
|
+
label: cfg.active ? "Active" : "Inactive",
|
|
146
|
+
size: "small",
|
|
147
|
+
className: cfg.active ? classes.activeChip : classes.inactiveChip
|
|
148
|
+
}
|
|
149
|
+
)), /* @__PURE__ */ React__default.createElement(TableCell, null, cfg.last_synced_at ? /* @__PURE__ */ React__default.createElement(Typography, { className: classes.lastSynced }, new Date(cfg.last_synced_at).toLocaleString()) : /* @__PURE__ */ React__default.createElement(Typography, { className: classes.lastSynced }, "Never")), /* @__PURE__ */ React__default.createElement(TableCell, null, /* @__PURE__ */ React__default.createElement(Typography, { className: classes.lastSynced }, new Date(cfg.created_at).toLocaleDateString()))))))))), /* @__PURE__ */ React__default.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React__default.createElement(InfoCard, { title: "Unmask Developer Name" }, /* @__PURE__ */ React__default.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__default.createElement(Box, { display: "flex", alignItems: "flex-start" }, /* @__PURE__ */ React__default.createElement(
|
|
111
150
|
TextField,
|
|
112
151
|
{
|
|
113
152
|
label: "Masked username",
|
|
@@ -120,7 +159,7 @@ const DashboardContent = ({ api }) => {
|
|
|
120
159
|
style: { minWidth: 300 },
|
|
121
160
|
inputProps: { maxLength: 16 }
|
|
122
161
|
}
|
|
123
|
-
), /* @__PURE__ */
|
|
162
|
+
), /* @__PURE__ */ React__default.createElement(
|
|
124
163
|
Button,
|
|
125
164
|
{
|
|
126
165
|
variant: "contained",
|
|
@@ -130,28 +169,28 @@ const DashboardContent = ({ api }) => {
|
|
|
130
169
|
style: { marginLeft: 12, height: 40 }
|
|
131
170
|
},
|
|
132
171
|
"Unmask"
|
|
133
|
-
)), unmaskResult && /* @__PURE__ */
|
|
172
|
+
)), unmaskResult && /* @__PURE__ */ React__default.createElement(Box, { className: classes.unmaskResult }, /* @__PURE__ */ React__default.createElement(Typography, { className: classes.resultLabel, variant: "body2" }, "Masked:"), /* @__PURE__ */ React__default.createElement(Typography, { className: classes.resultValue }, unmaskResult.maskedName), /* @__PURE__ */ React__default.createElement(Box, { mt: 1 }, /* @__PURE__ */ React__default.createElement(Typography, { className: classes.resultLabel, variant: "body2" }, "Real Name:"), /* @__PURE__ */ React__default.createElement(Typography, { className: classes.resultValue }, unmaskResult.realName ? /* @__PURE__ */ React__default.createElement(
|
|
134
173
|
Chip,
|
|
135
174
|
{
|
|
136
175
|
label: unmaskResult.realName,
|
|
137
176
|
color: "primary",
|
|
138
177
|
variant: "outlined"
|
|
139
178
|
}
|
|
140
|
-
) : /* @__PURE__ */
|
|
179
|
+
) : /* @__PURE__ */ React__default.createElement(
|
|
141
180
|
Chip,
|
|
142
181
|
{
|
|
143
182
|
label: "No mapping found",
|
|
144
183
|
color: "default",
|
|
145
184
|
variant: "outlined"
|
|
146
185
|
}
|
|
147
|
-
)))), unmaskError && /* @__PURE__ */
|
|
186
|
+
)))), unmaskError && /* @__PURE__ */ React__default.createElement(Box, { mt: 2 }, /* @__PURE__ */ React__default.createElement(Typography, { color: "error" }, unmaskError)))));
|
|
148
187
|
};
|
|
149
188
|
function ConfigItem({
|
|
150
189
|
label,
|
|
151
190
|
value,
|
|
152
191
|
ok
|
|
153
192
|
}) {
|
|
154
|
-
return /* @__PURE__ */
|
|
193
|
+
return /* @__PURE__ */ React__default.createElement(Box, { display: "flex", alignItems: "center", mb: 1 }, ok ? /* @__PURE__ */ React__default.createElement(CheckCircleIcon, { style: { color: "#4caf50", marginRight: 8 }, fontSize: "small" }) : /* @__PURE__ */ React__default.createElement(ErrorIcon, { style: { color: "#f44336", marginRight: 8 }, fontSize: "small" }), /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2" }, /* @__PURE__ */ React__default.createElement("strong", null, label, ":"), " ", value));
|
|
155
194
|
}
|
|
156
195
|
|
|
157
196
|
export { DashboardContent };
|
|
@@ -1 +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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"}
|
|
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 Table,\n TableBody,\n TableCell,\n TableContainer,\n TableHead,\n TableRow,\n Paper,\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, GithubSyncConfig } 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 activeChip: {\n backgroundColor: '#e8f5e9',\n color: '#2e7d32',\n },\n inactiveChip: {\n backgroundColor: '#f5f5f5',\n color: '#757575',\n },\n lastSynced: {\n fontSize: '0.75rem',\n color: theme.palette.text.secondary,\n },\n emptySync: {\n padding: theme.spacing(2),\n color: theme.palette.text.secondary,\n fontStyle: 'italic',\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 const [syncConfigs, setSyncConfigs] = useState<GithubSyncConfig[]>([]);\n const [syncConfigsLoading, setSyncConfigsLoading] = useState(true);\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 const loadSyncConfigs = useCallback(async () => {\n try {\n setSyncConfigsLoading(true);\n const result = await api.getGithubSyncConfigs();\n setSyncConfigs(result.configs);\n } catch {\n // failed silently\n } finally {\n setSyncConfigsLoading(false);\n }\n }, [api]);\n\n useEffect(() => {\n loadConfig();\n loadSyncConfigs();\n // Trigger auto-sync on page load (throttled to 24h server-side)\n api.triggerAutoSync().catch(() => {/* silent */});\n }, [loadConfig, loadSyncConfigs, api]);\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 via CSV or configure GitHub auto-sync in the\n Settings tab to populate the mapping database.\n </Typography>\n </Box>\n </InfoCard>\n </Grid>\n\n {/* GitHub Auto-Sync Configurations */}\n <Grid item xs={12}>\n <InfoCard title=\"GitHub Auto-Sync Configurations\">\n {syncConfigsLoading ? (\n <Typography variant=\"body2\" color=\"textSecondary\">\n Loading sync configurations...\n </Typography>\n ) : syncConfigs.length === 0 ? (\n <Typography className={classes.emptySync} variant=\"body2\">\n No GitHub sync configurations. Go to Settings to register a GitHub App for automatic member syncing.\n </Typography>\n ) : (\n <TableContainer component={Paper} variant=\"outlined\">\n <Table size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Organization</TableCell>\n <TableCell>GitHub Host</TableCell>\n <TableCell>Client ID</TableCell>\n <TableCell>Status</TableCell>\n <TableCell>Last Synced</TableCell>\n <TableCell>Registered</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {syncConfigs.map(cfg => (\n <TableRow key={cfg.id}>\n <TableCell>\n <strong>{cfg.org_name}</strong>\n </TableCell>\n <TableCell style={{ fontSize: '0.85em' }}>\n {cfg.github_hostname ?? 'github.com'}\n </TableCell>\n <TableCell style={{ fontFamily: 'monospace', fontSize: '0.85em' }}>\n {cfg.app_client_id}\n </TableCell>\n <TableCell>\n <Chip\n label={cfg.active ? 'Active' : 'Inactive'}\n size=\"small\"\n className={cfg.active ? classes.activeChip : classes.inactiveChip}\n />\n </TableCell>\n <TableCell>\n {cfg.last_synced_at ? (\n <Typography className={classes.lastSynced}>\n {new Date(cfg.last_synced_at).toLocaleString()}\n </Typography>\n ) : (\n <Typography className={classes.lastSynced}>Never</Typography>\n )}\n </TableCell>\n <TableCell>\n <Typography className={classes.lastSynced}>\n {new Date(cfg.created_at).toLocaleDateString()}\n </Typography>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </TableContainer>\n )}\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":["React"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
import { Page, Header, Content } from '@backstage/core-components';
|
|
3
3
|
import { useApi, fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
|
|
4
4
|
import { Tabs, Tab, Box } from '@material-ui/core';
|
|
@@ -7,10 +7,10 @@ import { DashboardContent } from './DashboardContent.esm.js';
|
|
|
7
7
|
import { SettingsContent } from './SettingsContent.esm.js';
|
|
8
8
|
|
|
9
9
|
const DevxpPage = () => {
|
|
10
|
-
const [tab, setTab] = useState(0);
|
|
10
|
+
const [tab, setTab] = React.useState(0);
|
|
11
11
|
const fetchApi = useApi(fetchApiRef);
|
|
12
12
|
const discoveryApi = useApi(discoveryApiRef);
|
|
13
|
-
const api = useMemo(
|
|
13
|
+
const api = React.useMemo(
|
|
14
14
|
() => new DevxpClient({ fetchApi, discoveryApi }),
|
|
15
15
|
[fetchApi, discoveryApi]
|
|
16
16
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DevxpPage.esm.js","sources":["../../src/components/DevxpPage.tsx"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"DevxpPage.esm.js","sources":["../../src/components/DevxpPage.tsx"],"sourcesContent":["import * as React 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] = React.useState(0);\n const fetchApi = useApi(fetchApiRef);\n const discoveryApi = useApi(discoveryApiRef);\n\n const api = React.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,KAAA,CAAM,SAAS,CAAC,CAAA;AACtC,EAAA,MAAM,QAAA,GAAW,OAAO,WAAW,CAAA;AACnC,EAAA,MAAM,YAAA,GAAe,OAAO,eAAe,CAAA;AAE3C,EAAA,MAAM,MAAM,KAAA,CAAM,OAAA;AAAA,IAChB,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,310 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Box, Typography, Collapse, Link, Button, TextField, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, Chip, Tooltip, IconButton } from '@material-ui/core';
|
|
3
|
+
import { makeStyles } from '@material-ui/core/styles';
|
|
4
|
+
import DeleteIcon from '@material-ui/icons/Delete';
|
|
5
|
+
import SyncIcon from '@material-ui/icons/Sync';
|
|
6
|
+
import AddIcon from '@material-ui/icons/Add';
|
|
7
|
+
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
|
8
|
+
import ExpandLessIcon from '@material-ui/icons/ExpandLess';
|
|
9
|
+
import { InfoCard } from '@backstage/core-components';
|
|
10
|
+
|
|
11
|
+
const useStyles = makeStyles((theme) => ({
|
|
12
|
+
form: {
|
|
13
|
+
display: "flex",
|
|
14
|
+
flexDirection: "column",
|
|
15
|
+
gap: theme.spacing(2),
|
|
16
|
+
marginTop: theme.spacing(2)
|
|
17
|
+
},
|
|
18
|
+
formRow: {
|
|
19
|
+
display: "flex",
|
|
20
|
+
gap: theme.spacing(2),
|
|
21
|
+
alignItems: "flex-start",
|
|
22
|
+
flexWrap: "wrap"
|
|
23
|
+
},
|
|
24
|
+
textField: {
|
|
25
|
+
flex: 1,
|
|
26
|
+
minWidth: 200
|
|
27
|
+
},
|
|
28
|
+
privateKeyField: {
|
|
29
|
+
width: "100%"
|
|
30
|
+
},
|
|
31
|
+
statusMessage: {
|
|
32
|
+
marginTop: theme.spacing(2),
|
|
33
|
+
padding: theme.spacing(1.5),
|
|
34
|
+
borderRadius: theme.shape.borderRadius
|
|
35
|
+
},
|
|
36
|
+
success: {
|
|
37
|
+
backgroundColor: "#e8f5e9",
|
|
38
|
+
color: "#2e7d32"
|
|
39
|
+
},
|
|
40
|
+
error: {
|
|
41
|
+
backgroundColor: "#ffebee",
|
|
42
|
+
color: "#c62828"
|
|
43
|
+
},
|
|
44
|
+
activeChip: {
|
|
45
|
+
backgroundColor: "#e8f5e9",
|
|
46
|
+
color: "#2e7d32"
|
|
47
|
+
},
|
|
48
|
+
inactiveChip: {
|
|
49
|
+
backgroundColor: "#f5f5f5",
|
|
50
|
+
color: "#757575"
|
|
51
|
+
},
|
|
52
|
+
guideBox: {
|
|
53
|
+
backgroundColor: theme.palette.background.default,
|
|
54
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
55
|
+
borderRadius: theme.shape.borderRadius,
|
|
56
|
+
padding: theme.spacing(2),
|
|
57
|
+
marginBottom: theme.spacing(2)
|
|
58
|
+
},
|
|
59
|
+
guideHeader: {
|
|
60
|
+
display: "flex",
|
|
61
|
+
alignItems: "center",
|
|
62
|
+
cursor: "pointer",
|
|
63
|
+
userSelect: "none"
|
|
64
|
+
},
|
|
65
|
+
syncingRow: {
|
|
66
|
+
opacity: 0.6
|
|
67
|
+
},
|
|
68
|
+
lastSynced: {
|
|
69
|
+
fontSize: "0.75rem",
|
|
70
|
+
color: theme.palette.text.secondary
|
|
71
|
+
},
|
|
72
|
+
emptyState: {
|
|
73
|
+
textAlign: "center",
|
|
74
|
+
padding: theme.spacing(3),
|
|
75
|
+
color: theme.palette.text.secondary
|
|
76
|
+
}
|
|
77
|
+
}));
|
|
78
|
+
const GithubSyncSection = ({ api, onSyncComplete }) => {
|
|
79
|
+
const classes = useStyles();
|
|
80
|
+
const [configs, setConfigs] = React.useState([]);
|
|
81
|
+
const [loading, setLoading] = React.useState(true);
|
|
82
|
+
const [showForm, setShowForm] = React.useState(false);
|
|
83
|
+
const [showGuide, setShowGuide] = React.useState(false);
|
|
84
|
+
const [syncingIds, setSyncingIds] = React.useState(/* @__PURE__ */ new Set());
|
|
85
|
+
const [statusMessage, setStatusMessage] = React.useState(null);
|
|
86
|
+
const [orgName, setOrgName] = React.useState("");
|
|
87
|
+
const [githubHostname, setGithubHostname] = React.useState("github.com");
|
|
88
|
+
const [appClientId, setAppClientId] = React.useState("");
|
|
89
|
+
const [appPrivateKey, setAppPrivateKey] = React.useState("");
|
|
90
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
91
|
+
const loadConfigs = React.useCallback(async () => {
|
|
92
|
+
try {
|
|
93
|
+
setLoading(true);
|
|
94
|
+
const result = await api.getGithubSyncConfigs();
|
|
95
|
+
setConfigs(result.configs);
|
|
96
|
+
} catch {
|
|
97
|
+
} finally {
|
|
98
|
+
setLoading(false);
|
|
99
|
+
}
|
|
100
|
+
}, [api]);
|
|
101
|
+
React.useEffect(() => {
|
|
102
|
+
loadConfigs();
|
|
103
|
+
}, [loadConfigs]);
|
|
104
|
+
const handleCreate = async () => {
|
|
105
|
+
if (!orgName.trim() || !appClientId.trim() || !appPrivateKey.trim()) return;
|
|
106
|
+
setSubmitting(true);
|
|
107
|
+
setStatusMessage(null);
|
|
108
|
+
try {
|
|
109
|
+
const hostname = githubHostname.trim() || "github.com";
|
|
110
|
+
await api.createGithubSyncConfig(orgName.trim(), hostname, appClientId.trim(), appPrivateKey.trim());
|
|
111
|
+
setStatusMessage({ text: `GitHub sync registered for org "${orgName}" on "${hostname}"`, isError: false });
|
|
112
|
+
setOrgName("");
|
|
113
|
+
setGithubHostname("github.com");
|
|
114
|
+
setAppClientId("");
|
|
115
|
+
setAppPrivateKey("");
|
|
116
|
+
setShowForm(false);
|
|
117
|
+
await loadConfigs();
|
|
118
|
+
} catch (e) {
|
|
119
|
+
setStatusMessage({ text: e.message || "Failed to register sync config", isError: true });
|
|
120
|
+
} finally {
|
|
121
|
+
setSubmitting(false);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const handleToggle = async (config) => {
|
|
125
|
+
try {
|
|
126
|
+
await api.toggleGithubSyncConfig(config.id, !config.active);
|
|
127
|
+
await loadConfigs();
|
|
128
|
+
} catch (e) {
|
|
129
|
+
setStatusMessage({ text: e.message || "Failed to update config", isError: true });
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const handleDelete = async (id) => {
|
|
133
|
+
try {
|
|
134
|
+
await api.deleteGithubSyncConfig(id);
|
|
135
|
+
await loadConfigs();
|
|
136
|
+
} catch (e) {
|
|
137
|
+
setStatusMessage({ text: e.message || "Failed to delete config", isError: true });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const handleSync = async (id) => {
|
|
141
|
+
setSyncingIds((prev) => new Set(prev).add(id));
|
|
142
|
+
setStatusMessage(null);
|
|
143
|
+
try {
|
|
144
|
+
const result = await api.syncGithubConfig(id);
|
|
145
|
+
if (result.error) {
|
|
146
|
+
setStatusMessage({ text: result.error, isError: true });
|
|
147
|
+
} else {
|
|
148
|
+
setStatusMessage({
|
|
149
|
+
text: `Synced ${result.count} members from "${result.orgName}"`,
|
|
150
|
+
isError: false
|
|
151
|
+
});
|
|
152
|
+
await loadConfigs();
|
|
153
|
+
onSyncComplete?.();
|
|
154
|
+
}
|
|
155
|
+
} catch (e) {
|
|
156
|
+
setStatusMessage({ text: e.message || "Sync failed", isError: true });
|
|
157
|
+
} finally {
|
|
158
|
+
setSyncingIds((prev) => {
|
|
159
|
+
const next = new Set(prev);
|
|
160
|
+
next.delete(id);
|
|
161
|
+
return next;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const githubAppCreateUrl = "https://github.com/settings/apps/new?name=devxp-member-sync&url=https%3A%2F%2Fdevxp.net&public=false&members=read";
|
|
166
|
+
return /* @__PURE__ */ React.createElement(InfoCard, { title: "GitHub Organization Auto-Sync" }, /* @__PURE__ */ React.createElement(
|
|
167
|
+
Box,
|
|
168
|
+
{
|
|
169
|
+
className: classes.guideBox,
|
|
170
|
+
onClick: () => setShowGuide((v) => !v)
|
|
171
|
+
},
|
|
172
|
+
/* @__PURE__ */ React.createElement(Box, { className: classes.guideHeader }, showGuide ? /* @__PURE__ */ React.createElement(ExpandLessIcon, { fontSize: "small" }) : /* @__PURE__ */ React.createElement(ExpandMoreIcon, { fontSize: "small" }), /* @__PURE__ */ React.createElement(Typography, { variant: "body2", style: { marginLeft: 4, fontWeight: 600 } }, "How to set up a GitHub App for member sync")),
|
|
173
|
+
/* @__PURE__ */ React.createElement(Collapse, { in: showGuide }, /* @__PURE__ */ React.createElement(Box, { mt: 1 }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", paragraph: true }, "DevXP syncs GitHub organization members automatically using a", " ", /* @__PURE__ */ React.createElement(Link, { href: "https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps", target: "_blank", rel: "noopener noreferrer" }, "GitHub App"), ". The app needs ", /* @__PURE__ */ React.createElement("strong", null, "Read-only"), " access to ", /* @__PURE__ */ React.createElement("strong", null, "Organization members"), "."), /* @__PURE__ */ React.createElement(Typography, { variant: "body2", component: "div" }, /* @__PURE__ */ React.createElement("strong", null, "Steps:"), /* @__PURE__ */ React.createElement("ol", null, /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link, { href: githubAppCreateUrl, target: "_blank", rel: "noopener noreferrer" }, "Click here to pre-configure and create your GitHub App"), " ", "\u2014 this link pre-fills the name and required permissions."), /* @__PURE__ */ React.createElement("li", null, "On the creation page, scroll to ", /* @__PURE__ */ React.createElement("em", null, "Organization permissions"), " and verify", " ", /* @__PURE__ */ React.createElement("strong", null, "Members \u2192 Read-only"), " is set."), /* @__PURE__ */ React.createElement("li", null, "After creating the app, note down the ", /* @__PURE__ */ React.createElement("strong", null, "Client ID"), " from the app's", " ", /* @__PURE__ */ React.createElement("em", null, "General"), " tab."), /* @__PURE__ */ React.createElement("li", null, "Scroll to ", /* @__PURE__ */ React.createElement("em", null, "Private keys"), " and click ", /* @__PURE__ */ React.createElement("strong", null, "Generate a private key"), ". Download the ", /* @__PURE__ */ React.createElement("code", null, ".pem"), " file and paste its full contents below."), /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link, { href: "https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app", target: "_blank", rel: "noopener noreferrer" }, "Install the app"), " ", "in your GitHub organization (Settings \u2192 Developer settings \u2192 GitHub Apps \u2192 Install)."))), /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, /* @__PURE__ */ React.createElement(Link, { href: "https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app", target: "_blank", rel: "noopener noreferrer" }, "Full GitHub App documentation \u2192"))))
|
|
174
|
+
), statusMessage && /* @__PURE__ */ React.createElement(
|
|
175
|
+
Box,
|
|
176
|
+
{
|
|
177
|
+
className: `${classes.statusMessage} ${statusMessage.isError ? classes.error : classes.success}`,
|
|
178
|
+
mb: 2
|
|
179
|
+
},
|
|
180
|
+
/* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, statusMessage.text)
|
|
181
|
+
), !showForm ? /* @__PURE__ */ React.createElement(
|
|
182
|
+
Button,
|
|
183
|
+
{
|
|
184
|
+
variant: "outlined",
|
|
185
|
+
color: "primary",
|
|
186
|
+
startIcon: /* @__PURE__ */ React.createElement(AddIcon, null),
|
|
187
|
+
onClick: () => {
|
|
188
|
+
setShowForm(true);
|
|
189
|
+
setStatusMessage(null);
|
|
190
|
+
},
|
|
191
|
+
size: "small"
|
|
192
|
+
},
|
|
193
|
+
"Register GitHub App"
|
|
194
|
+
) : /* @__PURE__ */ React.createElement(Box, { className: classes.form }, /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle2" }, "Register new GitHub App sync"), /* @__PURE__ */ React.createElement(Box, { className: classes.formRow }, /* @__PURE__ */ React.createElement(
|
|
195
|
+
TextField,
|
|
196
|
+
{
|
|
197
|
+
label: "GitHub Org Name",
|
|
198
|
+
value: orgName,
|
|
199
|
+
onChange: (e) => setOrgName(e.target.value),
|
|
200
|
+
variant: "outlined",
|
|
201
|
+
size: "small",
|
|
202
|
+
className: classes.textField,
|
|
203
|
+
placeholder: "my-organization",
|
|
204
|
+
helperText: "Your GitHub organization's login name"
|
|
205
|
+
}
|
|
206
|
+
), /* @__PURE__ */ React.createElement(
|
|
207
|
+
TextField,
|
|
208
|
+
{
|
|
209
|
+
label: "GitHub Hostname",
|
|
210
|
+
value: githubHostname,
|
|
211
|
+
onChange: (e) => setGithubHostname(e.target.value),
|
|
212
|
+
variant: "outlined",
|
|
213
|
+
size: "small",
|
|
214
|
+
className: classes.textField,
|
|
215
|
+
placeholder: "github.com",
|
|
216
|
+
helperText: 'Use "github.com" or your GitHub Enterprise Server hostname (e.g. github.acme.com)'
|
|
217
|
+
}
|
|
218
|
+
)), /* @__PURE__ */ React.createElement(Box, { className: classes.formRow }, /* @__PURE__ */ React.createElement(
|
|
219
|
+
TextField,
|
|
220
|
+
{
|
|
221
|
+
label: "App Client ID",
|
|
222
|
+
value: appClientId,
|
|
223
|
+
onChange: (e) => setAppClientId(e.target.value),
|
|
224
|
+
variant: "outlined",
|
|
225
|
+
size: "small",
|
|
226
|
+
className: classes.textField,
|
|
227
|
+
placeholder: "Iv1.a1b2c3d4e5f67890",
|
|
228
|
+
helperText: "Found on the GitHub App's General tab"
|
|
229
|
+
}
|
|
230
|
+
)), /* @__PURE__ */ React.createElement(
|
|
231
|
+
TextField,
|
|
232
|
+
{
|
|
233
|
+
label: "App Private Key (PEM)",
|
|
234
|
+
value: appPrivateKey,
|
|
235
|
+
onChange: (e) => setAppPrivateKey(e.target.value),
|
|
236
|
+
variant: "outlined",
|
|
237
|
+
size: "small",
|
|
238
|
+
multiline: true,
|
|
239
|
+
rows: 6,
|
|
240
|
+
className: classes.privateKeyField,
|
|
241
|
+
placeholder: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
|
|
242
|
+
helperText: "Paste the full contents of the .pem file downloaded from GitHub"
|
|
243
|
+
}
|
|
244
|
+
), /* @__PURE__ */ React.createElement(Box, { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React.createElement(
|
|
245
|
+
Button,
|
|
246
|
+
{
|
|
247
|
+
variant: "contained",
|
|
248
|
+
color: "primary",
|
|
249
|
+
onClick: handleCreate,
|
|
250
|
+
disabled: submitting || !orgName.trim() || !appClientId.trim() || !appPrivateKey.trim()
|
|
251
|
+
},
|
|
252
|
+
submitting ? "Registering..." : "Register"
|
|
253
|
+
), /* @__PURE__ */ React.createElement(
|
|
254
|
+
Button,
|
|
255
|
+
{
|
|
256
|
+
variant: "outlined",
|
|
257
|
+
onClick: () => {
|
|
258
|
+
setShowForm(false);
|
|
259
|
+
setOrgName("");
|
|
260
|
+
setGithubHostname("github.com");
|
|
261
|
+
setAppClientId("");
|
|
262
|
+
setAppPrivateKey("");
|
|
263
|
+
},
|
|
264
|
+
disabled: submitting,
|
|
265
|
+
style: { marginLeft: 8 }
|
|
266
|
+
},
|
|
267
|
+
"Cancel"
|
|
268
|
+
))), /* @__PURE__ */ React.createElement(Box, { mt: 3 }, loading ? /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "Loading configurations...") : configs.length === 0 ? /* @__PURE__ */ React.createElement(Box, { className: classes.emptyState }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, "No GitHub sync configurations yet. Register a GitHub App above to enable automatic member syncing.")) : /* @__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, "Organization"), /* @__PURE__ */ React.createElement(TableCell, null, "GitHub Host"), /* @__PURE__ */ React.createElement(TableCell, null, "Client ID"), /* @__PURE__ */ React.createElement(TableCell, null, "Status"), /* @__PURE__ */ React.createElement(TableCell, null, "Last Synced"), /* @__PURE__ */ React.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React.createElement(TableBody, null, configs.map((cfg) => {
|
|
269
|
+
const isSyncing = syncingIds.has(cfg.id);
|
|
270
|
+
return /* @__PURE__ */ React.createElement(TableRow, { key: cfg.id, className: isSyncing ? classes.syncingRow : void 0 }, /* @__PURE__ */ React.createElement(TableCell, null, /* @__PURE__ */ React.createElement("strong", null, cfg.org_name)), /* @__PURE__ */ React.createElement(TableCell, { style: { fontSize: "0.85em" } }, cfg.github_hostname ?? "github.com"), /* @__PURE__ */ React.createElement(TableCell, { style: { fontFamily: "monospace", fontSize: "0.85em" } }, cfg.app_client_id), /* @__PURE__ */ React.createElement(TableCell, null, /* @__PURE__ */ React.createElement(
|
|
271
|
+
Chip,
|
|
272
|
+
{
|
|
273
|
+
label: cfg.active ? "Active" : "Inactive",
|
|
274
|
+
size: "small",
|
|
275
|
+
className: cfg.active ? classes.activeChip : classes.inactiveChip,
|
|
276
|
+
onClick: () => handleToggle(cfg),
|
|
277
|
+
clickable: true
|
|
278
|
+
}
|
|
279
|
+
)), /* @__PURE__ */ React.createElement(TableCell, null, cfg.last_synced_at ? /* @__PURE__ */ React.createElement(Typography, { className: classes.lastSynced }, new Date(cfg.last_synced_at).toLocaleString()) : /* @__PURE__ */ React.createElement(Typography, { className: classes.lastSynced }, "Never")), /* @__PURE__ */ React.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React.createElement(Tooltip, { title: "Sync now" }, /* @__PURE__ */ React.createElement("span", null, /* @__PURE__ */ React.createElement(
|
|
280
|
+
IconButton,
|
|
281
|
+
{
|
|
282
|
+
size: "small",
|
|
283
|
+
onClick: () => handleSync(cfg.id),
|
|
284
|
+
disabled: isSyncing,
|
|
285
|
+
"aria-label": "Sync now"
|
|
286
|
+
},
|
|
287
|
+
/* @__PURE__ */ React.createElement(
|
|
288
|
+
SyncIcon,
|
|
289
|
+
{
|
|
290
|
+
fontSize: "small",
|
|
291
|
+
style: isSyncing ? { animation: "spin 1s linear infinite" } : void 0
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
))), /* @__PURE__ */ React.createElement(Tooltip, { title: "Delete configuration" }, /* @__PURE__ */ React.createElement(
|
|
295
|
+
IconButton,
|
|
296
|
+
{
|
|
297
|
+
size: "small",
|
|
298
|
+
onClick: () => handleDelete(cfg.id),
|
|
299
|
+
disabled: isSyncing,
|
|
300
|
+
"aria-label": "Delete config"
|
|
301
|
+
},
|
|
302
|
+
/* @__PURE__ */ React.createElement(DeleteIcon, { fontSize: "small" })
|
|
303
|
+
))));
|
|
304
|
+
}))))), /* @__PURE__ */ React.createElement("style", null, `
|
|
305
|
+
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
306
|
+
`));
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export { GithubSyncSection };
|
|
310
|
+
//# sourceMappingURL=GithubSyncSection.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GithubSyncSection.esm.js","sources":["../../src/components/GithubSyncSection.tsx"],"sourcesContent":["import * as React 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 TextField,\n Chip,\n Tooltip,\n Collapse,\n Link,\n} from '@material-ui/core';\nimport { makeStyles } from '@material-ui/core/styles';\nimport DeleteIcon from '@material-ui/icons/Delete';\nimport SyncIcon from '@material-ui/icons/Sync';\nimport AddIcon from '@material-ui/icons/Add';\nimport ExpandMoreIcon from '@material-ui/icons/ExpandMore';\nimport ExpandLessIcon from '@material-ui/icons/ExpandLess';\nimport { InfoCard } from '@backstage/core-components';\nimport type { DevxpApi } from '../api';\nimport type { GithubSyncConfig } from '../types';\n\nconst useStyles = makeStyles(theme => ({\n form: {\n display: 'flex',\n flexDirection: 'column',\n gap: theme.spacing(2),\n marginTop: theme.spacing(2),\n },\n formRow: {\n display: 'flex',\n gap: theme.spacing(2),\n alignItems: 'flex-start',\n flexWrap: 'wrap',\n },\n textField: {\n flex: 1,\n minWidth: 200,\n },\n privateKeyField: {\n width: '100%',\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 activeChip: {\n backgroundColor: '#e8f5e9',\n color: '#2e7d32',\n },\n inactiveChip: {\n backgroundColor: '#f5f5f5',\n color: '#757575',\n },\n guideBox: {\n backgroundColor: theme.palette.background.default,\n border: `1px solid ${theme.palette.divider}`,\n borderRadius: theme.shape.borderRadius,\n padding: theme.spacing(2),\n marginBottom: theme.spacing(2),\n },\n guideHeader: {\n display: 'flex',\n alignItems: 'center',\n cursor: 'pointer',\n userSelect: 'none',\n },\n syncingRow: {\n opacity: 0.6,\n },\n lastSynced: {\n fontSize: '0.75rem',\n color: theme.palette.text.secondary,\n },\n emptyState: {\n textAlign: 'center',\n padding: theme.spacing(3),\n color: theme.palette.text.secondary,\n },\n}));\n\ninterface GithubSyncSectionProps {\n api: DevxpApi;\n onSyncComplete?: () => void;\n}\n\nexport const GithubSyncSection = ({ api, onSyncComplete }: GithubSyncSectionProps) => {\n const classes = useStyles();\n const [configs, setConfigs] = React.useState<GithubSyncConfig[]>([]);\n const [loading, setLoading] = React.useState(true);\n const [showForm, setShowForm] = React.useState(false);\n const [showGuide, setShowGuide] = React.useState(false);\n const [syncingIds, setSyncingIds] = React.useState<Set<number>>(new Set());\n const [statusMessage, setStatusMessage] = React.useState<{ text: string; isError: boolean } | null>(null);\n\n // Form state\n const [orgName, setOrgName] = React.useState('');\n const [githubHostname, setGithubHostname] = React.useState('github.com');\n const [appClientId, setAppClientId] = React.useState('');\n const [appPrivateKey, setAppPrivateKey] = React.useState('');\n const [submitting, setSubmitting] = React.useState(false);\n\n const loadConfigs = React.useCallback(async () => {\n try {\n setLoading(true);\n const result = await api.getGithubSyncConfigs();\n setConfigs(result.configs);\n } catch {\n // failed silently\n } finally {\n setLoading(false);\n }\n }, [api]);\n\n React.useEffect(() => {\n loadConfigs();\n }, [loadConfigs]);\n\n const handleCreate = async () => {\n if (!orgName.trim() || !appClientId.trim() || !appPrivateKey.trim()) return;\n setSubmitting(true);\n setStatusMessage(null);\n try {\n const hostname = githubHostname.trim() || 'github.com';\n await api.createGithubSyncConfig(orgName.trim(), hostname, appClientId.trim(), appPrivateKey.trim());\n setStatusMessage({ text: `GitHub sync registered for org \"${orgName}\" on \"${hostname}\"`, isError: false });\n setOrgName('');\n setGithubHostname('github.com');\n setAppClientId('');\n setAppPrivateKey('');\n setShowForm(false);\n await loadConfigs();\n } catch (e: any) {\n setStatusMessage({ text: e.message || 'Failed to register sync config', isError: true });\n } finally {\n setSubmitting(false);\n }\n };\n\n const handleToggle = async (config: GithubSyncConfig) => {\n try {\n await api.toggleGithubSyncConfig(config.id, !config.active);\n await loadConfigs();\n } catch (e: any) {\n setStatusMessage({ text: e.message || 'Failed to update config', isError: true });\n }\n };\n\n const handleDelete = async (id: number) => {\n try {\n await api.deleteGithubSyncConfig(id);\n await loadConfigs();\n } catch (e: any) {\n setStatusMessage({ text: e.message || 'Failed to delete config', isError: true });\n }\n };\n\n const handleSync = async (id: number) => {\n setSyncingIds(prev => new Set(prev).add(id));\n setStatusMessage(null);\n try {\n const result = await api.syncGithubConfig(id);\n if (result.error) {\n setStatusMessage({ text: result.error, isError: true });\n } else {\n setStatusMessage({\n text: `Synced ${result.count} members from \"${result.orgName}\"`,\n isError: false,\n });\n await loadConfigs();\n onSyncComplete?.();\n }\n } catch (e: any) {\n setStatusMessage({ text: e.message || 'Sync failed', isError: true });\n } finally {\n setSyncingIds(prev => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n }\n };\n\n // Preconfigured GitHub App creation URL with required permissions\n const githubAppCreateUrl =\n 'https://github.com/settings/apps/new?' +\n 'name=devxp-member-sync&' +\n 'url=https%3A%2F%2Fdevxp.net&' +\n 'public=false&' +\n 'members=read';\n\n return (\n <InfoCard title=\"GitHub Organization Auto-Sync\">\n {/* Setup Guide */}\n <Box\n className={classes.guideBox}\n onClick={() => setShowGuide(v => !v)}\n >\n <Box className={classes.guideHeader}>\n {showGuide ? <ExpandLessIcon fontSize=\"small\" /> : <ExpandMoreIcon fontSize=\"small\" />}\n <Typography variant=\"body2\" style={{ marginLeft: 4, fontWeight: 600 }}>\n How to set up a GitHub App for member sync\n </Typography>\n </Box>\n <Collapse in={showGuide}>\n <Box mt={1}>\n <Typography variant=\"body2\" paragraph>\n DevXP syncs GitHub organization members automatically using a{' '}\n <Link href=\"https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps\" target=\"_blank\" rel=\"noopener noreferrer\">\n GitHub App\n </Link>\n . The app needs <strong>Read-only</strong> access to <strong>Organization members</strong>.\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n <strong>Steps:</strong>\n <ol>\n <li>\n <Link href={githubAppCreateUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n Click here to pre-configure and create your GitHub App\n </Link>\n {' '}— this link pre-fills the name and required permissions.\n </li>\n <li>\n On the creation page, scroll to <em>Organization permissions</em> and verify{' '}\n <strong>Members → Read-only</strong> is set.\n </li>\n <li>\n After creating the app, note down the <strong>Client ID</strong> from the app's{' '}\n <em>General</em> tab.\n </li>\n <li>\n Scroll to <em>Private keys</em> and click <strong>Generate a private key</strong>.\n Download the <code>.pem</code> file and paste its full contents below.\n </li>\n <li>\n <Link href=\"https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app\" target=\"_blank\" rel=\"noopener noreferrer\">\n Install the app\n </Link>\n {' '}in your GitHub organization (Settings → Developer settings → GitHub Apps → Install).\n </li>\n </ol>\n </Typography>\n <Typography variant=\"body2\">\n <Link href=\"https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app\" target=\"_blank\" rel=\"noopener noreferrer\">\n Full GitHub App documentation →\n </Link>\n </Typography>\n </Box>\n </Collapse>\n </Box>\n\n {/* Status message */}\n {statusMessage && (\n <Box\n className={`${classes.statusMessage} ${statusMessage.isError ? classes.error : classes.success}`}\n mb={2}\n >\n <Typography variant=\"body2\">{statusMessage.text}</Typography>\n </Box>\n )}\n\n {/* Add new config button / form */}\n {!showForm ? (\n <Button\n variant=\"outlined\"\n color=\"primary\"\n startIcon={<AddIcon />}\n onClick={() => { setShowForm(true); setStatusMessage(null); }}\n size=\"small\"\n >\n Register GitHub App\n </Button>\n ) : (\n <Box className={classes.form}>\n <Typography variant=\"subtitle2\">Register new GitHub App sync</Typography>\n <Box className={classes.formRow}>\n <TextField\n label=\"GitHub Org Name\"\n value={orgName}\n onChange={e => setOrgName(e.target.value)}\n variant=\"outlined\"\n size=\"small\"\n className={classes.textField}\n placeholder=\"my-organization\"\n helperText=\"Your GitHub organization's login name\"\n />\n <TextField\n label=\"GitHub Hostname\"\n value={githubHostname}\n onChange={e => setGithubHostname(e.target.value)}\n variant=\"outlined\"\n size=\"small\"\n className={classes.textField}\n placeholder=\"github.com\"\n helperText='Use \"github.com\" or your GitHub Enterprise Server hostname (e.g. github.acme.com)'\n />\n </Box>\n <Box className={classes.formRow}>\n <TextField\n label=\"App Client ID\"\n value={appClientId}\n onChange={e => setAppClientId(e.target.value)}\n variant=\"outlined\"\n size=\"small\"\n className={classes.textField}\n placeholder=\"Iv1.a1b2c3d4e5f67890\"\n helperText=\"Found on the GitHub App's General tab\"\n />\n </Box>\n <TextField\n label=\"App Private Key (PEM)\"\n value={appPrivateKey}\n onChange={e => setAppPrivateKey(e.target.value)}\n variant=\"outlined\"\n size=\"small\"\n multiline\n rows={6}\n className={classes.privateKeyField}\n placeholder=\"-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----\"\n helperText=\"Paste the full contents of the .pem file downloaded from GitHub\"\n />\n <Box style={{ display: 'flex', gap: 8 }}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n onClick={handleCreate}\n disabled={submitting || !orgName.trim() || !appClientId.trim() || !appPrivateKey.trim()}\n >\n {submitting ? 'Registering...' : 'Register'}\n </Button>\n <Button\n variant=\"outlined\"\n onClick={() => { setShowForm(false); setOrgName(''); setGithubHostname('github.com'); setAppClientId(''); setAppPrivateKey(''); }}\n disabled={submitting}\n style={{ marginLeft: 8 }}\n >\n Cancel\n </Button>\n </Box>\n </Box>\n )}\n\n\n\n {/* Configs table */}\n <Box mt={3}>\n {loading ? (\n <Typography variant=\"body2\" color=\"textSecondary\">Loading configurations...</Typography>\n ) : configs.length === 0 ? (\n <Box className={classes.emptyState}>\n <Typography variant=\"body2\">\n No GitHub sync configurations yet. Register a GitHub App above to enable automatic member syncing.\n </Typography>\n </Box>\n ) : (\n <TableContainer component={Paper} variant=\"outlined\">\n <Table size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Organization</TableCell>\n <TableCell>GitHub Host</TableCell>\n <TableCell>Client ID</TableCell>\n <TableCell>Status</TableCell>\n <TableCell>Last Synced</TableCell>\n <TableCell align=\"right\">Actions</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {configs.map(cfg => {\n const isSyncing = syncingIds.has(cfg.id);\n return (\n <TableRow key={cfg.id} className={isSyncing ? classes.syncingRow : undefined}>\n <TableCell>\n <strong>{cfg.org_name}</strong>\n </TableCell>\n <TableCell style={{ fontSize: '0.85em' }}>\n {cfg.github_hostname ?? 'github.com'}\n </TableCell>\n <TableCell style={{ fontFamily: 'monospace', fontSize: '0.85em' }}>\n {cfg.app_client_id}\n </TableCell>\n <TableCell>\n <Chip\n label={cfg.active ? 'Active' : 'Inactive'}\n size=\"small\"\n className={cfg.active ? classes.activeChip : classes.inactiveChip}\n onClick={() => handleToggle(cfg)}\n clickable\n />\n </TableCell>\n <TableCell>\n {cfg.last_synced_at ? (\n <Typography className={classes.lastSynced}>\n {new Date(cfg.last_synced_at).toLocaleString()}\n </Typography>\n ) : (\n <Typography className={classes.lastSynced}>Never</Typography>\n )}\n </TableCell>\n <TableCell align=\"right\">\n <Tooltip title=\"Sync now\">\n <span>\n <IconButton\n size=\"small\"\n onClick={() => handleSync(cfg.id)}\n disabled={isSyncing}\n aria-label=\"Sync now\"\n >\n <SyncIcon\n fontSize=\"small\"\n style={isSyncing ? { animation: 'spin 1s linear infinite' } : undefined}\n />\n </IconButton>\n </span>\n </Tooltip>\n <Tooltip title=\"Delete configuration\">\n <IconButton\n size=\"small\"\n onClick={() => handleDelete(cfg.id)}\n disabled={isSyncing}\n aria-label=\"Delete config\"\n >\n <DeleteIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </TableContainer>\n )}\n </Box>\n\n <style>{`\n @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n `}</style>\n </InfoCard>\n );\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React__default, { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
2
|
import { Grid, Typography, Box, Button, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, IconButton } from '@material-ui/core';
|
|
3
3
|
import { makeStyles } from '@material-ui/core/styles';
|
|
4
4
|
import DeleteIcon from '@material-ui/icons/Delete';
|
|
5
5
|
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
|
6
6
|
import { InfoCard } from '@backstage/core-components';
|
|
7
|
+
import { GithubSyncSection } from './GithubSyncSection.esm.js';
|
|
7
8
|
|
|
8
9
|
const useStyles = makeStyles((theme) => ({
|
|
9
10
|
fileInput: {
|
|
@@ -100,7 +101,7 @@ const SettingsContent = ({ api }) => {
|
|
|
100
101
|
} catch {
|
|
101
102
|
}
|
|
102
103
|
};
|
|
103
|
-
return /* @__PURE__ */
|
|
104
|
+
return /* @__PURE__ */ React__default.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React__default.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React__default.createElement(GithubSyncSection, { api, onSyncComplete: loadMappings })), /* @__PURE__ */ React__default.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React__default.createElement(InfoCard, { title: "Upload Developer Names (CSV)" }, /* @__PURE__ */ React__default.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__default.createElement(Box, { display: "flex", alignItems: "center" }, /* @__PURE__ */ React__default.createElement(
|
|
104
105
|
"input",
|
|
105
106
|
{
|
|
106
107
|
ref: fileInputRef,
|
|
@@ -110,31 +111,31 @@ const SettingsContent = ({ api }) => {
|
|
|
110
111
|
onChange: handleFileSelect,
|
|
111
112
|
id: "devxp-csv-upload"
|
|
112
113
|
}
|
|
113
|
-
), /* @__PURE__ */
|
|
114
|
+
), /* @__PURE__ */ React__default.createElement("label", { htmlFor: "devxp-csv-upload" }, /* @__PURE__ */ React__default.createElement(Button, { variant: "outlined", component: "span" }, "Choose CSV File")), selectedFileName && /* @__PURE__ */ React__default.createElement(Typography, { className: classes.fileName, variant: "body2" }, selectedFileName), /* @__PURE__ */ React__default.createElement(
|
|
114
115
|
Button,
|
|
115
116
|
{
|
|
116
117
|
variant: "contained",
|
|
117
118
|
color: "primary",
|
|
118
119
|
onClick: handleUpload,
|
|
119
120
|
disabled: !fileContent || uploading,
|
|
120
|
-
startIcon: /* @__PURE__ */
|
|
121
|
+
startIcon: /* @__PURE__ */ React__default.createElement(CloudUploadIcon, null),
|
|
121
122
|
style: { marginLeft: 16 }
|
|
122
123
|
},
|
|
123
124
|
uploading ? "Processing..." : "Upload & Process"
|
|
124
|
-
)), statusMessage && /* @__PURE__ */
|
|
125
|
+
)), statusMessage && /* @__PURE__ */ React__default.createElement(
|
|
125
126
|
Box,
|
|
126
127
|
{
|
|
127
128
|
className: `${classes.statusMessage} ${statusMessage.isError ? classes.error : classes.success}`
|
|
128
129
|
},
|
|
129
|
-
/* @__PURE__ */
|
|
130
|
-
))), /* @__PURE__ */
|
|
130
|
+
/* @__PURE__ */ React__default.createElement(Typography, { variant: "body2" }, statusMessage.text)
|
|
131
|
+
))), /* @__PURE__ */ React__default.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React__default.createElement(InfoCard, { title: "Developer Mappings" }, loading ? /* @__PURE__ */ React__default.createElement(Typography, null, "Loading mappings...") : mappings.length === 0 ? /* @__PURE__ */ React__default.createElement(Box, { className: classes.emptyState }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body1" }, "No developer mappings yet. Upload a CSV file or sync via GitHub above to get started.")) : /* @__PURE__ */ React__default.createElement(TableContainer, { component: Paper, variant: "outlined" }, /* @__PURE__ */ React__default.createElement(Table, { size: "small" }, /* @__PURE__ */ React__default.createElement(TableHead, null, /* @__PURE__ */ React__default.createElement(TableRow, null, /* @__PURE__ */ React__default.createElement(TableCell, null, "Masked Name"), /* @__PURE__ */ React__default.createElement(TableCell, null, "Real Name"), /* @__PURE__ */ React__default.createElement(TableCell, null, "Created"), /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React__default.createElement(TableBody, null, mappings.map((m) => /* @__PURE__ */ React__default.createElement(TableRow, { key: m.masked_name }, /* @__PURE__ */ React__default.createElement(TableCell, { className: classes.maskedCell }, m.masked_name), /* @__PURE__ */ React__default.createElement(TableCell, null, m.real_name), /* @__PURE__ */ React__default.createElement(TableCell, null, new Date(m.created_at).toLocaleDateString()), /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React__default.createElement(
|
|
131
132
|
IconButton,
|
|
132
133
|
{
|
|
133
134
|
size: "small",
|
|
134
135
|
onClick: () => handleDelete(m.masked_name),
|
|
135
136
|
"aria-label": "Delete mapping"
|
|
136
137
|
},
|
|
137
|
-
/* @__PURE__ */
|
|
138
|
+
/* @__PURE__ */ React__default.createElement(DeleteIcon, { fontSize: "small" })
|
|
138
139
|
))))))))));
|
|
139
140
|
};
|
|
140
141
|
|
|
@@ -1 +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":"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"}
|
|
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';\nimport { GithubSyncSection } from './GithubSyncSection';\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 {/* GitHub Auto-Sync Section */}\n <Grid item xs={12}>\n <GithubSyncSection api={api} onSyncComplete={loadMappings} />\n </Grid>\n\n {/* CSV Upload Section */}\n <Grid item xs={12}>\n <InfoCard title=\"Upload Developer Names (CSV)\">\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 or sync via GitHub\n above to get 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":["React"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as react from 'react';
|
|
2
1
|
import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
|
|
2
|
+
import * as React from 'react';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
declare const _default: _backstage_frontend_plugin_api.OverridableFrontendPlugin<{}, {}, {
|
|
@@ -14,15 +14,15 @@ declare const _default: _backstage_frontend_plugin_api.OverridableFrontendPlugin
|
|
|
14
14
|
title?: string | undefined;
|
|
15
15
|
path?: string | undefined;
|
|
16
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", {
|
|
17
|
+
output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<React.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
|
|
18
18
|
optional: true;
|
|
19
|
-
}> | _backstage_frontend_plugin_api.ExtensionDataRef<
|
|
19
|
+
}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.title", {
|
|
20
20
|
optional: true;
|
|
21
21
|
}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
|
|
22
22
|
optional: true;
|
|
23
23
|
}>;
|
|
24
24
|
inputs: {
|
|
25
|
-
pages: _backstage_frontend_plugin_api.ExtensionInput<_backstage_frontend_plugin_api.ConfigurableExtensionDataRef<
|
|
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
26
|
optional: true;
|
|
27
27
|
}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.title", {
|
|
28
28
|
optional: true;
|
|
@@ -38,7 +38,7 @@ declare const _default: _backstage_frontend_plugin_api.OverridableFrontendPlugin
|
|
|
38
38
|
path: string;
|
|
39
39
|
title?: string;
|
|
40
40
|
icon?: _backstage_frontend_plugin_api.IconElement;
|
|
41
|
-
loader?: () => Promise<
|
|
41
|
+
loader?: () => Promise<React.JSX.Element>;
|
|
42
42
|
routeRef?: _backstage_frontend_plugin_api.RouteRef;
|
|
43
43
|
noHeader?: boolean;
|
|
44
44
|
};
|
package/dist/plugin.esm.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
1
2
|
import { PageBlueprint, createFrontendPlugin } from '@backstage/frontend-plugin-api';
|
|
2
3
|
|
|
3
4
|
const devxpPage = PageBlueprint.make({
|
|
4
5
|
params: {
|
|
5
6
|
path: "/devxp",
|
|
6
7
|
title: "Dev Intelligence",
|
|
7
|
-
loader: () => import('./components/DevxpPage.esm.js').then((m) =>
|
|
8
|
+
loader: () => import('./components/DevxpPage.esm.js').then((m) => React.createElement(m.DevxpPage, null))
|
|
8
9
|
}
|
|
9
10
|
});
|
|
10
11
|
var plugin = createFrontendPlugin({
|
package/dist/plugin.esm.js.map
CHANGED
|
@@ -1 +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 =>
|
|
1
|
+
{"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["import * as React from 'react';\nimport { 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 => React.createElement(m.DevxpPage, null)),\n },\n});\n\nexport default createFrontendPlugin({\n pluginId: 'devxp',\n extensions: [devxpPage],\n});\n"],"names":[],"mappings":";;;AAGA,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,KAAK,KAAA,CAAM,aAAA,CAAc,CAAA,CAAE,SAAA,EAAW,IAAI,CAAC;AAAA;AAEvF,CAAC,CAAA;AAED,aAAe,oBAAA,CAAqB;AAAA,EAClC,QAAA,EAAU,OAAA;AAAA,EACV,UAAA,EAAY,CAAC,SAAS;AACxB,CAAC,CAAA;;;;"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karimov-labs/backstage-plugin-devxp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Backstage frontend plugin for developer intelligence — masked identity management, CSV upload, and unmask tooling.",
|
|
5
|
-
"main": "
|
|
6
|
-
"
|
|
5
|
+
"main": "dist/index.esm.js",
|
|
6
|
+
"module": "dist/index.esm.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
7
8
|
"license": "Apache-2.0",
|
|
8
9
|
"author": "karimov-labs",
|
|
9
10
|
"homepage": "https://devxp.net",
|
|
@@ -19,10 +20,7 @@
|
|
|
19
20
|
"pluginId": "devxp"
|
|
20
21
|
},
|
|
21
22
|
"publishConfig": {
|
|
22
|
-
"access": "public"
|
|
23
|
-
"main": "dist/index.cjs.js",
|
|
24
|
-
"module": "dist/index.esm.js",
|
|
25
|
-
"types": "dist/index.d.ts"
|
|
23
|
+
"access": "public"
|
|
26
24
|
},
|
|
27
25
|
"scripts": {
|
|
28
26
|
"start": "backstage-cli package start",
|
package/src/index.ts
DELETED