@karimov-labs/backstage-plugin-devxp 1.1.1 → 1.2.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.
@@ -0,0 +1,251 @@
1
+ import React__default, { useState, useCallback, useEffect } from 'react';
2
+ import { Box, TextField, IconButton, CircularProgress, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, Typography, LinearProgress, Button, Tooltip, Chip } from '@material-ui/core';
3
+ import { makeStyles } from '@material-ui/core/styles';
4
+ import SearchIcon from '@material-ui/icons/Search';
5
+ import ArrowBackIcon from '@material-ui/icons/ArrowBack';
6
+ import StorageIcon from '@material-ui/icons/Storage';
7
+ import PersonIcon from '@material-ui/icons/Person';
8
+ import { InfoCard, EmptyState } from '@backstage/core-components';
9
+
10
+ const CATEGORY_COLORS = {
11
+ "Frontend Engineering": "#e91e63",
12
+ "Backend Systems": "#3f51b5",
13
+ "Database & Data": "#9c27b0",
14
+ "Infrastructure (DevOps)": "#ff9800",
15
+ "Mobile Development": "#4caf50",
16
+ "AI & Machine Learning": "#607d8b",
17
+ "Testing & QA": "#00bcd4",
18
+ "Security & Auth": "#f44336",
19
+ "Distributed Systems": "#795548",
20
+ "Documentation": "#9e9e9e"
21
+ };
22
+ const useStyles = makeStyles((theme) => ({
23
+ header: {
24
+ display: "flex",
25
+ alignItems: "center",
26
+ gap: theme.spacing(1),
27
+ marginBottom: theme.spacing(2)
28
+ },
29
+ searchRow: {
30
+ display: "flex",
31
+ gap: theme.spacing(2),
32
+ marginBottom: theme.spacing(2),
33
+ alignItems: "center"
34
+ },
35
+ searchField: {
36
+ flex: 1,
37
+ maxWidth: 400
38
+ },
39
+ clickableRow: {
40
+ cursor: "pointer",
41
+ "&:hover": {
42
+ backgroundColor: theme.palette.action.hover
43
+ }
44
+ },
45
+ repoName: {
46
+ fontFamily: "monospace",
47
+ fontWeight: 600
48
+ },
49
+ pagination: {
50
+ display: "flex",
51
+ justifyContent: "space-between",
52
+ alignItems: "center",
53
+ padding: theme.spacing(1, 0),
54
+ marginTop: theme.spacing(1)
55
+ },
56
+ categoryChip: {
57
+ margin: theme.spacing(0.25),
58
+ fontSize: "0.65rem",
59
+ height: 18
60
+ },
61
+ scoreBar: {
62
+ height: 6,
63
+ borderRadius: 3,
64
+ marginTop: 4
65
+ },
66
+ sectionTitle: {
67
+ fontWeight: 600,
68
+ marginBottom: theme.spacing(1),
69
+ marginTop: theme.spacing(2)
70
+ },
71
+ statValue: {
72
+ fontWeight: 700,
73
+ fontSize: "1.5rem"
74
+ },
75
+ statLabel: {
76
+ color: theme.palette.text.secondary,
77
+ fontSize: "0.75rem"
78
+ },
79
+ statBox: {
80
+ textAlign: "center",
81
+ padding: theme.spacing(1.5)
82
+ }
83
+ }));
84
+ const RepositoriesContent = ({ api, onViewDeveloper }) => {
85
+ const [view, setView] = useState("list");
86
+ const [selectedRepo, setSelectedRepo] = useState("");
87
+ return /* @__PURE__ */ React__default.createElement(Box, null, view === "list" ? /* @__PURE__ */ React__default.createElement(
88
+ RepositoryList,
89
+ {
90
+ api,
91
+ onViewRepo: (repoName) => {
92
+ setSelectedRepo(repoName);
93
+ setView("details");
94
+ }
95
+ }
96
+ ) : /* @__PURE__ */ React__default.createElement(
97
+ RepositoryDetailsView,
98
+ {
99
+ api,
100
+ repoName: selectedRepo,
101
+ onBack: () => setView("list"),
102
+ onViewDeveloper
103
+ }
104
+ ));
105
+ };
106
+ const RepositoryList = ({ api, onViewRepo }) => {
107
+ const classes = useStyles();
108
+ const [repositories, setRepositories] = useState([]);
109
+ const [totalCount, setTotalCount] = useState(0);
110
+ const [totalPages, setTotalPages] = useState(1);
111
+ const [page, setPage] = useState(1);
112
+ const pageSize = 25;
113
+ const [loading, setLoading] = useState(true);
114
+ const [error, setError] = useState(null);
115
+ const [searchInput, setSearchInput] = useState("");
116
+ const [appliedSearch, setAppliedSearch] = useState("");
117
+ const load = useCallback(
118
+ async (pg, search) => {
119
+ setLoading(true);
120
+ setError(null);
121
+ try {
122
+ const data = await api.getRepositories({ page: pg, pageSize, searchQuery: search });
123
+ setRepositories(data.repositories);
124
+ setTotalCount(data.totalCount);
125
+ setTotalPages(data.totalPages);
126
+ } catch (e) {
127
+ setError(e.message ?? "Failed to load repositories");
128
+ } finally {
129
+ setLoading(false);
130
+ }
131
+ },
132
+ [api]
133
+ );
134
+ useEffect(() => {
135
+ load(page, appliedSearch);
136
+ }, [load, page, appliedSearch]);
137
+ const applySearch = () => {
138
+ setPage(1);
139
+ setAppliedSearch(searchInput);
140
+ };
141
+ const formatDate = (iso) => {
142
+ try {
143
+ return new Date(iso).toLocaleDateString(void 0, {
144
+ year: "numeric",
145
+ month: "short",
146
+ day: "numeric"
147
+ });
148
+ } catch {
149
+ return iso;
150
+ }
151
+ };
152
+ return /* @__PURE__ */ React__default.createElement(InfoCard, { title: `Repositories (${totalCount})`, noPadding: false }, /* @__PURE__ */ React__default.createElement(Box, { className: classes.searchRow }, /* @__PURE__ */ React__default.createElement(
153
+ TextField,
154
+ {
155
+ className: classes.searchField,
156
+ size: "small",
157
+ variant: "outlined",
158
+ label: "Search repositories",
159
+ value: searchInput,
160
+ onChange: (e) => setSearchInput(e.target.value),
161
+ onKeyDown: (e) => e.key === "Enter" && applySearch(),
162
+ InputProps: {
163
+ endAdornment: /* @__PURE__ */ React__default.createElement(IconButton, { size: "small", onClick: applySearch }, /* @__PURE__ */ React__default.createElement(SearchIcon, { fontSize: "small" }))
164
+ }
165
+ }
166
+ )), loading ? /* @__PURE__ */ React__default.createElement(Box, { display: "flex", justifyContent: "center", py: 4 }, /* @__PURE__ */ React__default.createElement(CircularProgress, null)) : error ? /* @__PURE__ */ React__default.createElement(EmptyState, { missing: "data", title: "Repositories unavailable", description: error }) : repositories.length === 0 ? /* @__PURE__ */ React__default.createElement(EmptyState, { missing: "data", title: "No repositories found", description: "Try a different search." }) : /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__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, "Repository"), /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, "Developers"), /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, "Events"), /* @__PURE__ */ React__default.createElement(TableCell, { style: { minWidth: 140 } }, "Avg Proficiency"), /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, "Last Activity"))), /* @__PURE__ */ React__default.createElement(TableBody, null, repositories.map((repo) => /* @__PURE__ */ React__default.createElement(
167
+ TableRow,
168
+ {
169
+ key: repo.repoName,
170
+ className: classes.clickableRow,
171
+ onClick: () => onViewRepo(repo.repoName)
172
+ },
173
+ /* @__PURE__ */ React__default.createElement(TableCell, null, /* @__PURE__ */ React__default.createElement(Box, { display: "flex", alignItems: "center", style: { gap: 8 } }, /* @__PURE__ */ React__default.createElement(StorageIcon, { fontSize: "small", color: "action" }), /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2", className: classes.repoName }, repo.repoName))),
174
+ /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2" }, repo.developerCount)),
175
+ /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2" }, repo.eventCount.toLocaleString())),
176
+ /* @__PURE__ */ React__default.createElement(TableCell, { style: { minWidth: 140 } }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2", style: { fontWeight: 600 } }, (repo.avgProficiency * 10).toFixed(1), "%"), /* @__PURE__ */ React__default.createElement(
177
+ LinearProgress,
178
+ {
179
+ variant: "determinate",
180
+ value: repo.avgProficiency * 10,
181
+ className: classes.scoreBar
182
+ }
183
+ )),
184
+ /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "caption", color: "textSecondary" }, formatDate(repo.lastActivity)))
185
+ ))))), /* @__PURE__ */ React__default.createElement(Box, { className: classes.pagination }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "caption", color: "textSecondary" }, "Showing ", (page - 1) * pageSize + 1, "\u2013", Math.min(page * pageSize, totalCount), " of ", totalCount), /* @__PURE__ */ React__default.createElement(Box, { display: "flex", alignItems: "center", style: { gap: 8 } }, /* @__PURE__ */ React__default.createElement(Button, { size: "small", disabled: page <= 1, onClick: () => setPage((p) => p - 1) }, "Prev"), /* @__PURE__ */ React__default.createElement(Typography, { variant: "caption" }, page, " / ", totalPages), /* @__PURE__ */ React__default.createElement(Button, { size: "small", disabled: page >= totalPages, onClick: () => setPage((p) => p + 1) }, "Next")))));
186
+ };
187
+ const RepositoryDetailsView = ({ api, repoName, onBack, onViewDeveloper }) => {
188
+ const classes = useStyles();
189
+ const [details, setDetails] = useState(null);
190
+ const [loading, setLoading] = useState(true);
191
+ const [error, setError] = useState(null);
192
+ useEffect(() => {
193
+ setLoading(true);
194
+ setError(null);
195
+ api.getRepositoryDetails(repoName).then(setDetails).catch((e) => setError(e.message ?? "Failed to load repository details")).finally(() => setLoading(false));
196
+ }, [api, repoName]);
197
+ const formatDate = (iso) => {
198
+ try {
199
+ return new Date(iso).toLocaleDateString(void 0, {
200
+ year: "numeric",
201
+ month: "short",
202
+ day: "numeric"
203
+ });
204
+ } catch {
205
+ return iso;
206
+ }
207
+ };
208
+ return /* @__PURE__ */ React__default.createElement(Box, null, /* @__PURE__ */ React__default.createElement(Box, { className: classes.header }, /* @__PURE__ */ React__default.createElement(Tooltip, { title: "Back to repositories" }, /* @__PURE__ */ React__default.createElement(IconButton, { size: "small", onClick: onBack }, /* @__PURE__ */ React__default.createElement(ArrowBackIcon, null))), /* @__PURE__ */ React__default.createElement(StorageIcon, { color: "action" }), /* @__PURE__ */ React__default.createElement(Typography, { variant: "h6", className: classes.repoName }, repoName)), loading ? /* @__PURE__ */ React__default.createElement(Box, { display: "flex", justifyContent: "center", py: 6 }, /* @__PURE__ */ React__default.createElement(CircularProgress, null)) : error ? /* @__PURE__ */ React__default.createElement(EmptyState, { missing: "data", title: "Details unavailable", description: error }) : details ? /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement(InfoCard, { title: "Overview" }, /* @__PURE__ */ React__default.createElement(Box, { display: "flex", justifyContent: "space-around", flexWrap: "wrap" }, /* @__PURE__ */ React__default.createElement(Box, { className: classes.statBox }, /* @__PURE__ */ React__default.createElement(Typography, { className: classes.statValue }, details.developerCount), /* @__PURE__ */ React__default.createElement(Typography, { className: classes.statLabel }, "Developers")), /* @__PURE__ */ React__default.createElement(Box, { className: classes.statBox }, /* @__PURE__ */ React__default.createElement(Typography, { className: classes.statValue }, details.eventCount.toLocaleString()), /* @__PURE__ */ React__default.createElement(Typography, { className: classes.statLabel }, "Skill Events")), /* @__PURE__ */ React__default.createElement(Box, { className: classes.statBox }, /* @__PURE__ */ React__default.createElement(Typography, { className: classes.statValue }, (details.avgProficiency * 10).toFixed(1), "%"), /* @__PURE__ */ React__default.createElement(Typography, { className: classes.statLabel }, "Avg Proficiency")), /* @__PURE__ */ React__default.createElement(Box, { className: classes.statBox }, /* @__PURE__ */ React__default.createElement(Typography, { className: classes.statValue, style: { fontSize: "1rem" } }, formatDate(details.lastActivity)), /* @__PURE__ */ React__default.createElement(Typography, { className: classes.statLabel }, "Last Activity")))), details.categoryBreakdown.length > 0 && /* @__PURE__ */ React__default.createElement(Box, { mt: 2 }, /* @__PURE__ */ React__default.createElement(InfoCard, { title: "Skill Categories" }, /* @__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, "Category"), /* @__PURE__ */ React__default.createElement(TableCell, { style: { minWidth: 160 } }, "Avg Proficiency"))), /* @__PURE__ */ React__default.createElement(TableBody, null, details.categoryBreakdown.map((cat) => /* @__PURE__ */ React__default.createElement(TableRow, { key: cat.category }, /* @__PURE__ */ React__default.createElement(TableCell, null, /* @__PURE__ */ React__default.createElement(
209
+ Chip,
210
+ {
211
+ label: cat.category,
212
+ size: "small",
213
+ className: classes.categoryChip,
214
+ style: {
215
+ backgroundColor: `${CATEGORY_COLORS[cat.category] ?? "#9e9e9e"}20`,
216
+ color: CATEGORY_COLORS[cat.category] ?? "#9e9e9e",
217
+ height: 20
218
+ }
219
+ }
220
+ )), /* @__PURE__ */ React__default.createElement(TableCell, { style: { minWidth: 160 } }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2", style: { fontWeight: 600 } }, (cat.averageProficiency * 10).toFixed(1), "%"), /* @__PURE__ */ React__default.createElement(
221
+ LinearProgress,
222
+ {
223
+ variant: "determinate",
224
+ value: cat.averageProficiency * 10,
225
+ className: classes.scoreBar,
226
+ style: { backgroundColor: `${CATEGORY_COLORS[cat.category] ?? "#9e9e9e"}30` }
227
+ }
228
+ ))))))))), details.topDevelopers.length > 0 && /* @__PURE__ */ React__default.createElement(Box, { mt: 2 }, /* @__PURE__ */ React__default.createElement(InfoCard, { title: "Top Developers" }, /* @__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, "#"), /* @__PURE__ */ React__default.createElement(TableCell, null, "Developer"), /* @__PURE__ */ React__default.createElement(TableCell, { style: { minWidth: 140 } }, "Avg Proficiency"), /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, "Skills"), onViewDeveloper && /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }))), /* @__PURE__ */ React__default.createElement(TableBody, null, details.topDevelopers.map((dev, idx) => /* @__PURE__ */ React__default.createElement(
229
+ TableRow,
230
+ {
231
+ key: dev.userId,
232
+ className: onViewDeveloper ? classes.clickableRow : void 0,
233
+ onClick: () => onViewDeveloper?.(dev.userId)
234
+ },
235
+ /* @__PURE__ */ React__default.createElement(TableCell, null, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2", color: "textSecondary" }, idx + 1)),
236
+ /* @__PURE__ */ React__default.createElement(TableCell, null, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2", style: { fontFamily: "monospace", fontWeight: 600 } }, dev.userId)),
237
+ /* @__PURE__ */ React__default.createElement(TableCell, { style: { minWidth: 140 } }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2", style: { fontWeight: 600 } }, (dev.averageProficiency * 10).toFixed(1), "%"), /* @__PURE__ */ React__default.createElement(
238
+ LinearProgress,
239
+ {
240
+ variant: "determinate",
241
+ value: dev.averageProficiency * 10,
242
+ className: classes.scoreBar
243
+ }
244
+ )),
245
+ /* @__PURE__ */ React__default.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React__default.createElement(Typography, { variant: "body2" }, dev.skillCount)),
246
+ onViewDeveloper && /* @__PURE__ */ React__default.createElement(TableCell, { align: "right", onClick: (e) => e.stopPropagation() }, /* @__PURE__ */ React__default.createElement(Tooltip, { title: "View developer details" }, /* @__PURE__ */ React__default.createElement(IconButton, { size: "small", onClick: () => onViewDeveloper(dev.userId) }, /* @__PURE__ */ React__default.createElement(PersonIcon, { fontSize: "small" }))))
247
+ )))))))) : null);
248
+ };
249
+
250
+ export { RepositoriesContent };
251
+ //# sourceMappingURL=RepositoriesContent.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RepositoriesContent.esm.js","sources":["../../src/components/RepositoriesContent.tsx"],"sourcesContent":["import React, { useState, useCallback, useEffect } from 'react';\nimport {\n Box,\n Typography,\n Table,\n TableBody,\n TableCell,\n TableContainer,\n TableHead,\n TableRow,\n Paper,\n TextField,\n Chip,\n IconButton,\n Button,\n CircularProgress,\n Tooltip,\n LinearProgress,\n} from '@material-ui/core';\nimport { makeStyles } from '@material-ui/core/styles';\nimport SearchIcon from '@material-ui/icons/Search';\nimport ArrowBackIcon from '@material-ui/icons/ArrowBack';\nimport StorageIcon from '@material-ui/icons/Storage';\nimport PersonIcon from '@material-ui/icons/Person';\nimport { InfoCard, EmptyState } from '@backstage/core-components';\nimport type { DevxpApi } from '../api';\nimport type { RepositoryStats, RepositoryDetails } from '../types';\n\nconst CATEGORY_COLORS: Record<string, string> = {\n 'Frontend Engineering': '#e91e63',\n 'Backend Systems': '#3f51b5',\n 'Database & Data': '#9c27b0',\n 'Infrastructure (DevOps)': '#ff9800',\n 'Mobile Development': '#4caf50',\n 'AI & Machine Learning': '#607d8b',\n 'Testing & QA': '#00bcd4',\n 'Security & Auth': '#f44336',\n 'Distributed Systems': '#795548',\n 'Documentation': '#9e9e9e',\n};\n\nconst useStyles = makeStyles(theme => ({\n header: {\n display: 'flex',\n alignItems: 'center',\n gap: theme.spacing(1),\n marginBottom: theme.spacing(2),\n },\n searchRow: {\n display: 'flex',\n gap: theme.spacing(2),\n marginBottom: theme.spacing(2),\n alignItems: 'center',\n },\n searchField: {\n flex: 1,\n maxWidth: 400,\n },\n clickableRow: {\n cursor: 'pointer',\n '&:hover': {\n backgroundColor: theme.palette.action.hover,\n },\n },\n repoName: {\n fontFamily: 'monospace',\n fontWeight: 600,\n },\n pagination: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n padding: theme.spacing(1, 0),\n marginTop: theme.spacing(1),\n },\n categoryChip: {\n margin: theme.spacing(0.25),\n fontSize: '0.65rem',\n height: 18,\n },\n scoreBar: {\n height: 6,\n borderRadius: 3,\n marginTop: 4,\n },\n sectionTitle: {\n fontWeight: 600,\n marginBottom: theme.spacing(1),\n marginTop: theme.spacing(2),\n },\n statValue: {\n fontWeight: 700,\n fontSize: '1.5rem',\n },\n statLabel: {\n color: theme.palette.text.secondary,\n fontSize: '0.75rem',\n },\n statBox: {\n textAlign: 'center',\n padding: theme.spacing(1.5),\n },\n}));\n\ninterface RepositoriesContentProps {\n api: DevxpApi;\n onViewDeveloper?: (userId: string) => void;\n}\n\nexport const RepositoriesContent = ({ api, onViewDeveloper }: RepositoriesContentProps) => {\n const [view, setView] = useState<'list' | 'details'>('list');\n const [selectedRepo, setSelectedRepo] = useState('');\n\n return (\n <Box>\n {view === 'list' ? (\n <RepositoryList\n api={api}\n onViewRepo={repoName => {\n setSelectedRepo(repoName);\n setView('details');\n }}\n />\n ) : (\n <RepositoryDetailsView\n api={api}\n repoName={selectedRepo}\n onBack={() => setView('list')}\n onViewDeveloper={onViewDeveloper}\n />\n )}\n </Box>\n );\n};\n\n// ─── Repository List ──────────────────────────────────────────────────────────\n\ninterface RepositoryListProps {\n api: DevxpApi;\n onViewRepo: (repoName: string) => void;\n}\n\nconst RepositoryList = ({ api, onViewRepo }: RepositoryListProps) => {\n const classes = useStyles();\n\n const [repositories, setRepositories] = useState<RepositoryStats[]>([]);\n const [totalCount, setTotalCount] = useState(0);\n const [totalPages, setTotalPages] = useState(1);\n const [page, setPage] = useState(1);\n const pageSize = 25;\n\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [searchInput, setSearchInput] = useState('');\n const [appliedSearch, setAppliedSearch] = useState('');\n\n const load = useCallback(\n async (pg: number, search: string) => {\n setLoading(true);\n setError(null);\n try {\n const data = await api.getRepositories({ page: pg, pageSize, searchQuery: search });\n setRepositories(data.repositories);\n setTotalCount(data.totalCount);\n setTotalPages(data.totalPages);\n } catch (e: any) {\n setError(e.message ?? 'Failed to load repositories');\n } finally {\n setLoading(false);\n }\n },\n [api],\n );\n\n useEffect(() => {\n load(page, appliedSearch);\n }, [load, page, appliedSearch]);\n\n const applySearch = () => {\n setPage(1);\n setAppliedSearch(searchInput);\n };\n\n const formatDate = (iso: string) => {\n try {\n return new Date(iso).toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n });\n } catch {\n return iso;\n }\n };\n\n return (\n <InfoCard title={`Repositories (${totalCount})`} noPadding={false}>\n <Box className={classes.searchRow}>\n <TextField\n className={classes.searchField}\n size=\"small\"\n variant=\"outlined\"\n label=\"Search repositories\"\n value={searchInput}\n onChange={e => setSearchInput(e.target.value)}\n onKeyDown={e => e.key === 'Enter' && applySearch()}\n InputProps={{\n endAdornment: (\n <IconButton size=\"small\" onClick={applySearch}>\n <SearchIcon fontSize=\"small\" />\n </IconButton>\n ),\n }}\n />\n </Box>\n\n {loading ? (\n <Box display=\"flex\" justifyContent=\"center\" py={4}>\n <CircularProgress />\n </Box>\n ) : error ? (\n <EmptyState missing=\"data\" title=\"Repositories unavailable\" description={error} />\n ) : repositories.length === 0 ? (\n <EmptyState missing=\"data\" title=\"No repositories found\" description=\"Try a different search.\" />\n ) : (\n <React.Fragment>\n <TableContainer component={Paper} variant=\"outlined\">\n <Table size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Repository</TableCell>\n <TableCell align=\"right\">Developers</TableCell>\n <TableCell align=\"right\">Events</TableCell>\n <TableCell style={{ minWidth: 140 }}>Avg Proficiency</TableCell>\n <TableCell align=\"right\">Last Activity</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {repositories.map(repo => (\n <TableRow\n key={repo.repoName}\n className={classes.clickableRow}\n onClick={() => onViewRepo(repo.repoName)}\n >\n <TableCell>\n <Box display=\"flex\" alignItems=\"center\" style={{ gap: 8 }}>\n <StorageIcon fontSize=\"small\" color=\"action\" />\n <Typography variant=\"body2\" className={classes.repoName}>\n {repo.repoName}\n </Typography>\n </Box>\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"body2\">{repo.developerCount}</Typography>\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"body2\">{repo.eventCount.toLocaleString()}</Typography>\n </TableCell>\n <TableCell style={{ minWidth: 140 }}>\n <Typography variant=\"body2\" style={{ fontWeight: 600 }}>\n {(repo.avgProficiency * 10).toFixed(1)}%\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={repo.avgProficiency * 10}\n className={classes.scoreBar}\n />\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"caption\" color=\"textSecondary\">\n {formatDate(repo.lastActivity)}\n </Typography>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </TableContainer>\n\n <Box className={classes.pagination}>\n <Typography variant=\"caption\" color=\"textSecondary\">\n Showing {(page - 1) * pageSize + 1}–{Math.min(page * pageSize, totalCount)} of {totalCount}\n </Typography>\n <Box display=\"flex\" alignItems=\"center\" style={{ gap: 8 }}>\n <Button size=\"small\" disabled={page <= 1} onClick={() => setPage(p => p - 1)}>\n Prev\n </Button>\n <Typography variant=\"caption\">\n {page} / {totalPages}\n </Typography>\n <Button size=\"small\" disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>\n Next\n </Button>\n </Box>\n </Box>\n </React.Fragment>\n )}\n </InfoCard>\n );\n};\n\n// ─── Repository Details ───────────────────────────────────────────────────────\n\ninterface RepositoryDetailsViewProps {\n api: DevxpApi;\n repoName: string;\n onBack: () => void;\n onViewDeveloper?: (userId: string) => void;\n}\n\nconst RepositoryDetailsView = ({ api, repoName, onBack, onViewDeveloper }: RepositoryDetailsViewProps) => {\n const classes = useStyles();\n\n const [details, setDetails] = useState<RepositoryDetails | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n setLoading(true);\n setError(null);\n api\n .getRepositoryDetails(repoName)\n .then(setDetails)\n .catch((e: any) => setError(e.message ?? 'Failed to load repository details'))\n .finally(() => setLoading(false));\n }, [api, repoName]);\n\n const formatDate = (iso: string) => {\n try {\n return new Date(iso).toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n });\n } catch {\n return iso;\n }\n };\n\n return (\n <Box>\n <Box className={classes.header}>\n <Tooltip title=\"Back to repositories\">\n <IconButton size=\"small\" onClick={onBack}>\n <ArrowBackIcon />\n </IconButton>\n </Tooltip>\n <StorageIcon color=\"action\" />\n <Typography variant=\"h6\" className={classes.repoName}>\n {repoName}\n </Typography>\n </Box>\n\n {loading ? (\n <Box display=\"flex\" justifyContent=\"center\" py={6}>\n <CircularProgress />\n </Box>\n ) : error ? (\n <EmptyState missing=\"data\" title=\"Details unavailable\" description={error} />\n ) : details ? (\n <React.Fragment>\n {/* Summary stats */}\n <InfoCard title=\"Overview\">\n <Box display=\"flex\" justifyContent=\"space-around\" flexWrap=\"wrap\">\n <Box className={classes.statBox}>\n <Typography className={classes.statValue}>{details.developerCount}</Typography>\n <Typography className={classes.statLabel}>Developers</Typography>\n </Box>\n <Box className={classes.statBox}>\n <Typography className={classes.statValue}>{details.eventCount.toLocaleString()}</Typography>\n <Typography className={classes.statLabel}>Skill Events</Typography>\n </Box>\n <Box className={classes.statBox}>\n <Typography className={classes.statValue}>\n {(details.avgProficiency * 10).toFixed(1)}%\n </Typography>\n <Typography className={classes.statLabel}>Avg Proficiency</Typography>\n </Box>\n <Box className={classes.statBox}>\n <Typography className={classes.statValue} style={{ fontSize: '1rem' }}>\n {formatDate(details.lastActivity)}\n </Typography>\n <Typography className={classes.statLabel}>Last Activity</Typography>\n </Box>\n </Box>\n </InfoCard>\n\n {/* Category breakdown */}\n {details.categoryBreakdown.length > 0 && (\n <Box mt={2}>\n <InfoCard title=\"Skill Categories\">\n <TableContainer component={Paper} variant=\"outlined\">\n <Table size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Category</TableCell>\n <TableCell style={{ minWidth: 160 }}>Avg Proficiency</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {details.categoryBreakdown.map(cat => (\n <TableRow key={cat.category}>\n <TableCell>\n <Chip\n label={cat.category}\n size=\"small\"\n className={classes.categoryChip}\n style={{\n backgroundColor: `${CATEGORY_COLORS[cat.category] ?? '#9e9e9e'}20`,\n color: CATEGORY_COLORS[cat.category] ?? '#9e9e9e',\n height: 20,\n }}\n />\n </TableCell>\n <TableCell style={{ minWidth: 160 }}>\n <Typography variant=\"body2\" style={{ fontWeight: 600 }}>\n {(cat.averageProficiency * 10).toFixed(1)}%\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={cat.averageProficiency * 10}\n className={classes.scoreBar}\n style={{ backgroundColor: `${CATEGORY_COLORS[cat.category] ?? '#9e9e9e'}30` }}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </TableContainer>\n </InfoCard>\n </Box>\n )}\n\n {/* Top developers */}\n {details.topDevelopers.length > 0 && (\n <Box mt={2}>\n <InfoCard title=\"Top Developers\">\n <TableContainer component={Paper} variant=\"outlined\">\n <Table size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>#</TableCell>\n <TableCell>Developer</TableCell>\n <TableCell style={{ minWidth: 140 }}>Avg Proficiency</TableCell>\n <TableCell align=\"right\">Skills</TableCell>\n {onViewDeveloper && <TableCell align=\"right\" />}\n </TableRow>\n </TableHead>\n <TableBody>\n {details.topDevelopers.map((dev, idx) => (\n <TableRow\n key={dev.userId}\n className={onViewDeveloper ? classes.clickableRow : undefined}\n onClick={() => onViewDeveloper?.(dev.userId)}\n >\n <TableCell>\n <Typography variant=\"body2\" color=\"textSecondary\">\n {idx + 1}\n </Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" style={{ fontFamily: 'monospace', fontWeight: 600 }}>\n {dev.userId}\n </Typography>\n </TableCell>\n <TableCell style={{ minWidth: 140 }}>\n <Typography variant=\"body2\" style={{ fontWeight: 600 }}>\n {(dev.averageProficiency * 10).toFixed(1)}%\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={dev.averageProficiency * 10}\n className={classes.scoreBar}\n />\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"body2\">{dev.skillCount}</Typography>\n </TableCell>\n {onViewDeveloper && (\n <TableCell align=\"right\" onClick={e => e.stopPropagation()}>\n <Tooltip title=\"View developer details\">\n <IconButton size=\"small\" onClick={() => onViewDeveloper(dev.userId)}>\n <PersonIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n </TableCell>\n )}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </TableContainer>\n </InfoCard>\n </Box>\n )}\n </React.Fragment>\n ) : null}\n </Box>\n );\n};\n"],"names":["React"],"mappings":"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"}
@@ -0,0 +1,174 @@
1
+ import React__default from 'react';
2
+ import { makeStyles } from '@material-ui/core/styles';
3
+
4
+ const CATEGORIES = [
5
+ "Frontend Engineering",
6
+ "Backend Systems",
7
+ "Database & Data",
8
+ "Infrastructure (DevOps)",
9
+ "Mobile Development",
10
+ "AI & Machine Learning",
11
+ "Testing & QA",
12
+ "Security & Auth",
13
+ "Distributed Systems",
14
+ "Documentation"
15
+ ];
16
+ const SHORT_NAMES = {
17
+ "Frontend Engineering": "Frontend",
18
+ "Backend Systems": "Backend",
19
+ "Database & Data": "Database",
20
+ "Infrastructure (DevOps)": "DevOps",
21
+ "Mobile Development": "Mobile",
22
+ "AI & Machine Learning": "AI/ML",
23
+ "Testing & QA": "Testing",
24
+ "Security & Auth": "Security",
25
+ "Distributed Systems": "Distributed",
26
+ "Documentation": "Docs"
27
+ };
28
+ const CENTER_X = 180;
29
+ const CENTER_Y = 180;
30
+ const MAX_RADIUS = 130;
31
+ const LABEL_RADIUS = 155;
32
+ const NUM_LEVELS = 5;
33
+ const SVG_SIZE = 360;
34
+ function polarToCartesian(cx, cy, r, angleDeg) {
35
+ const rad = (angleDeg - 90) * Math.PI / 180;
36
+ return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
37
+ }
38
+ function buildPolygon(values, maxVal) {
39
+ const step = 360 / values.length;
40
+ const pts = values.map((v, i) => {
41
+ const r = Math.max(v, 0) / maxVal * MAX_RADIUS;
42
+ const p = polarToCartesian(CENTER_X, CENTER_Y, r, i * step);
43
+ return `${p.x},${p.y}`;
44
+ });
45
+ return pts.join(" ");
46
+ }
47
+ function buildGridPolygon(level) {
48
+ const r = level / NUM_LEVELS * MAX_RADIUS;
49
+ const step = 360 / CATEGORIES.length;
50
+ return CATEGORIES.map((_, i) => {
51
+ const p = polarToCartesian(CENTER_X, CENTER_Y, r, i * step);
52
+ return `${p.x},${p.y}`;
53
+ }).join(" ");
54
+ }
55
+ const useStyles = makeStyles((theme) => ({
56
+ svg: {
57
+ width: "100%",
58
+ maxWidth: SVG_SIZE,
59
+ display: "block",
60
+ margin: "0 auto"
61
+ },
62
+ gridPolygon: {
63
+ fill: "none",
64
+ stroke: theme.palette.divider,
65
+ strokeWidth: 1
66
+ },
67
+ radialLine: {
68
+ stroke: theme.palette.divider,
69
+ strokeWidth: 1
70
+ },
71
+ dataPolygon: {
72
+ fill: "rgba(63, 81, 181, 0.18)",
73
+ stroke: "#3f51b5",
74
+ strokeWidth: 2
75
+ },
76
+ comparePolygon: {
77
+ fill: "rgba(233, 30, 99, 0.12)",
78
+ stroke: "#e91e63",
79
+ strokeWidth: 2
80
+ },
81
+ dataPoint: {
82
+ fill: "#3f51b5"
83
+ },
84
+ comparePoint: {
85
+ fill: "#e91e63"
86
+ },
87
+ label: {
88
+ fontSize: 10,
89
+ fontWeight: 600,
90
+ fill: theme.palette.text.primary,
91
+ dominantBaseline: "middle"
92
+ }
93
+ }));
94
+ const SpiderChart = ({ primary, compare, maxValue = 10 }) => {
95
+ const classes = useStyles();
96
+ const primaryMap = new Map(primary.categoryAverages.map((c) => [c.category, c.averageProficiency]));
97
+ const compareMap = compare ? new Map(compare.categoryAverages.map((c) => [c.category, c.averageProficiency])) : null;
98
+ const primaryValues = CATEGORIES.map((cat) => Math.max(primaryMap.get(cat) ?? 0, 1));
99
+ const compareValues = compareMap ? CATEGORIES.map((cat) => Math.max(compareMap.get(cat) ?? 0, 1)) : null;
100
+ const step = 360 / CATEGORIES.length;
101
+ return /* @__PURE__ */ React__default.createElement(
102
+ "svg",
103
+ {
104
+ viewBox: `0 0 ${SVG_SIZE} ${SVG_SIZE}`,
105
+ className: classes.svg,
106
+ "aria-label": "Skill radar chart"
107
+ },
108
+ Array.from({ length: NUM_LEVELS }, (_, i) => i + 1).map((level) => /* @__PURE__ */ React__default.createElement(
109
+ "polygon",
110
+ {
111
+ key: level,
112
+ points: buildGridPolygon(level),
113
+ className: classes.gridPolygon
114
+ }
115
+ )),
116
+ CATEGORIES.map((_, i) => {
117
+ const outer = polarToCartesian(CENTER_X, CENTER_Y, MAX_RADIUS, i * step);
118
+ return /* @__PURE__ */ React__default.createElement(
119
+ "line",
120
+ {
121
+ key: i,
122
+ x1: CENTER_X,
123
+ y1: CENTER_Y,
124
+ x2: outer.x,
125
+ y2: outer.y,
126
+ className: classes.radialLine
127
+ }
128
+ );
129
+ }),
130
+ compareValues && /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement(
131
+ "polygon",
132
+ {
133
+ points: buildPolygon(compareValues, maxValue),
134
+ className: classes.comparePolygon
135
+ }
136
+ ), compareValues.map((v, i) => {
137
+ const r = Math.max(v, 0) / maxValue * MAX_RADIUS;
138
+ const p = polarToCartesian(CENTER_X, CENTER_Y, r, i * step);
139
+ return /* @__PURE__ */ React__default.createElement("circle", { key: i, cx: p.x, cy: p.y, r: 3, className: classes.comparePoint });
140
+ })),
141
+ /* @__PURE__ */ React__default.createElement(
142
+ "polygon",
143
+ {
144
+ points: buildPolygon(primaryValues, maxValue),
145
+ className: classes.dataPolygon
146
+ }
147
+ ),
148
+ primaryValues.map((v, i) => {
149
+ const r = Math.max(v, 0) / maxValue * MAX_RADIUS;
150
+ const p = polarToCartesian(CENTER_X, CENTER_Y, r, i * step);
151
+ return /* @__PURE__ */ React__default.createElement("circle", { key: i, cx: p.x, cy: p.y, r: 4, className: classes.dataPoint });
152
+ }),
153
+ CATEGORIES.map((cat, i) => {
154
+ const pos = polarToCartesian(CENTER_X, CENTER_Y, LABEL_RADIUS, i * step);
155
+ let anchor = "middle";
156
+ if (pos.x < CENTER_X - 10) anchor = "end";
157
+ else if (pos.x > CENTER_X + 10) anchor = "start";
158
+ return /* @__PURE__ */ React__default.createElement(
159
+ "text",
160
+ {
161
+ key: cat,
162
+ x: pos.x,
163
+ y: pos.y,
164
+ textAnchor: anchor,
165
+ className: classes.label
166
+ },
167
+ SHORT_NAMES[cat] ?? cat
168
+ );
169
+ })
170
+ );
171
+ };
172
+
173
+ export { SpiderChart };
174
+ //# sourceMappingURL=SpiderChart.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpiderChart.esm.js","sources":["../../src/components/SpiderChart.tsx"],"sourcesContent":["import React from 'react';\nimport { makeStyles } from '@material-ui/core/styles';\n\nconst CATEGORIES = [\n 'Frontend Engineering',\n 'Backend Systems',\n 'Database & Data',\n 'Infrastructure (DevOps)',\n 'Mobile Development',\n 'AI & Machine Learning',\n 'Testing & QA',\n 'Security & Auth',\n 'Distributed Systems',\n 'Documentation',\n];\n\nconst SHORT_NAMES: Record<string, string> = {\n 'Frontend Engineering': 'Frontend',\n 'Backend Systems': 'Backend',\n 'Database & Data': 'Database',\n 'Infrastructure (DevOps)': 'DevOps',\n 'Mobile Development': 'Mobile',\n 'AI & Machine Learning': 'AI/ML',\n 'Testing & QA': 'Testing',\n 'Security & Auth': 'Security',\n 'Distributed Systems': 'Distributed',\n 'Documentation': 'Docs',\n};\n\nconst CENTER_X = 180;\nconst CENTER_Y = 180;\nconst MAX_RADIUS = 130;\nconst LABEL_RADIUS = 155;\nconst NUM_LEVELS = 5;\nconst SVG_SIZE = 360;\n\nfunction polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {\n const rad = ((angleDeg - 90) * Math.PI) / 180;\n return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };\n}\n\nfunction buildPolygon(values: number[], maxVal: number): string {\n const step = 360 / values.length;\n const pts = values.map((v, i) => {\n const r = (Math.max(v, 0) / maxVal) * MAX_RADIUS;\n const p = polarToCartesian(CENTER_X, CENTER_Y, r, i * step);\n return `${p.x},${p.y}`;\n });\n return pts.join(' ');\n}\n\nfunction buildGridPolygon(level: number): string {\n const r = (level / NUM_LEVELS) * MAX_RADIUS;\n const step = 360 / CATEGORIES.length;\n return CATEGORIES.map((_, i) => {\n const p = polarToCartesian(CENTER_X, CENTER_Y, r, i * step);\n return `${p.x},${p.y}`;\n }).join(' ');\n}\n\nconst useStyles = makeStyles(theme => ({\n svg: {\n width: '100%',\n maxWidth: SVG_SIZE,\n display: 'block',\n margin: '0 auto',\n },\n gridPolygon: {\n fill: 'none',\n stroke: theme.palette.divider,\n strokeWidth: 1,\n },\n radialLine: {\n stroke: theme.palette.divider,\n strokeWidth: 1,\n },\n dataPolygon: {\n fill: 'rgba(63, 81, 181, 0.18)',\n stroke: '#3f51b5',\n strokeWidth: 2,\n },\n comparePolygon: {\n fill: 'rgba(233, 30, 99, 0.12)',\n stroke: '#e91e63',\n strokeWidth: 2,\n },\n dataPoint: {\n fill: '#3f51b5',\n },\n comparePoint: {\n fill: '#e91e63',\n },\n label: {\n fontSize: 10,\n fontWeight: 600,\n fill: theme.palette.text.primary,\n dominantBaseline: 'middle',\n },\n}));\n\nexport interface SpiderChartData {\n /** Map of category name → proficiency value (0-10 scale) */\n categoryAverages: Array<{ category: string; averageProficiency: number }>;\n}\n\ninterface SpiderChartProps {\n primary: SpiderChartData;\n /** Optional second dataset shown as overlay (e.g. platform average or second developer) */\n compare?: SpiderChartData;\n maxValue?: number;\n}\n\nexport const SpiderChart = ({ primary, compare, maxValue = 10 }: SpiderChartProps) => {\n const classes = useStyles();\n\n const primaryMap = new Map(primary.categoryAverages.map(c => [c.category, c.averageProficiency]));\n const compareMap = compare\n ? new Map(compare.categoryAverages.map(c => [c.category, c.averageProficiency]))\n : null;\n\n const primaryValues = CATEGORIES.map(cat => Math.max(primaryMap.get(cat) ?? 0, 1));\n const compareValues = compareMap ? CATEGORIES.map(cat => Math.max(compareMap.get(cat) ?? 0, 1)) : null;\n\n const step = 360 / CATEGORIES.length;\n\n return (\n <svg\n viewBox={`0 0 ${SVG_SIZE} ${SVG_SIZE}`}\n className={classes.svg}\n aria-label=\"Skill radar chart\"\n >\n {/* Grid */}\n {Array.from({ length: NUM_LEVELS }, (_, i) => i + 1).map(level => (\n <polygon\n key={level}\n points={buildGridPolygon(level)}\n className={classes.gridPolygon}\n />\n ))}\n\n {/* Radial lines */}\n {CATEGORIES.map((_, i) => {\n const outer = polarToCartesian(CENTER_X, CENTER_Y, MAX_RADIUS, i * step);\n return (\n <line\n key={i}\n x1={CENTER_X}\n y1={CENTER_Y}\n x2={outer.x}\n y2={outer.y}\n className={classes.radialLine}\n />\n );\n })}\n\n {/* Compare overlay */}\n {compareValues && (\n <React.Fragment>\n <polygon\n points={buildPolygon(compareValues, maxValue)}\n className={classes.comparePolygon}\n />\n {compareValues.map((v, i) => {\n const r = (Math.max(v, 0) / maxValue) * MAX_RADIUS;\n const p = polarToCartesian(CENTER_X, CENTER_Y, r, i * step);\n return <circle key={i} cx={p.x} cy={p.y} r={3} className={classes.comparePoint} />;\n })}\n </React.Fragment>\n )}\n\n {/* Primary data */}\n <polygon\n points={buildPolygon(primaryValues, maxValue)}\n className={classes.dataPolygon}\n />\n {primaryValues.map((v, i) => {\n const r = (Math.max(v, 0) / maxValue) * MAX_RADIUS;\n const p = polarToCartesian(CENTER_X, CENTER_Y, r, i * step);\n return <circle key={i} cx={p.x} cy={p.y} r={4} className={classes.dataPoint} />;\n })}\n\n {/* Category labels */}\n {CATEGORIES.map((cat, i) => {\n const pos = polarToCartesian(CENTER_X, CENTER_Y, LABEL_RADIUS, i * step);\n let anchor: 'inherit' | 'end' | 'start' | 'middle' = 'middle';\n if (pos.x < CENTER_X - 10) anchor = 'end';\n else if (pos.x > CENTER_X + 10) anchor = 'start';\n return (\n <text\n key={cat}\n x={pos.x}\n y={pos.y}\n textAnchor={anchor}\n className={classes.label}\n >\n {SHORT_NAMES[cat] ?? cat}\n </text>\n );\n })}\n </svg>\n );\n};\n"],"names":["React"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karimov-labs/backstage-plugin-devxp",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Backstage frontend plugin for developer intelligence — masked identity management, CSV upload, and unmask tooling.",
5
5
  "main": "dist/index.esm.js",
6
6
  "module": "dist/index.esm.js",