@kuadrant/kuadrant-backstage-plugin-frontend 0.0.2-dev-83763ff → 0.0.2-dev-b696169

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.
Files changed (91) hide show
  1. package/dist/components/ApiKeyDetailPage/ApiKeyDetailPage.esm.js +319 -0
  2. package/dist/components/ApiKeyDetailPage/ApiKeyDetailPage.esm.js.map +1 -0
  3. package/dist/components/ApiKeyDetailPage/index.esm.js +2 -0
  4. package/dist/components/ApiKeyDetailPage/index.esm.js.map +1 -0
  5. package/dist/components/ApiKeyManagementTab/ApiKeyManagementTab.esm.js +340 -162
  6. package/dist/components/ApiKeyManagementTab/ApiKeyManagementTab.esm.js.map +1 -1
  7. package/dist/components/ApiKeysPage/ApiKeysPage.esm.js +43 -0
  8. package/dist/components/ApiKeysPage/ApiKeysPage.esm.js.map +1 -0
  9. package/dist/components/ApiKeysPage/index.esm.js +2 -0
  10. package/dist/components/ApiKeysPage/index.esm.js.map +1 -0
  11. package/dist/components/ApprovalQueueCard/ApprovalQueueCard.esm.js +312 -118
  12. package/dist/components/ApprovalQueueCard/ApprovalQueueCard.esm.js.map +1 -1
  13. package/dist/components/ApprovalQueueTable/ApprovalQueueTable.esm.js +645 -0
  14. package/dist/components/ApprovalQueueTable/ApprovalQueueTable.esm.js.map +1 -0
  15. package/dist/components/EditAPIKeyDialog/EditAPIKeyDialog.esm.js +14 -2
  16. package/dist/components/EditAPIKeyDialog/EditAPIKeyDialog.esm.js.map +1 -1
  17. package/dist/components/EntityApiApprovalTab/EntityApiApprovalTab.esm.js +276 -0
  18. package/dist/components/EntityApiApprovalTab/EntityApiApprovalTab.esm.js.map +1 -0
  19. package/dist/components/EntityApiApprovalTab/index.esm.js +2 -0
  20. package/dist/components/EntityApiApprovalTab/index.esm.js.map +1 -0
  21. package/dist/components/FilterPanel/FilterPanel.esm.js +127 -0
  22. package/dist/components/FilterPanel/FilterPanel.esm.js.map +1 -0
  23. package/dist/components/KuadrantPage/KuadrantPage.esm.js +123 -40
  24. package/dist/components/KuadrantPage/KuadrantPage.esm.js.map +1 -1
  25. package/dist/components/KuadrantPage/index.esm.js +1 -1
  26. package/dist/components/{MyApiKeysCard/MyApiKeysCard.esm.js → MyApiKeysTable/MyApiKeysTable.esm.js} +294 -176
  27. package/dist/components/MyApiKeysTable/MyApiKeysTable.esm.js.map +1 -0
  28. package/dist/components/PlanPolicyDetailsCard/PlanPolicyDetails.esm.js +5 -4
  29. package/dist/components/PlanPolicyDetailsCard/PlanPolicyDetails.esm.js.map +1 -1
  30. package/dist/index.d.ts +8 -4
  31. package/dist/index.esm.js +1 -1
  32. package/dist/plugin.esm.js +30 -1
  33. package/dist/plugin.esm.js.map +1 -1
  34. package/dist/utils/styles.esm.js +14 -0
  35. package/dist/utils/styles.esm.js.map +1 -0
  36. package/dist-scalprum/{internal.plugin-kuadrant.6dd50c0e0265251d3d4f.js → internal.plugin-kuadrant.e37d8046ec4d7ed991e0.js} +2 -2
  37. package/dist-scalprum/internal.plugin-kuadrant.e37d8046ec4d7ed991e0.js.map +1 -0
  38. package/dist-scalprum/plugin-manifest.json +2 -2
  39. package/dist-scalprum/static/2967.c684efaf.chunk.js +2 -0
  40. package/dist-scalprum/static/2967.c684efaf.chunk.js.map +1 -0
  41. package/dist-scalprum/static/3097.4bd6b35f.chunk.js +2 -0
  42. package/dist-scalprum/static/3097.4bd6b35f.chunk.js.map +1 -0
  43. package/dist-scalprum/static/4306.3a218ff1.chunk.js +2 -0
  44. package/dist-scalprum/static/4306.3a218ff1.chunk.js.map +1 -0
  45. package/dist-scalprum/static/5010.acf9a415.chunk.js +3 -0
  46. package/dist-scalprum/static/5010.acf9a415.chunk.js.map +1 -0
  47. package/dist-scalprum/static/5453.ae292ab1.chunk.js +2 -0
  48. package/dist-scalprum/static/5453.ae292ab1.chunk.js.map +1 -0
  49. package/dist-scalprum/static/6800.736d5da3.chunk.js +2 -0
  50. package/dist-scalprum/static/6800.736d5da3.chunk.js.map +1 -0
  51. package/dist-scalprum/static/6813.036a322f.chunk.js +2 -0
  52. package/dist-scalprum/static/6813.036a322f.chunk.js.map +1 -0
  53. package/dist-scalprum/static/6840.4728fab9.chunk.js +2 -0
  54. package/dist-scalprum/static/6840.4728fab9.chunk.js.map +1 -0
  55. package/dist-scalprum/static/6979.9699b350.chunk.js +2 -0
  56. package/dist-scalprum/static/6979.9699b350.chunk.js.map +1 -0
  57. package/dist-scalprum/static/7684.3d1fc1a1.chunk.js +2 -0
  58. package/dist-scalprum/static/7684.3d1fc1a1.chunk.js.map +1 -0
  59. package/dist-scalprum/static/8365.d3360f18.chunk.js +2 -0
  60. package/dist-scalprum/static/8365.d3360f18.chunk.js.map +1 -0
  61. package/dist-scalprum/static/8416.3604a311.chunk.js +2 -0
  62. package/dist-scalprum/static/8416.3604a311.chunk.js.map +1 -0
  63. package/dist-scalprum/static/8563.7e068fb0.chunk.js +3 -0
  64. package/dist-scalprum/static/8563.7e068fb0.chunk.js.map +1 -0
  65. package/dist-scalprum/static/exposed-PluginRoot.0717f1ce.chunk.js +2 -0
  66. package/dist-scalprum/static/exposed-PluginRoot.0717f1ce.chunk.js.map +1 -0
  67. package/package.json +1 -1
  68. package/dist/components/MyApiKeysCard/MyApiKeysCard.esm.js.map +0 -1
  69. package/dist-scalprum/internal.plugin-kuadrant.6dd50c0e0265251d3d4f.js.map +0 -1
  70. package/dist-scalprum/static/3483.2f14a8ca.chunk.js +0 -3
  71. package/dist-scalprum/static/3483.2f14a8ca.chunk.js.map +0 -1
  72. package/dist-scalprum/static/4306.b68910c9.chunk.js +0 -2
  73. package/dist-scalprum/static/4306.b68910c9.chunk.js.map +0 -1
  74. package/dist-scalprum/static/4556.c6bedfc4.chunk.js +0 -3
  75. package/dist-scalprum/static/4556.c6bedfc4.chunk.js.map +0 -1
  76. package/dist-scalprum/static/5222.796292ca.chunk.js +0 -2
  77. package/dist-scalprum/static/5222.796292ca.chunk.js.map +0 -1
  78. package/dist-scalprum/static/532.e406b85b.chunk.js +0 -2
  79. package/dist-scalprum/static/532.e406b85b.chunk.js.map +0 -1
  80. package/dist-scalprum/static/5453.29118045.chunk.js +0 -2
  81. package/dist-scalprum/static/5453.29118045.chunk.js.map +0 -1
  82. package/dist-scalprum/static/6281.eb6e16a2.chunk.js +0 -2
  83. package/dist-scalprum/static/6281.eb6e16a2.chunk.js.map +0 -1
  84. package/dist-scalprum/static/8365.75ea3581.chunk.js +0 -2
  85. package/dist-scalprum/static/8365.75ea3581.chunk.js.map +0 -1
  86. package/dist-scalprum/static/8441.62394cfd.chunk.js +0 -2
  87. package/dist-scalprum/static/8441.62394cfd.chunk.js.map +0 -1
  88. package/dist-scalprum/static/exposed-PluginRoot.7023ce03.chunk.js +0 -2
  89. package/dist-scalprum/static/exposed-PluginRoot.7023ce03.chunk.js.map +0 -1
  90. /package/dist-scalprum/static/{4556.c6bedfc4.chunk.js.LICENSE.txt → 5010.acf9a415.chunk.js.LICENSE.txt} +0 -0
  91. /package/dist-scalprum/static/{3483.2f14a8ca.chunk.js.LICENSE.txt → 8563.7e068fb0.chunk.js.LICENSE.txt} +0 -0
@@ -0,0 +1,645 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { useApi, configApiRef, fetchApiRef, identityApiRef, alertApiRef } from '@backstage/core-plugin-api';
3
+ import { useAsync } from 'react-use';
4
+ import { Progress, ResponseErrorPanel, Table, Link } from '@backstage/core-components';
5
+ import { kuadrantApiKeyUpdateAllPermission, kuadrantApiKeyUpdateOwnPermission } from '../../permissions.esm.js';
6
+ import { useKuadrantPermission } from '../../utils/permissions.esm.js';
7
+ import { makeStyles, Box, Typography, Button, Dialog, DialogTitle, DialogContent, TextField, DialogActions, CircularProgress, Chip } from '@material-ui/core';
8
+ import CheckCircleIcon from '@material-ui/icons/CheckCircle';
9
+ import CancelIcon from '@material-ui/icons/Cancel';
10
+ import { FilterPanel } from '../FilterPanel/FilterPanel.esm.js';
11
+ import { getStatusChipStyle } from '../../utils/styles.esm.js';
12
+
13
+ const useStyles = makeStyles((theme) => ({
14
+ container: {
15
+ display: "flex",
16
+ height: "100%",
17
+ minHeight: 400
18
+ },
19
+ tableContainer: {
20
+ flex: 1,
21
+ overflow: "auto"
22
+ },
23
+ useCasePanel: {
24
+ padding: theme.spacing(2),
25
+ backgroundColor: theme.palette.background.default
26
+ },
27
+ useCaseLabel: {
28
+ fontWeight: 600,
29
+ marginBottom: theme.spacing(1),
30
+ color: theme.palette.text.secondary,
31
+ textTransform: "uppercase",
32
+ fontSize: "0.75rem"
33
+ },
34
+ bulkActions: {
35
+ padding: theme.spacing(2),
36
+ backgroundColor: theme.palette.background.default,
37
+ borderBottom: `1px solid ${theme.palette.divider}`,
38
+ display: "flex",
39
+ alignItems: "center",
40
+ justifyContent: "space-between"
41
+ }
42
+ }));
43
+ const ApprovalDialog = ({
44
+ open,
45
+ request,
46
+ action,
47
+ processing,
48
+ onClose,
49
+ onConfirm
50
+ }) => {
51
+ const [confirmInput, setConfirmInput] = React.useState("");
52
+ const actionLabel = action === "approve" ? "Approve" : "Reject";
53
+ const processingLabel = action === "approve" ? "Approving..." : "Rejecting...";
54
+ const isReject = action === "reject";
55
+ const confirmText = request?.spec.requestedBy?.userId || "";
56
+ const canConfirm = isReject ? confirmInput === confirmText : true;
57
+ React.useEffect(() => {
58
+ if (!open) {
59
+ setConfirmInput("");
60
+ }
61
+ }, [open]);
62
+ return /* @__PURE__ */ React.createElement(
63
+ Dialog,
64
+ {
65
+ open,
66
+ onClose: processing ? undefined : onClose,
67
+ maxWidth: "sm",
68
+ fullWidth: true
69
+ },
70
+ /* @__PURE__ */ React.createElement(DialogTitle, null, isReject ? /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", style: { gap: 8 } }, /* @__PURE__ */ React.createElement(CancelIcon, { color: "error" }), /* @__PURE__ */ React.createElement("span", null, actionLabel, " API Key")) : /* @__PURE__ */ React.createElement("span", null, actionLabel, " API Key")),
71
+ /* @__PURE__ */ React.createElement(DialogContent, null, request && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "User:"), " ", request.spec.requestedBy.userId), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "API:"), " ", request.spec.apiProductRef?.name || "unknown"), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "Tier:"), " ", request.spec.planTier), /* @__PURE__ */ React.createElement(Box, { mb: 2 }, /* @__PURE__ */ React.createElement(
72
+ Typography,
73
+ {
74
+ variant: "body2",
75
+ component: "span",
76
+ style: { fontWeight: "bold" }
77
+ },
78
+ "Use Case:"
79
+ ), " ", /* @__PURE__ */ React.createElement(
80
+ Typography,
81
+ {
82
+ variant: "body2",
83
+ component: "span",
84
+ style: { whiteSpace: "pre-wrap" }
85
+ },
86
+ request.spec.useCase || "-"
87
+ )), isReject && /* @__PURE__ */ React.createElement(Box, { mt: 2 }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary", gutterBottom: true }, "Type ", /* @__PURE__ */ React.createElement("strong", null, confirmText), " to confirm rejection:"), /* @__PURE__ */ React.createElement(
88
+ TextField,
89
+ {
90
+ fullWidth: true,
91
+ variant: "outlined",
92
+ size: "small",
93
+ value: confirmInput,
94
+ onChange: (e) => setConfirmInput(e.target.value),
95
+ disabled: processing,
96
+ autoFocus: true,
97
+ placeholder: confirmText
98
+ }
99
+ )))),
100
+ /* @__PURE__ */ React.createElement(DialogActions, null, /* @__PURE__ */ React.createElement(Button, { onClick: onClose, disabled: processing }, "Cancel"), /* @__PURE__ */ React.createElement(
101
+ Button,
102
+ {
103
+ onClick: onConfirm,
104
+ color: action === "approve" ? "primary" : "secondary",
105
+ variant: "contained",
106
+ disabled: processing || !canConfirm,
107
+ startIcon: processing ? /* @__PURE__ */ React.createElement(CircularProgress, { size: 16, color: "inherit" }) : undefined
108
+ },
109
+ processing ? processingLabel : actionLabel
110
+ ))
111
+ );
112
+ };
113
+ const BulkActionDialog = ({
114
+ open,
115
+ requests,
116
+ action,
117
+ processing,
118
+ onClose,
119
+ onConfirm
120
+ }) => {
121
+ const isApprove = action === "approve";
122
+ const actionLabel = isApprove ? "Approve All" : "Reject All";
123
+ const processingLabel = isApprove ? "Approving..." : "Rejecting...";
124
+ return /* @__PURE__ */ React.createElement(
125
+ Dialog,
126
+ {
127
+ open,
128
+ onClose: processing ? undefined : onClose,
129
+ maxWidth: "md",
130
+ fullWidth: true
131
+ },
132
+ /* @__PURE__ */ React.createElement(DialogTitle, null, isApprove ? "Approve" : "Reject", " ", requests.length, " API Keys"),
133
+ /* @__PURE__ */ React.createElement(DialogContent, null, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", paragraph: true }, "You are about to ", isApprove ? "approve" : "reject", " the following API keys:"), /* @__PURE__ */ React.createElement(Box, { mb: 2, maxHeight: 200, overflow: "auto" }, requests.map((request) => /* @__PURE__ */ React.createElement(
134
+ Box,
135
+ {
136
+ key: `${request.metadata.namespace}/${request.metadata.name}`,
137
+ mb: 1,
138
+ p: 1,
139
+ bgcolor: "background.default"
140
+ },
141
+ /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, /* @__PURE__ */ React.createElement("strong", null, request.spec.requestedBy.userId), " -", " ", request.spec.apiProductRef?.name || "unknown", " (", request.spec.planTier, ")")
142
+ )))),
143
+ /* @__PURE__ */ React.createElement(DialogActions, null, /* @__PURE__ */ React.createElement(Button, { onClick: onClose, disabled: processing }, "Cancel"), /* @__PURE__ */ React.createElement(
144
+ Button,
145
+ {
146
+ onClick: onConfirm,
147
+ color: isApprove ? "primary" : "secondary",
148
+ variant: "contained",
149
+ disabled: processing,
150
+ startIcon: processing ? /* @__PURE__ */ React.createElement(CircularProgress, { size: 16, color: "inherit" }) : undefined
151
+ },
152
+ processing ? processingLabel : actionLabel
153
+ ))
154
+ );
155
+ };
156
+ const ExpandedRowContent = ({ request }) => {
157
+ const classes = useStyles();
158
+ return /* @__PURE__ */ React.createElement(Box, { className: classes.useCasePanel, onClick: (e) => e.stopPropagation() }, /* @__PURE__ */ React.createElement(Typography, { className: classes.useCaseLabel }, "Use Case"), /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, request.spec.useCase || "No use case provided"));
159
+ };
160
+ const ApprovalQueueTable = () => {
161
+ const classes = useStyles();
162
+ const config = useApi(configApiRef);
163
+ const fetchApi = useApi(fetchApiRef);
164
+ const identityApi = useApi(identityApiRef);
165
+ const alertApi = useApi(alertApiRef);
166
+ const backendUrl = config.getString("backend.baseUrl");
167
+ const [refresh, setRefresh] = useState(0);
168
+ const [selectedRequests, setSelectedRequests] = useState([]);
169
+ const [dialogState, setDialogState] = useState({
170
+ open: false,
171
+ request: null,
172
+ action: "approve",
173
+ processing: false
174
+ });
175
+ const [bulkDialogState, setBulkDialogState] = useState({
176
+ open: false,
177
+ requests: [],
178
+ action: "approve",
179
+ processing: false
180
+ });
181
+ const [filters, setFilters] = useState({
182
+ status: [],
183
+ apiProduct: [],
184
+ tier: []
185
+ });
186
+ const {
187
+ allowed: canUpdateAllRequests,
188
+ loading: updateAllPermissionLoading,
189
+ error: updateAllPermissionError
190
+ } = useKuadrantPermission(kuadrantApiKeyUpdateAllPermission);
191
+ const {
192
+ allowed: canUpdateOwnRequests,
193
+ loading: updateOwnPermissionLoading,
194
+ error: updateOwnPermissionError
195
+ } = useKuadrantPermission(kuadrantApiKeyUpdateOwnPermission);
196
+ const updatePermissionLoading = updateAllPermissionLoading || updateOwnPermissionLoading;
197
+ const updatePermissionError = updateAllPermissionError || updateOwnPermissionError;
198
+ const { value, loading, error } = useAsync(async () => {
199
+ const identity = await identityApi.getBackstageIdentity();
200
+ const reviewedBy = identity.userEntityRef;
201
+ const [requestsResponse, apiProductsResponse] = await Promise.all([
202
+ fetchApi.fetch(`${backendUrl}/api/kuadrant/requests`),
203
+ fetchApi.fetch(`${backendUrl}/api/kuadrant/apiproducts`)
204
+ ]);
205
+ if (!requestsResponse.ok) {
206
+ return {
207
+ allRequests: [],
208
+ reviewedBy,
209
+ ownedApiProducts: /* @__PURE__ */ new Set()
210
+ };
211
+ }
212
+ const contentType = requestsResponse.headers.get("content-type");
213
+ if (!contentType?.includes("application/json")) {
214
+ alertApi.post({
215
+ message: "Unexpected content-type from the server response.",
216
+ display: "transient",
217
+ severity: "warning"
218
+ });
219
+ return {
220
+ allRequests: [],
221
+ reviewedBy,
222
+ ownedApiProducts: /* @__PURE__ */ new Set()
223
+ };
224
+ }
225
+ const data = await requestsResponse.json();
226
+ const allRequests = data.items || [];
227
+ const ownedApiProducts = /* @__PURE__ */ new Set();
228
+ if (apiProductsResponse.ok) {
229
+ const apiProductsData = await apiProductsResponse.json();
230
+ for (const product of apiProductsData.items || []) {
231
+ const owner = product.metadata?.annotations?.["backstage.io/owner"];
232
+ if (owner === reviewedBy) {
233
+ ownedApiProducts.add(
234
+ `${product.metadata.namespace}/${product.metadata.name}`
235
+ );
236
+ }
237
+ }
238
+ }
239
+ return { allRequests, reviewedBy, ownedApiProducts };
240
+ }, [backendUrl, fetchApi, identityApi, refresh]);
241
+ const filterSections = useMemo(() => {
242
+ if (!value?.allRequests) return [];
243
+ const statusCounts = { Approved: 0, Pending: 0, Rejected: 0 };
244
+ const apiProductCounts = /* @__PURE__ */ new Map();
245
+ const tierCounts = /* @__PURE__ */ new Map();
246
+ value.allRequests.forEach((r) => {
247
+ const status = r.status?.phase || "Pending";
248
+ statusCounts[status]++;
249
+ const apiProduct = r.spec.apiProductRef?.name || "unknown";
250
+ apiProductCounts.set(
251
+ apiProduct,
252
+ (apiProductCounts.get(apiProduct) || 0) + 1
253
+ );
254
+ const tier = r.spec.planTier || "unknown";
255
+ tierCounts.set(tier, (tierCounts.get(tier) || 0) + 1);
256
+ });
257
+ return [
258
+ {
259
+ id: "status",
260
+ title: "Status",
261
+ options: [
262
+ { value: "Pending", label: "Pending", count: statusCounts.Pending },
263
+ {
264
+ value: "Approved",
265
+ label: "Approved",
266
+ count: statusCounts.Approved
267
+ },
268
+ {
269
+ value: "Rejected",
270
+ label: "Rejected",
271
+ count: statusCounts.Rejected
272
+ }
273
+ ]
274
+ },
275
+ {
276
+ id: "apiProduct",
277
+ title: "API Product",
278
+ options: Array.from(apiProductCounts.entries()).map(
279
+ ([name, count]) => ({
280
+ value: name,
281
+ label: name,
282
+ count
283
+ })
284
+ ),
285
+ collapsed: apiProductCounts.size > 5
286
+ },
287
+ {
288
+ id: "tier",
289
+ title: "Tier",
290
+ options: Array.from(tierCounts.entries()).map(([tier, count]) => ({
291
+ value: tier,
292
+ label: tier.charAt(0).toUpperCase() + tier.slice(1),
293
+ count
294
+ }))
295
+ }
296
+ ];
297
+ }, [value?.allRequests]);
298
+ const filteredRequests = useMemo(() => {
299
+ if (!value?.allRequests) return [];
300
+ return value.allRequests.filter((r) => {
301
+ if (filters.status.length > 0) {
302
+ const status = r.status?.phase || "Pending";
303
+ if (!filters.status.includes(status)) return false;
304
+ }
305
+ if (filters.apiProduct.length > 0) {
306
+ const apiProduct = r.spec.apiProductRef?.name || "unknown";
307
+ if (!filters.apiProduct.includes(apiProduct)) return false;
308
+ }
309
+ if (filters.tier.length > 0) {
310
+ const tier = r.spec.planTier || "unknown";
311
+ if (!filters.tier.includes(tier)) return false;
312
+ }
313
+ return true;
314
+ });
315
+ }, [value?.allRequests, filters]);
316
+ const handleApprove = (request) => {
317
+ setDialogState({
318
+ open: true,
319
+ request,
320
+ action: "approve",
321
+ processing: false
322
+ });
323
+ };
324
+ const handleReject = (request) => {
325
+ setDialogState({
326
+ open: true,
327
+ request,
328
+ action: "reject",
329
+ processing: false
330
+ });
331
+ };
332
+ const handleConfirm = async () => {
333
+ if (!dialogState.request || !value) return;
334
+ setDialogState((prev) => ({ ...prev, processing: true }));
335
+ const endpoint = dialogState.action === "approve" ? `${backendUrl}/api/kuadrant/requests/${dialogState.request.metadata.namespace}/${dialogState.request.metadata.name}/approve` : `${backendUrl}/api/kuadrant/requests/${dialogState.request.metadata.namespace}/${dialogState.request.metadata.name}/reject`;
336
+ try {
337
+ const response = await fetchApi.fetch(endpoint, {
338
+ method: "POST",
339
+ headers: { "Content-Type": "application/json" },
340
+ body: JSON.stringify({ reviewedBy: value.reviewedBy })
341
+ });
342
+ if (!response.ok) {
343
+ throw new Error(`failed to ${dialogState.action} request`);
344
+ }
345
+ setDialogState({
346
+ open: false,
347
+ request: null,
348
+ action: "approve",
349
+ processing: false
350
+ });
351
+ setSelectedRequests(
352
+ (prev) => prev.filter(
353
+ (r) => r.metadata.name !== dialogState.request?.metadata.name || r.metadata.namespace !== dialogState.request?.metadata.namespace
354
+ )
355
+ );
356
+ setRefresh((r) => r + 1);
357
+ const action = dialogState.action === "approve" ? "approved" : "rejected";
358
+ alertApi.post({
359
+ message: `API key ${action}`,
360
+ severity: "success",
361
+ display: "transient"
362
+ });
363
+ } catch (err) {
364
+ console.error(`error ${dialogState.action}ing request:`, err);
365
+ setDialogState((prev) => ({ ...prev, processing: false }));
366
+ alertApi.post({
367
+ message: `Failed to ${dialogState.action} API key`,
368
+ severity: "error",
369
+ display: "transient"
370
+ });
371
+ }
372
+ };
373
+ const handleBulkApprove = () => {
374
+ if (selectedRequests.length === 0) return;
375
+ setBulkDialogState({
376
+ open: true,
377
+ requests: selectedRequests,
378
+ action: "approve",
379
+ processing: false
380
+ });
381
+ };
382
+ const handleBulkReject = () => {
383
+ if (selectedRequests.length === 0) return;
384
+ setBulkDialogState({
385
+ open: true,
386
+ requests: selectedRequests,
387
+ action: "reject",
388
+ processing: false
389
+ });
390
+ };
391
+ const handleBulkConfirm = async () => {
392
+ if (!value || bulkDialogState.requests.length === 0) return;
393
+ setBulkDialogState((prev) => ({ ...prev, processing: true }));
394
+ const isApprove = bulkDialogState.action === "approve";
395
+ const endpoint = isApprove ? `${backendUrl}/api/kuadrant/requests/bulk-approve` : `${backendUrl}/api/kuadrant/requests/bulk-reject`;
396
+ try {
397
+ const response = await fetchApi.fetch(endpoint, {
398
+ method: "POST",
399
+ headers: { "Content-Type": "application/json" },
400
+ body: JSON.stringify({
401
+ requests: bulkDialogState.requests.map((r) => ({
402
+ namespace: r.metadata.namespace,
403
+ name: r.metadata.name
404
+ })),
405
+ reviewedBy: value.reviewedBy
406
+ })
407
+ });
408
+ if (!response.ok) {
409
+ throw new Error(`failed to bulk ${bulkDialogState.action} requests`);
410
+ }
411
+ const count = bulkDialogState.requests.length;
412
+ const action = isApprove ? "approved" : "rejected";
413
+ setBulkDialogState({
414
+ open: false,
415
+ requests: [],
416
+ action: "approve",
417
+ processing: false
418
+ });
419
+ setSelectedRequests([]);
420
+ setRefresh((r) => r + 1);
421
+ alertApi.post({
422
+ message: `${count} API keys ${action}`,
423
+ severity: "success",
424
+ display: "transient"
425
+ });
426
+ } catch (err) {
427
+ console.error(`error bulk ${bulkDialogState.action}ing requests:`, err);
428
+ setBulkDialogState((prev) => ({ ...prev, processing: false }));
429
+ alertApi.post({
430
+ message: `Failed to bulk ${bulkDialogState.action} API keys`,
431
+ severity: "error",
432
+ display: "transient"
433
+ });
434
+ }
435
+ };
436
+ const formatDate = (dateString) => {
437
+ const date = new Date(dateString);
438
+ return date.toLocaleDateString("en-GB", {
439
+ year: "numeric",
440
+ month: "short",
441
+ day: "numeric"
442
+ });
443
+ };
444
+ const columns = [
445
+ {
446
+ title: "Requester",
447
+ field: "spec.requestedBy.userId",
448
+ render: (row) => /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, row.spec.requestedBy.userId)
449
+ },
450
+ {
451
+ title: "API Product",
452
+ field: "spec.apiProductRef.name",
453
+ render: (row) => {
454
+ const name = row.spec.apiProductRef?.name || "unknown";
455
+ return /* @__PURE__ */ React.createElement(Link, { to: `/catalog/default/api/${name}` }, /* @__PURE__ */ React.createElement("strong", null, name));
456
+ }
457
+ },
458
+ {
459
+ title: "Status",
460
+ field: "status.phase",
461
+ render: (row) => {
462
+ const phase = row.status?.phase || "Pending";
463
+ return /* @__PURE__ */ React.createElement(Chip, { label: phase, size: "small", style: getStatusChipStyle(phase) });
464
+ }
465
+ },
466
+ {
467
+ title: "Tier",
468
+ field: "spec.planTier",
469
+ render: (row) => /* @__PURE__ */ React.createElement(Chip, { label: row.spec.planTier, size: "small", variant: "outlined" })
470
+ },
471
+ {
472
+ title: "Requested",
473
+ field: "metadata.creationTimestamp",
474
+ render: (row) => /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, row.metadata.creationTimestamp ? formatDate(row.metadata.creationTimestamp) : "-")
475
+ },
476
+ {
477
+ title: "Reviewed By",
478
+ field: "status.reviewedBy",
479
+ render: (row) => {
480
+ if (!row.status?.reviewedBy)
481
+ return /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "-");
482
+ const reviewer = row.status.reviewedBy.replace(/^user:default\//, "");
483
+ const isAutomatic = reviewer === "system";
484
+ return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, isAutomatic ? "Automatic" : reviewer), row.status.reviewedAt && /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "textSecondary" }, formatDate(row.status.reviewedAt)));
485
+ }
486
+ },
487
+ {
488
+ title: "Actions",
489
+ filtering: false,
490
+ render: (row) => {
491
+ const phase = row.status?.phase || "Pending";
492
+ if (phase !== "Pending") return null;
493
+ const apiProductKey = `${row.metadata.namespace}/${row.spec.apiProductRef?.name || "unknown"}`;
494
+ const ownsApiProduct = value?.ownedApiProducts?.has(apiProductKey) ?? false;
495
+ const canUpdate = canUpdateAllRequests || canUpdateOwnRequests && ownsApiProduct;
496
+ if (!canUpdate) return null;
497
+ return /* @__PURE__ */ React.createElement(Box, { display: "flex", style: { gap: 8 } }, /* @__PURE__ */ React.createElement(
498
+ Button,
499
+ {
500
+ size: "small",
501
+ startIcon: /* @__PURE__ */ React.createElement(CheckCircleIcon, null),
502
+ onClick: () => handleApprove(row),
503
+ color: "primary",
504
+ variant: "outlined"
505
+ },
506
+ "Approve"
507
+ ), /* @__PURE__ */ React.createElement(
508
+ Button,
509
+ {
510
+ size: "small",
511
+ startIcon: /* @__PURE__ */ React.createElement(CancelIcon, null),
512
+ onClick: () => handleReject(row),
513
+ color: "secondary",
514
+ variant: "outlined"
515
+ },
516
+ "Reject"
517
+ ));
518
+ }
519
+ }
520
+ ];
521
+ const detailPanelConfig = useMemo(
522
+ () => [
523
+ {
524
+ render: (data) => {
525
+ const request = data.rowData;
526
+ if (!request?.metadata?.name) {
527
+ return /* @__PURE__ */ React.createElement(Box, null);
528
+ }
529
+ return /* @__PURE__ */ React.createElement(ExpandedRowContent, { request });
530
+ }
531
+ }
532
+ ],
533
+ []
534
+ );
535
+ if (loading || updatePermissionLoading) {
536
+ return /* @__PURE__ */ React.createElement(Progress, null);
537
+ }
538
+ if (error) {
539
+ return /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error });
540
+ }
541
+ if (updatePermissionError) {
542
+ return /* @__PURE__ */ React.createElement(Box, { p: 2 }, /* @__PURE__ */ React.createElement(Typography, { color: "error" }, "Unable to check permissions: ", updatePermissionError.message));
543
+ }
544
+ const canSelectRows = canUpdateAllRequests || canUpdateOwnRequests;
545
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { className: classes.container }, /* @__PURE__ */ React.createElement(
546
+ FilterPanel,
547
+ {
548
+ sections: filterSections,
549
+ filters,
550
+ onChange: setFilters
551
+ }
552
+ ), /* @__PURE__ */ React.createElement(Box, { className: classes.tableContainer }, selectedRequests.length > 0 && /* @__PURE__ */ React.createElement(Box, { className: classes.bulkActions }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, selectedRequests.length, " request", selectedRequests.length !== 1 ? "s" : "", " selected"), /* @__PURE__ */ React.createElement(Box, { display: "flex", style: { gap: 8 } }, /* @__PURE__ */ React.createElement(
553
+ Button,
554
+ {
555
+ size: "small",
556
+ variant: "contained",
557
+ color: "primary",
558
+ startIcon: /* @__PURE__ */ React.createElement(CheckCircleIcon, null),
559
+ onClick: handleBulkApprove
560
+ },
561
+ "Approve Selected"
562
+ ), /* @__PURE__ */ React.createElement(
563
+ Button,
564
+ {
565
+ size: "small",
566
+ variant: "contained",
567
+ color: "secondary",
568
+ startIcon: /* @__PURE__ */ React.createElement(CancelIcon, null),
569
+ onClick: handleBulkReject
570
+ },
571
+ "Reject Selected"
572
+ ))), filteredRequests.length === 0 ? /* @__PURE__ */ React.createElement(Box, { p: 4, textAlign: "center" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body1", color: "textSecondary" }, value?.allRequests?.length === 0 ? "No API keys found." : "No API keys match the selected filters.")) : /* @__PURE__ */ React.createElement(
573
+ Table,
574
+ {
575
+ options: {
576
+ selection: canSelectRows,
577
+ showSelectAllCheckbox: filteredRequests.some(
578
+ (r) => !r.status?.phase || r.status.phase === "Pending"
579
+ ),
580
+ selectionProps: (row) => ({
581
+ disabled: row.status?.phase !== "Pending" && row.status?.phase !== undefined
582
+ }),
583
+ paging: filteredRequests.length > 10,
584
+ pageSize: 20,
585
+ search: true,
586
+ filtering: false,
587
+ debounceInterval: 300,
588
+ showTextRowsSelected: false,
589
+ toolbar: true,
590
+ emptyRowsWhenPaging: false
591
+ },
592
+ columns,
593
+ data: filteredRequests.map((item) => {
594
+ const isSelected = selectedRequests.some(
595
+ (selected) => selected.metadata.name === item.metadata.name && selected.metadata.namespace === item.metadata.namespace
596
+ );
597
+ return {
598
+ ...item,
599
+ id: item.metadata.name,
600
+ tableData: { checked: isSelected }
601
+ };
602
+ }),
603
+ onSelectionChange: (rows) => {
604
+ const pendingOnly = rows.filter(
605
+ (r) => !r.status?.phase || r.status.phase === "Pending"
606
+ );
607
+ setSelectedRequests(pendingOnly);
608
+ },
609
+ detailPanel: detailPanelConfig
610
+ }
611
+ ))), /* @__PURE__ */ React.createElement(
612
+ ApprovalDialog,
613
+ {
614
+ open: dialogState.open,
615
+ request: dialogState.request,
616
+ action: dialogState.action,
617
+ processing: dialogState.processing,
618
+ onClose: () => setDialogState({
619
+ open: false,
620
+ request: null,
621
+ action: "approve",
622
+ processing: false
623
+ }),
624
+ onConfirm: handleConfirm
625
+ }
626
+ ), /* @__PURE__ */ React.createElement(
627
+ BulkActionDialog,
628
+ {
629
+ open: bulkDialogState.open,
630
+ requests: bulkDialogState.requests,
631
+ action: bulkDialogState.action,
632
+ processing: bulkDialogState.processing,
633
+ onClose: () => setBulkDialogState({
634
+ open: false,
635
+ requests: [],
636
+ action: "approve",
637
+ processing: false
638
+ }),
639
+ onConfirm: handleBulkConfirm
640
+ }
641
+ ));
642
+ };
643
+
644
+ export { ApprovalQueueTable };
645
+ //# sourceMappingURL=ApprovalQueueTable.esm.js.map