@karimov-labs/backstage-plugin-devxp 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -9
- package/dist/api.esm.js +51 -0
- package/dist/api.esm.js.map +1 -1
- package/dist/components/DashboardContent.esm.js +42 -3
- package/dist/components/DashboardContent.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 +3 -2
- package/dist/components/SettingsContent.esm.js.map +1 -1
- package/package.json +2 -7
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
|
|
|
@@ -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
1
|
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
|
-
import { Typography, Grid, Box,
|
|
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("");
|
|
@@ -107,7 +139,14 @@ const DashboardContent = ({ api }) => {
|
|
|
107
139
|
color: "textSecondary"
|
|
108
140
|
},
|
|
109
141
|
"developer name mappings stored"
|
|
110
|
-
)), /* @__PURE__ */ React.createElement(Box, { mt: 2 }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "Upload developer names in the Settings tab to populate the mapping database.
|
|
142
|
+
)), /* @__PURE__ */ React.createElement(Box, { mt: 2 }, /* @__PURE__ */ React.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.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "GitHub Auto-Sync Configurations" }, syncConfigsLoading ? /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "Loading sync configurations...") : syncConfigs.length === 0 ? /* @__PURE__ */ React.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.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, null, "Registered"))), /* @__PURE__ */ React.createElement(TableBody, null, syncConfigs.map((cfg) => /* @__PURE__ */ React.createElement(TableRow, { key: cfg.id }, /* @__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(
|
|
143
|
+
Chip,
|
|
144
|
+
{
|
|
145
|
+
label: cfg.active ? "Active" : "Inactive",
|
|
146
|
+
size: "small",
|
|
147
|
+
className: cfg.active ? classes.activeChip : classes.inactiveChip
|
|
148
|
+
}
|
|
149
|
+
)), /* @__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, null, /* @__PURE__ */ React.createElement(Typography, { className: classes.lastSynced }, new Date(cfg.created_at).toLocaleDateString()))))))))), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Unmask Developer Name" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary", paragraph: true }, "Enter a masked developer username (16-character hex hash) to look up the original name from the mapping database."), /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "flex-start" }, /* @__PURE__ */ React.createElement(
|
|
111
150
|
TextField,
|
|
112
151
|
{
|
|
113
152
|
label: "Masked username",
|
|
@@ -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":[],"mappings":"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"}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } 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] = useState([]);
|
|
81
|
+
const [loading, setLoading] = useState(true);
|
|
82
|
+
const [showForm, setShowForm] = useState(false);
|
|
83
|
+
const [showGuide, setShowGuide] = useState(false);
|
|
84
|
+
const [syncingIds, setSyncingIds] = useState(/* @__PURE__ */ new Set());
|
|
85
|
+
const [statusMessage, setStatusMessage] = useState(null);
|
|
86
|
+
const [orgName, setOrgName] = useState("");
|
|
87
|
+
const [githubHostname, setGithubHostname] = useState("github.com");
|
|
88
|
+
const [appClientId, setAppClientId] = useState("");
|
|
89
|
+
const [appPrivateKey, setAppPrivateKey] = useState("");
|
|
90
|
+
const [submitting, setSubmitting] = useState(false);
|
|
91
|
+
const loadConfigs = 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
|
+
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 { useState, useEffect, useCallback } 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] = useState<GithubSyncConfig[]>([]);\n const [loading, setLoading] = useState(true);\n const [showForm, setShowForm] = useState(false);\n const [showGuide, setShowGuide] = useState(false);\n const [syncingIds, setSyncingIds] = useState<Set<number>>(new Set());\n const [statusMessage, setStatusMessage] = useState<{ text: string; isError: boolean } | null>(null);\n\n // Form state\n const [orgName, setOrgName] = useState('');\n const [githubHostname, setGithubHostname] = useState('github.com');\n const [appClientId, setAppClientId] = useState('');\n const [appPrivateKey, setAppPrivateKey] = useState('');\n const [submitting, setSubmitting] = useState(false);\n\n const loadConfigs = 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 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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"}
|
|
@@ -4,6 +4,7 @@ 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__ */ React.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Upload Developer Names" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary", paragraph: true }, "Upload a CSV file with developer names (one name per line). The system will compute SHA-256 hashes using the configured salt and store the masked-to-real name mappings. Duplicate names will be updated."), /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center" }, /* @__PURE__ */ React.createElement(
|
|
104
|
+
return /* @__PURE__ */ React.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(GithubSyncSection, { api, onSyncComplete: loadMappings })), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Upload Developer Names (CSV)" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary", paragraph: true }, "Upload a CSV file with developer names (one name per line). The system will compute SHA-256 hashes using the configured salt and store the masked-to-real name mappings. Duplicate names will be updated."), /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center" }, /* @__PURE__ */ React.createElement(
|
|
104
105
|
"input",
|
|
105
106
|
{
|
|
106
107
|
ref: fileInputRef,
|
|
@@ -127,7 +128,7 @@ const SettingsContent = ({ api }) => {
|
|
|
127
128
|
className: `${classes.statusMessage} ${statusMessage.isError ? classes.error : classes.success}`
|
|
128
129
|
},
|
|
129
130
|
/* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, statusMessage.text)
|
|
130
|
-
))), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Developer Mappings" }, loading ? /* @__PURE__ */ React.createElement(Typography, null, "Loading mappings...") : mappings.length === 0 ? /* @__PURE__ */ React.createElement(Box, { className: classes.emptyState }, /* @__PURE__ */ React.createElement(Typography, { variant: "body1" }, "No developer mappings yet. Upload a CSV file above to get started.")) : /* @__PURE__ */ React.createElement(TableContainer, { component: Paper, variant: "outlined" }, /* @__PURE__ */ React.createElement(Table, { size: "small" }, /* @__PURE__ */ React.createElement(TableHead, null, /* @__PURE__ */ React.createElement(TableRow, null, /* @__PURE__ */ React.createElement(TableCell, null, "Masked Name"), /* @__PURE__ */ React.createElement(TableCell, null, "Real Name"), /* @__PURE__ */ React.createElement(TableCell, null, "Created"), /* @__PURE__ */ React.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React.createElement(TableBody, null, mappings.map((m) => /* @__PURE__ */ React.createElement(TableRow, { key: m.masked_name }, /* @__PURE__ */ React.createElement(TableCell, { className: classes.maskedCell }, m.masked_name), /* @__PURE__ */ React.createElement(TableCell, null, m.real_name), /* @__PURE__ */ React.createElement(TableCell, null, new Date(m.created_at).toLocaleDateString()), /* @__PURE__ */ React.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React.createElement(
|
|
131
|
+
))), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Developer Mappings" }, loading ? /* @__PURE__ */ React.createElement(Typography, null, "Loading mappings...") : mappings.length === 0 ? /* @__PURE__ */ React.createElement(Box, { className: classes.emptyState }, /* @__PURE__ */ React.createElement(Typography, { variant: "body1" }, "No developer mappings yet. Upload a CSV file or sync via GitHub above to get started.")) : /* @__PURE__ */ React.createElement(TableContainer, { component: Paper, variant: "outlined" }, /* @__PURE__ */ React.createElement(Table, { size: "small" }, /* @__PURE__ */ React.createElement(TableHead, null, /* @__PURE__ */ React.createElement(TableRow, null, /* @__PURE__ */ React.createElement(TableCell, null, "Masked Name"), /* @__PURE__ */ React.createElement(TableCell, null, "Real Name"), /* @__PURE__ */ React.createElement(TableCell, null, "Created"), /* @__PURE__ */ React.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React.createElement(TableBody, null, mappings.map((m) => /* @__PURE__ */ React.createElement(TableRow, { key: m.masked_name }, /* @__PURE__ */ React.createElement(TableCell, { className: classes.maskedCell }, m.masked_name), /* @__PURE__ */ React.createElement(TableCell, null, m.real_name), /* @__PURE__ */ React.createElement(TableCell, null, new Date(m.created_at).toLocaleDateString()), /* @__PURE__ */ React.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React.createElement(
|
|
131
132
|
IconButton,
|
|
132
133
|
{
|
|
133
134
|
size: "small",
|
|
@@ -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":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karimov-labs/backstage-plugin-devxp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Backstage frontend plugin for developer intelligence — masked identity management, CSV upload, and unmask tooling.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"author": "karimov-labs",
|
|
9
|
-
"homepage": "https://
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "https://github.com/karimov-labs/backstage-plugin-devxp.git",
|
|
13
|
-
"directory": "plugins/devxp"
|
|
14
|
-
},
|
|
9
|
+
"homepage": "https://devxp.net",
|
|
15
10
|
"keywords": [
|
|
16
11
|
"backstage",
|
|
17
12
|
"plugin",
|