@kuadrant/kuadrant-backstage-plugin-frontend 0.0.2-dev-61ff3d3 → 0.0.2-dev-5c79230

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 (92) 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/ApiProductInfoCard/ApiProductInfoCard.esm.js +23 -2
  12. package/dist/components/ApiProductInfoCard/ApiProductInfoCard.esm.js.map +1 -1
  13. package/dist/components/ApprovalQueueCard/ApprovalQueueCard.esm.js +312 -118
  14. package/dist/components/ApprovalQueueCard/ApprovalQueueCard.esm.js.map +1 -1
  15. package/dist/components/ApprovalQueueTable/ApprovalQueueTable.esm.js +645 -0
  16. package/dist/components/ApprovalQueueTable/ApprovalQueueTable.esm.js.map +1 -0
  17. package/dist/components/EditAPIKeyDialog/EditAPIKeyDialog.esm.js +14 -2
  18. package/dist/components/EditAPIKeyDialog/EditAPIKeyDialog.esm.js.map +1 -1
  19. package/dist/components/EntityApiApprovalTab/EntityApiApprovalTab.esm.js +276 -0
  20. package/dist/components/EntityApiApprovalTab/EntityApiApprovalTab.esm.js.map +1 -0
  21. package/dist/components/EntityApiApprovalTab/index.esm.js +2 -0
  22. package/dist/components/EntityApiApprovalTab/index.esm.js.map +1 -0
  23. package/dist/components/FilterPanel/FilterPanel.esm.js +127 -0
  24. package/dist/components/FilterPanel/FilterPanel.esm.js.map +1 -0
  25. package/dist/components/KuadrantPage/KuadrantPage.esm.js +93 -43
  26. package/dist/components/KuadrantPage/KuadrantPage.esm.js.map +1 -1
  27. package/dist/components/KuadrantPage/index.esm.js +1 -1
  28. package/dist/components/{MyApiKeysCard/MyApiKeysCard.esm.js → MyApiKeysTable/MyApiKeysTable.esm.js} +294 -176
  29. package/dist/components/MyApiKeysTable/MyApiKeysTable.esm.js.map +1 -0
  30. package/dist/components/PlanPolicyDetailsCard/PlanPolicyDetails.esm.js.map +1 -1
  31. package/dist/index.d.ts +8 -4
  32. package/dist/index.esm.js +1 -1
  33. package/dist/plugin.esm.js +30 -1
  34. package/dist/plugin.esm.js.map +1 -1
  35. package/dist/utils/styles.esm.js +14 -0
  36. package/dist/utils/styles.esm.js.map +1 -0
  37. package/dist-scalprum/{internal.plugin-kuadrant.90488643192ec2f36944.js → internal.plugin-kuadrant.a36ec4956222cb0faf61.js} +2 -2
  38. package/dist-scalprum/internal.plugin-kuadrant.a36ec4956222cb0faf61.js.map +1 -0
  39. package/dist-scalprum/plugin-manifest.json +2 -2
  40. package/dist-scalprum/static/2967.c684efaf.chunk.js +2 -0
  41. package/dist-scalprum/static/2967.c684efaf.chunk.js.map +1 -0
  42. package/dist-scalprum/static/3097.4bd6b35f.chunk.js +2 -0
  43. package/dist-scalprum/static/3097.4bd6b35f.chunk.js.map +1 -0
  44. package/dist-scalprum/static/4306.3a218ff1.chunk.js +2 -0
  45. package/dist-scalprum/static/4306.3a218ff1.chunk.js.map +1 -0
  46. package/dist-scalprum/static/5010.acf9a415.chunk.js +3 -0
  47. package/dist-scalprum/static/5010.acf9a415.chunk.js.map +1 -0
  48. package/dist-scalprum/static/5453.c1f90bdf.chunk.js +2 -0
  49. package/dist-scalprum/static/5453.c1f90bdf.chunk.js.map +1 -0
  50. package/dist-scalprum/static/6800.736d5da3.chunk.js +2 -0
  51. package/dist-scalprum/static/6800.736d5da3.chunk.js.map +1 -0
  52. package/dist-scalprum/static/6813.036a322f.chunk.js +2 -0
  53. package/dist-scalprum/static/6813.036a322f.chunk.js.map +1 -0
  54. package/dist-scalprum/static/6840.4728fab9.chunk.js +2 -0
  55. package/dist-scalprum/static/6840.4728fab9.chunk.js.map +1 -0
  56. package/dist-scalprum/static/6979.9699b350.chunk.js +2 -0
  57. package/dist-scalprum/static/6979.9699b350.chunk.js.map +1 -0
  58. package/dist-scalprum/static/7684.3d1fc1a1.chunk.js +2 -0
  59. package/dist-scalprum/static/7684.3d1fc1a1.chunk.js.map +1 -0
  60. package/dist-scalprum/static/8365.d3360f18.chunk.js +2 -0
  61. package/dist-scalprum/static/8365.d3360f18.chunk.js.map +1 -0
  62. package/dist-scalprum/static/8416.3604a311.chunk.js +2 -0
  63. package/dist-scalprum/static/8416.3604a311.chunk.js.map +1 -0
  64. package/dist-scalprum/static/8563.7e068fb0.chunk.js +3 -0
  65. package/dist-scalprum/static/8563.7e068fb0.chunk.js.map +1 -0
  66. package/dist-scalprum/static/exposed-PluginRoot.0717f1ce.chunk.js +2 -0
  67. package/dist-scalprum/static/exposed-PluginRoot.0717f1ce.chunk.js.map +1 -0
  68. package/package.json +1 -1
  69. package/dist/components/MyApiKeysCard/MyApiKeysCard.esm.js.map +0 -1
  70. package/dist-scalprum/internal.plugin-kuadrant.90488643192ec2f36944.js.map +0 -1
  71. package/dist-scalprum/static/1483.be69af13.chunk.js +0 -2
  72. package/dist-scalprum/static/1483.be69af13.chunk.js.map +0 -1
  73. package/dist-scalprum/static/3483.2f14a8ca.chunk.js +0 -3
  74. package/dist-scalprum/static/3483.2f14a8ca.chunk.js.map +0 -1
  75. package/dist-scalprum/static/4306.b68910c9.chunk.js +0 -2
  76. package/dist-scalprum/static/4306.b68910c9.chunk.js.map +0 -1
  77. package/dist-scalprum/static/4556.c6bedfc4.chunk.js +0 -3
  78. package/dist-scalprum/static/4556.c6bedfc4.chunk.js.map +0 -1
  79. package/dist-scalprum/static/5222.796292ca.chunk.js +0 -2
  80. package/dist-scalprum/static/5222.796292ca.chunk.js.map +0 -1
  81. package/dist-scalprum/static/532.146b1fd6.chunk.js +0 -2
  82. package/dist-scalprum/static/532.146b1fd6.chunk.js.map +0 -1
  83. package/dist-scalprum/static/5453.29118045.chunk.js +0 -2
  84. package/dist-scalprum/static/5453.29118045.chunk.js.map +0 -1
  85. package/dist-scalprum/static/6281.eb6e16a2.chunk.js +0 -2
  86. package/dist-scalprum/static/6281.eb6e16a2.chunk.js.map +0 -1
  87. package/dist-scalprum/static/8365.75ea3581.chunk.js +0 -2
  88. package/dist-scalprum/static/8365.75ea3581.chunk.js.map +0 -1
  89. package/dist-scalprum/static/exposed-PluginRoot.4183ceef.chunk.js +0 -2
  90. package/dist-scalprum/static/exposed-PluginRoot.4183ceef.chunk.js.map +0 -1
  91. /package/dist-scalprum/static/{4556.c6bedfc4.chunk.js.LICENSE.txt → 5010.acf9a415.chunk.js.LICENSE.txt} +0 -0
  92. /package/dist-scalprum/static/{3483.2f14a8ca.chunk.js.LICENSE.txt → 8563.7e068fb0.chunk.js.LICENSE.txt} +0 -0
@@ -1,69 +1,188 @@
1
- import React, { useState } from 'react';
2
- import { InfoCard, Progress, Table, Link } from '@backstage/core-components';
3
- import { useApi, configApiRef, fetchApiRef, identityApiRef, alertApiRef } from '@backstage/core-plugin-api';
1
+ import React, { useState, useMemo } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { Progress, ResponseErrorPanel, Table, Link } from '@backstage/core-components';
4
+ import { useApi, configApiRef, fetchApiRef, alertApiRef } from '@backstage/core-plugin-api';
4
5
  import useAsync from 'react-use/lib/useAsync';
5
- import { Typography, Box, Tabs, Tab, Menu, MenuItem, Dialog, DialogTitle, DialogContent, DialogActions, Button, Chip, Tooltip, IconButton, CircularProgress } from '@material-ui/core';
6
+ import { makeStyles, Box, Typography, Menu, MenuItem, Dialog, DialogTitle, DialogContent, DialogActions, Button, Chip, Tooltip, IconButton, CircularProgress } from '@material-ui/core';
6
7
  import VisibilityIcon from '@material-ui/icons/Visibility';
7
8
  import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
8
- import MoreVertIcon from '@material-ui/icons/MoreVert';
9
9
  import FileCopyIcon from '@material-ui/icons/FileCopy';
10
10
  import WarningIcon from '@material-ui/icons/Warning';
11
+ import DeleteIcon from '@material-ui/icons/Delete';
11
12
  import { EditAPIKeyDialog } from '../EditAPIKeyDialog/EditAPIKeyDialog.esm.js';
12
13
  import { ConfirmDeleteDialog } from '../ConfirmDeleteDialog/ConfirmDeleteDialog.esm.js';
14
+ import { FilterPanel } from '../FilterPanel/FilterPanel.esm.js';
15
+ import { getStatusChipStyle } from '../../utils/styles.esm.js';
13
16
 
14
- const MyApiKeysCard = () => {
17
+ const useStyles = makeStyles((theme) => ({
18
+ container: {
19
+ display: "flex",
20
+ height: "100%",
21
+ minHeight: 400
22
+ },
23
+ tableContainer: {
24
+ flex: 1,
25
+ overflow: "auto"
26
+ },
27
+ useCasePanel: {
28
+ padding: theme.spacing(2),
29
+ backgroundColor: theme.palette.background.default
30
+ },
31
+ useCaseLabel: {
32
+ fontWeight: 600,
33
+ marginBottom: theme.spacing(1),
34
+ color: theme.palette.text.secondary,
35
+ textTransform: "uppercase",
36
+ fontSize: "0.75rem"
37
+ },
38
+ rejectedBanner: {
39
+ backgroundColor: theme.palette.error.light,
40
+ border: `1px solid ${theme.palette.error.main}`,
41
+ borderRadius: theme.shape.borderRadius,
42
+ padding: theme.spacing(1.5, 2),
43
+ marginBottom: theme.spacing(2),
44
+ display: "flex",
45
+ alignItems: "center",
46
+ gap: theme.spacing(1)
47
+ }
48
+ }));
49
+ const ExpandedRowContent = ({ request }) => {
50
+ const classes = useStyles();
51
+ const isRejected = request.status?.phase === "Rejected";
52
+ const apiProductName = request.spec.apiProductRef?.name || "unknown";
53
+ return /* @__PURE__ */ React.createElement(Box, { className: classes.useCasePanel, onClick: (e) => e.stopPropagation() }, isRejected && /* @__PURE__ */ React.createElement(Box, { className: classes.rejectedBanner }, /* @__PURE__ */ React.createElement(WarningIcon, { color: "error", fontSize: "small" }), /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, "This API key was rejected.", " ", /* @__PURE__ */ React.createElement(Link, { to: `/catalog/default/api/${apiProductName}/api-keys` }, "Request a new API key"))), /* @__PURE__ */ React.createElement(Typography, { className: classes.useCaseLabel }, "Use Case"), /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, request.spec.useCase || "No use case provided"));
54
+ };
55
+ const MyApiKeysTable = () => {
56
+ const classes = useStyles();
57
+ const navigate = useNavigate();
15
58
  const config = useApi(configApiRef);
16
59
  const fetchApi = useApi(fetchApiRef);
17
- const identityApi = useApi(identityApiRef);
18
60
  const alertApi = useApi(alertApiRef);
19
61
  const backendUrl = config.getString("backend.baseUrl");
20
- const [selectedTab, setSelectedTab] = useState(0);
21
- const [, setUserId] = useState("");
22
62
  const [visibleKeys, setVisibleKeys] = useState(/* @__PURE__ */ new Set());
23
63
  const [menuAnchor, setMenuAnchor] = useState(null);
24
64
  const [menuRequest, setMenuRequest] = useState(null);
25
- const [editDialogState, setEditDialogState] = useState({
26
- open: false,
27
- request: null,
28
- plans: []
29
- });
65
+ const [editDialogState, setEditDialogState] = useState({ open: false, request: null, plans: [] });
30
66
  const [refresh, setRefresh] = useState(0);
31
67
  const [deleting, setDeleting] = useState(null);
32
68
  const [deleteDialogState, setDeleteDialogState] = useState({ open: false, request: null });
33
- const [apiKeyValues, setApiKeyValues] = useState(/* @__PURE__ */ new Map());
69
+ const [apiKeyValues, setApiKeyValues] = useState(
70
+ /* @__PURE__ */ new Map()
71
+ );
34
72
  const [apiKeyLoading, setApiKeyLoading] = useState(/* @__PURE__ */ new Set());
35
- const [alreadyReadKeys, setAlreadyReadKeys] = useState(/* @__PURE__ */ new Set());
73
+ const [alreadyReadKeys, setAlreadyReadKeys] = useState(
74
+ /* @__PURE__ */ new Set()
75
+ );
36
76
  const [showOnceWarningOpen, setShowOnceWarningOpen] = useState(false);
37
77
  const [pendingKeyReveal, setPendingKeyReveal] = useState(null);
38
- useAsync(async () => {
39
- const identity = await identityApi.getBackstageIdentity();
40
- const extractedUserId = identity.userEntityRef.split("/")[1] || "guest";
41
- console.log(`MyApiKeysCard: setting userId from userEntityRef: ${identity.userEntityRef} -> "${extractedUserId}"`);
42
- setUserId(extractedUserId);
43
- }, [identityApi]);
44
78
  const [optimisticallyDeleted, setOptimisticallyDeleted] = useState(/* @__PURE__ */ new Set());
45
- const { value: requests, loading, error } = useAsync(async () => {
46
- const response = await fetchApi.fetch(
47
- `${backendUrl}/api/kuadrant/requests/my`
48
- );
49
- if (!response.ok) {
79
+ const [filters, setFilters] = useState({
80
+ status: [],
81
+ apiProduct: [],
82
+ tier: []
83
+ });
84
+ const {
85
+ value: data,
86
+ loading,
87
+ error
88
+ } = useAsync(async () => {
89
+ const [requestsResponse, productsResponse] = await Promise.all([
90
+ fetchApi.fetch(`${backendUrl}/api/kuadrant/requests/my`),
91
+ fetchApi.fetch(`${backendUrl}/api/kuadrant/apiproducts`)
92
+ ]);
93
+ if (!requestsResponse.ok) {
50
94
  throw new Error("failed to fetch requests");
51
95
  }
52
- const data = await response.json();
53
- return data.items || [];
96
+ const requestsData = await requestsResponse.json();
97
+ const requests = requestsData.items || [];
98
+ let products = [];
99
+ if (productsResponse.ok) {
100
+ const productsData = await productsResponse.json();
101
+ products = productsData.items || [];
102
+ }
103
+ const ownerMap = /* @__PURE__ */ new Map();
104
+ products.forEach((p) => {
105
+ const key = `${p.metadata.namespace}/${p.metadata.name}`;
106
+ const owner = p.metadata.annotations?.["backstage.io/owner"] || "unknown";
107
+ ownerMap.set(key, owner);
108
+ });
109
+ return { requests, products, ownerMap };
54
110
  }, [backendUrl, fetchApi, refresh]);
55
- if (loading) {
56
- return /* @__PURE__ */ React.createElement(InfoCard, { title: "My API Keys" }, /* @__PURE__ */ React.createElement(Progress, null));
57
- }
58
- if (error) {
59
- return /* @__PURE__ */ React.createElement(InfoCard, { title: "My API Keys" }, /* @__PURE__ */ React.createElement(Typography, { color: "error" }, "Error loading API keys: ", error.message));
60
- }
61
- const allRequests = (requests || []).filter(
62
- (r) => !optimisticallyDeleted.has(r.metadata.name)
63
- );
64
- const approvedRequests = allRequests.filter((r) => r.status?.phase === "Approved");
65
- const pendingRequests = allRequests.filter((r) => !r.status?.phase || r.status.phase === "Pending");
66
- const rejectedRequests = allRequests.filter((r) => r.status?.phase === "Rejected");
111
+ const allRequests = useMemo(() => {
112
+ if (!data?.requests) return [];
113
+ return data.requests.filter(
114
+ (r) => !optimisticallyDeleted.has(r.metadata.name)
115
+ );
116
+ }, [data?.requests, optimisticallyDeleted]);
117
+ const filterSections = useMemo(() => {
118
+ const statusCounts = { Approved: 0, Pending: 0, Rejected: 0 };
119
+ const apiProductCounts = /* @__PURE__ */ new Map();
120
+ const tierCounts = /* @__PURE__ */ new Map();
121
+ allRequests.forEach((r) => {
122
+ const status = r.status?.phase || "Pending";
123
+ statusCounts[status]++;
124
+ const apiProduct = r.spec.apiProductRef?.name || "unknown";
125
+ apiProductCounts.set(
126
+ apiProduct,
127
+ (apiProductCounts.get(apiProduct) || 0) + 1
128
+ );
129
+ const tier = r.spec.planTier || "unknown";
130
+ tierCounts.set(tier, (tierCounts.get(tier) || 0) + 1);
131
+ });
132
+ return [
133
+ {
134
+ id: "status",
135
+ title: "Status",
136
+ options: [
137
+ { value: "Approved", label: "Active", count: statusCounts.Approved },
138
+ { value: "Pending", label: "Pending", count: statusCounts.Pending },
139
+ {
140
+ value: "Rejected",
141
+ label: "Rejected",
142
+ count: statusCounts.Rejected
143
+ }
144
+ ]
145
+ },
146
+ {
147
+ id: "apiProduct",
148
+ title: "API Product",
149
+ options: Array.from(apiProductCounts.entries()).map(
150
+ ([name, count]) => ({
151
+ value: name,
152
+ label: name,
153
+ count
154
+ })
155
+ ),
156
+ collapsed: apiProductCounts.size > 5
157
+ },
158
+ {
159
+ id: "tier",
160
+ title: "Tier",
161
+ options: Array.from(tierCounts.entries()).map(([tier, count]) => ({
162
+ value: tier,
163
+ label: tier.charAt(0).toUpperCase() + tier.slice(1),
164
+ count
165
+ }))
166
+ }
167
+ ];
168
+ }, [allRequests]);
169
+ const filteredRequests = useMemo(() => {
170
+ return allRequests.filter((r) => {
171
+ if (filters.status.length > 0) {
172
+ const status = r.status?.phase || "Pending";
173
+ if (!filters.status.includes(status)) return false;
174
+ }
175
+ if (filters.apiProduct.length > 0) {
176
+ const apiProduct = r.spec.apiProductRef?.name || "unknown";
177
+ if (!filters.apiProduct.includes(apiProduct)) return false;
178
+ }
179
+ if (filters.tier.length > 0) {
180
+ const tier = r.spec.planTier || "unknown";
181
+ if (!filters.tier.includes(tier)) return false;
182
+ }
183
+ return true;
184
+ });
185
+ }, [allRequests, filters]);
67
186
  const toggleKeyVisibility = (keyName) => {
68
187
  setVisibleKeys((prev) => {
69
188
  const newSet = new Set(prev);
@@ -77,17 +196,15 @@ const MyApiKeysCard = () => {
77
196
  };
78
197
  const fetchApiKeyFromSecret = async (requestNamespace, requestName) => {
79
198
  const key = `${requestNamespace}/${requestName}`;
80
- if (apiKeyLoading.has(key)) {
81
- return;
82
- }
199
+ if (apiKeyLoading.has(key)) return;
83
200
  setApiKeyLoading((prev) => new Set(prev).add(key));
84
201
  try {
85
202
  const response = await fetchApi.fetch(
86
203
  `${backendUrl}/api/kuadrant/apikeys/${requestNamespace}/${requestName}/secret`
87
204
  );
88
205
  if (response.ok) {
89
- const data = await response.json();
90
- setApiKeyValues((prev) => new Map(prev).set(key, data.apiKey));
206
+ const result = await response.json();
207
+ setApiKeyValues((prev) => new Map(prev).set(key, result.apiKey));
91
208
  setAlreadyReadKeys((prev) => new Set(prev).add(key));
92
209
  } else if (response.status === 403) {
93
210
  setAlreadyReadKeys((prev) => new Set(prev).add(key));
@@ -134,7 +251,6 @@ const MyApiKeysCard = () => {
134
251
  const plans = apiProduct.spec?.plans || [];
135
252
  setEditDialogState({ open: true, request, plans });
136
253
  } else {
137
- console.error("Failed to fetch API product");
138
254
  setEditDialogState({ open: true, request, plans: [] });
139
255
  }
140
256
  } catch (err) {
@@ -163,7 +279,11 @@ const MyApiKeysCard = () => {
163
279
  throw new Error("Failed to delete request");
164
280
  }
165
281
  setRefresh((r) => r + 1);
166
- alertApi.post({ message: "Request deleted", severity: "success", display: "transient" });
282
+ alertApi.post({
283
+ message: "API key deleted",
284
+ severity: "success",
285
+ display: "transient"
286
+ });
167
287
  setDeleteDialogState({ open: false, request: null });
168
288
  } catch (err) {
169
289
  console.error("Error deleting request:", err);
@@ -172,7 +292,11 @@ const MyApiKeysCard = () => {
172
292
  next.delete(requestName);
173
293
  return next;
174
294
  });
175
- alertApi.post({ message: "Failed to delete request", severity: "error", display: "transient" });
295
+ alertApi.post({
296
+ message: "Failed to delete API key",
297
+ severity: "error",
298
+ display: "transient"
299
+ });
176
300
  } finally {
177
301
  setDeleting(null);
178
302
  }
@@ -190,33 +314,13 @@ const MyApiKeysCard = () => {
190
314
  }
191
315
  },
192
316
  {
193
- title: "Tier",
194
- field: "spec.planTier",
195
- render: (row) => {
196
- const color = row.spec.planTier === "gold" ? "primary" : row.spec.planTier === "silver" ? "default" : "secondary";
197
- return /* @__PURE__ */ React.createElement(Chip, { label: row.spec.planTier, color, size: "small" });
198
- }
199
- },
200
- {
201
- title: "Use Case",
202
- field: "spec.useCase",
317
+ title: "Owner",
318
+ field: "owner",
203
319
  render: (row) => {
204
- if (!row.spec.useCase) {
205
- return /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, "-");
206
- }
207
- return /* @__PURE__ */ React.createElement(Tooltip, { title: row.spec.useCase, placement: "top" }, /* @__PURE__ */ React.createElement(
208
- Typography,
209
- {
210
- variant: "body2",
211
- style: {
212
- maxWidth: "200px",
213
- overflow: "hidden",
214
- textOverflow: "ellipsis",
215
- whiteSpace: "nowrap"
216
- }
217
- },
218
- row.spec.useCase
219
- ));
320
+ const key = `${row.metadata.namespace}/${row.spec.apiProductRef?.name}`;
321
+ const owner = data?.ownerMap?.get(key) || "unknown";
322
+ const displayOwner = owner.replace(/^user:default\//, "");
323
+ return /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, displayOwner);
220
324
  }
221
325
  },
222
326
  {
@@ -224,20 +328,14 @@ const MyApiKeysCard = () => {
224
328
  field: "status.phase",
225
329
  render: (row) => {
226
330
  const phase = row.status?.phase || "Pending";
227
- const color = phase === "Approved" ? "primary" : phase === "Rejected" ? "secondary" : "default";
228
- return /* @__PURE__ */ React.createElement(Chip, { label: phase, color, size: "small" });
331
+ const label = phase === "Approved" ? "Active" : phase;
332
+ return /* @__PURE__ */ React.createElement(Chip, { label, size: "small", style: getStatusChipStyle(phase) });
229
333
  }
230
334
  },
231
335
  {
232
- title: "Reviewed By",
233
- field: "status.reviewedBy",
234
- render: (row) => {
235
- if ((row.status?.phase === "Approved" || row.status?.phase === "Rejected") && row.status.reviewedBy) {
236
- const reviewedDate = row.status.reviewedAt ? new Date(row.status.reviewedAt).toLocaleDateString() : "";
237
- return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, row.status.reviewedBy), reviewedDate && /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "textSecondary" }, reviewedDate));
238
- }
239
- return /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "-");
240
- }
336
+ title: "Tier",
337
+ field: "spec.planTier",
338
+ render: (row) => /* @__PURE__ */ React.createElement(Chip, { label: row.spec.planTier, size: "small", variant: "outlined" })
241
339
  },
242
340
  {
243
341
  title: "API Key",
@@ -273,7 +371,10 @@ const MyApiKeysCard = () => {
273
371
  clearApiKeyValue(row.metadata.namespace, row.metadata.name);
274
372
  toggleKeyVisibility(row.metadata.name);
275
373
  } else if (!isAlreadyRead) {
276
- setPendingKeyReveal({ namespace: row.metadata.namespace, name: row.metadata.name });
374
+ setPendingKeyReveal({
375
+ namespace: row.metadata.namespace,
376
+ name: row.metadata.name
377
+ });
277
378
  setShowOnceWarningOpen(true);
278
379
  }
279
380
  };
@@ -287,15 +388,21 @@ const MyApiKeysCard = () => {
287
388
  });
288
389
  }
289
390
  };
290
- return /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", style: { gap: 8 } }, /* @__PURE__ */ React.createElement(Box, { fontFamily: "monospace", fontSize: "0.875rem" }, isLoading ? "Loading..." : isVisible && apiKeyValue ? apiKeyValue : "\u2022".repeat(20) + "..."), isVisible && apiKeyValue && /* @__PURE__ */ React.createElement(Tooltip, { title: "Copy to clipboard" }, /* @__PURE__ */ React.createElement(IconButton, { size: "small", onClick: handleCopy }, /* @__PURE__ */ React.createElement(FileCopyIcon, { fontSize: "small" }))), /* @__PURE__ */ React.createElement(Tooltip, { title: isVisible ? "Hide API key" : "Reveal API key (one-time only)" }, /* @__PURE__ */ React.createElement("span", null, /* @__PURE__ */ React.createElement(
291
- IconButton,
391
+ return /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", style: { gap: 8 } }, /* @__PURE__ */ React.createElement(Box, { fontFamily: "monospace", fontSize: "0.875rem" }, isLoading ? "Loading..." : isVisible && apiKeyValue ? apiKeyValue : "\u2022".repeat(20) + "..."), isVisible && apiKeyValue && /* @__PURE__ */ React.createElement(Tooltip, { title: "Copy to clipboard" }, /* @__PURE__ */ React.createElement(IconButton, { size: "small", onClick: handleCopy }, /* @__PURE__ */ React.createElement(FileCopyIcon, { fontSize: "small" }))), /* @__PURE__ */ React.createElement(
392
+ Tooltip,
292
393
  {
293
- size: "small",
294
- onClick: handleRevealClick,
295
- disabled: isLoading || isAlreadyRead && !apiKeyValue
394
+ title: isVisible ? "Hide API key" : "Reveal API key (one-time only)"
296
395
  },
297
- isVisible ? /* @__PURE__ */ React.createElement(VisibilityOffIcon, { fontSize: "small" }) : /* @__PURE__ */ React.createElement(VisibilityIcon, { fontSize: "small" })
298
- ))));
396
+ /* @__PURE__ */ React.createElement("span", null, /* @__PURE__ */ React.createElement(
397
+ IconButton,
398
+ {
399
+ size: "small",
400
+ onClick: handleRevealClick,
401
+ disabled: isLoading || isAlreadyRead && !apiKeyValue
402
+ },
403
+ isVisible ? /* @__PURE__ */ React.createElement(VisibilityOffIcon, { fontSize: "small" }) : /* @__PURE__ */ React.createElement(VisibilityIcon, { fontSize: "small" })
404
+ ))
405
+ ));
299
406
  }
300
407
  },
301
408
  {
@@ -310,99 +417,88 @@ const MyApiKeysCard = () => {
310
417
  }
311
418
  },
312
419
  {
313
- title: "",
420
+ title: "Actions",
314
421
  filtering: false,
422
+ width: "100px",
315
423
  render: (row) => {
316
424
  const isDeleting = deleting === row.metadata.name;
317
425
  if (isDeleting) {
318
426
  return /* @__PURE__ */ React.createElement(CircularProgress, { size: 20 });
319
427
  }
320
- return /* @__PURE__ */ React.createElement(
428
+ return /* @__PURE__ */ React.createElement(Box, { display: "flex", style: { gap: 4 } }, /* @__PURE__ */ React.createElement(Tooltip, { title: "View details" }, /* @__PURE__ */ React.createElement(
321
429
  IconButton,
322
430
  {
323
431
  size: "small",
324
432
  onClick: (e) => {
325
433
  e.stopPropagation();
326
- const rect = e.currentTarget.getBoundingClientRect();
327
- setMenuAnchor({ top: rect.bottom, left: rect.left });
328
- setMenuRequest(row);
329
- },
330
- "aria-controls": menuAnchor ? "myapikeys-menu" : undefined,
331
- "aria-haspopup": "true"
434
+ navigate(
435
+ `/kuadrant/api-keys/${row.metadata.namespace}/${row.metadata.name}`
436
+ );
437
+ }
332
438
  },
333
- /* @__PURE__ */ React.createElement(MoreVertIcon, null)
334
- );
439
+ /* @__PURE__ */ React.createElement(VisibilityIcon, { fontSize: "small" })
440
+ )), /* @__PURE__ */ React.createElement(Tooltip, { title: "Delete" }, /* @__PURE__ */ React.createElement(
441
+ IconButton,
442
+ {
443
+ size: "small",
444
+ onClick: (e) => {
445
+ e.stopPropagation();
446
+ setDeleteDialogState({ open: true, request: row });
447
+ }
448
+ },
449
+ /* @__PURE__ */ React.createElement(DeleteIcon, { fontSize: "small" })
450
+ )));
335
451
  }
336
452
  }
337
453
  ];
338
- const getTabData = () => {
339
- switch (selectedTab) {
340
- case 0:
341
- return approvedRequests;
342
- case 1:
343
- return pendingRequests;
344
- case 2:
345
- return rejectedRequests;
346
- default:
347
- return allRequests;
348
- }
349
- };
350
- const getTabColumns = () => {
351
- switch (selectedTab) {
352
- case 0:
353
- return columns.filter((col) => col.title !== "Reason");
354
- case 1:
355
- return columns.filter(
356
- (col) => col.title !== "Reason" && col.title !== "Reviewed By" && col.title !== "API Key"
357
- );
358
- case 2:
359
- return columns.filter((col) => col.title !== "API Key");
360
- default:
361
- return columns;
362
- }
363
- };
364
- const tabData = getTabData();
365
- const tabColumns = getTabColumns();
454
+ const detailPanelConfig = useMemo(
455
+ () => [
456
+ {
457
+ render: (data2) => {
458
+ const request = data2.rowData;
459
+ if (!request?.metadata?.name) {
460
+ return /* @__PURE__ */ React.createElement(Box, null);
461
+ }
462
+ return /* @__PURE__ */ React.createElement(ExpandedRowContent, { request });
463
+ }
464
+ }
465
+ ],
466
+ []
467
+ );
468
+ if (loading) {
469
+ return /* @__PURE__ */ React.createElement(Progress, null);
470
+ }
471
+ if (error) {
472
+ return /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error });
473
+ }
366
474
  const isPending = (row) => !row.status || row.status.phase === "Pending";
367
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
368
- InfoCard,
475
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { className: classes.container }, /* @__PURE__ */ React.createElement(
476
+ FilterPanel,
369
477
  {
370
- title: "My API Keys",
371
- subheader: `${approvedRequests.length} active, ${pendingRequests.length} pending`
372
- },
373
- /* @__PURE__ */ React.createElement(Box, { mb: 2, "data-testid": "my-api-keys-card" }, /* @__PURE__ */ React.createElement(
374
- Tabs,
375
- {
376
- value: selectedTab,
377
- onChange: (_, newValue) => setSelectedTab(newValue),
378
- indicatorColor: "primary",
379
- textColor: "primary",
380
- "data-testid": "my-api-keys-tabs"
478
+ sections: filterSections,
479
+ filters,
480
+ onChange: setFilters
481
+ }
482
+ ), /* @__PURE__ */ React.createElement(Box, { className: classes.tableContainer }, filteredRequests.length === 0 ? /* @__PURE__ */ React.createElement(Box, { p: 4, textAlign: "center" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body1", color: "textSecondary" }, allRequests.length === 0 ? "No API keys found. Request access to an API to get started." : "No API keys match the selected filters.")) : /* @__PURE__ */ React.createElement(
483
+ Table,
484
+ {
485
+ options: {
486
+ paging: filteredRequests.length > 10,
487
+ pageSize: 20,
488
+ search: true,
489
+ filtering: false,
490
+ debounceInterval: 300,
491
+ toolbar: true,
492
+ emptyRowsWhenPaging: false
381
493
  },
382
- /* @__PURE__ */ React.createElement(Tab, { label: `Active (${approvedRequests.length})`, "data-testid": "my-api-keys-active-tab" }),
383
- /* @__PURE__ */ React.createElement(Tab, { label: `Pending (${pendingRequests.length})`, "data-testid": "my-api-keys-pending-tab" }),
384
- /* @__PURE__ */ React.createElement(Tab, { label: `Rejected (${rejectedRequests.length})`, "data-testid": "my-api-keys-rejected-tab" })
385
- )),
386
- tabData.length === 0 ? /* @__PURE__ */ React.createElement(Box, { p: 3, textAlign: "center" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body1", color: "textSecondary" }, selectedTab === 0 && "No active API keys. Request access to an API to get started.", selectedTab === 1 && "No pending requests.", selectedTab === 2 && "No rejected requests.")) : /* @__PURE__ */ React.createElement(
387
- Table,
388
- {
389
- options: {
390
- paging: tabData.length > 5,
391
- pageSize: 20,
392
- search: true,
393
- filtering: true,
394
- debounceInterval: 300,
395
- toolbar: true,
396
- emptyRowsWhenPaging: false
397
- },
398
- columns: tabColumns,
399
- data: tabData.map((item) => ({
400
- ...item,
401
- id: item.metadata.name
402
- }))
403
- }
404
- )
405
- ), /* @__PURE__ */ React.createElement(
494
+ columns,
495
+ data: filteredRequests.map((item) => ({
496
+ ...item,
497
+ id: item.metadata.name
498
+ })),
499
+ detailPanel: detailPanelConfig
500
+ }
501
+ ))), /* @__PURE__ */ React.createElement(
406
502
  Menu,
407
503
  {
408
504
  id: "myapikeys-menu",
@@ -413,10 +509,29 @@ const MyApiKeysCard = () => {
413
509
  },
414
510
  menuRequest && (() => {
415
511
  const items = [];
512
+ items.push(
513
+ /* @__PURE__ */ React.createElement(
514
+ MenuItem,
515
+ {
516
+ key: "view",
517
+ onClick: () => {
518
+ navigate(
519
+ `/kuadrant/api-keys/${menuRequest.metadata.namespace}/${menuRequest.metadata.name}`
520
+ );
521
+ handleMenuClose();
522
+ }
523
+ },
524
+ "View Details"
525
+ )
526
+ );
416
527
  if (isPending(menuRequest)) {
417
- items.push(/* @__PURE__ */ React.createElement(MenuItem, { key: "edit", onClick: handleEdit }, "Edit"));
528
+ items.push(
529
+ /* @__PURE__ */ React.createElement(MenuItem, { key: "edit", onClick: handleEdit }, "Edit")
530
+ );
418
531
  }
419
- items.push(/* @__PURE__ */ React.createElement(MenuItem, { key: "delete", onClick: handleDeleteClick }, "Delete"));
532
+ items.push(
533
+ /* @__PURE__ */ React.createElement(MenuItem, { key: "delete", onClick: handleDeleteClick }, "Delete")
534
+ );
420
535
  return items;
421
536
  })()
422
537
  ), editDialogState.request && /* @__PURE__ */ React.createElement(
@@ -435,8 +550,8 @@ const MyApiKeysCard = () => {
435
550
  ConfirmDeleteDialog,
436
551
  {
437
552
  open: deleteDialogState.open,
438
- title: "Delete API Key Request",
439
- description: `Are you sure you want to delete the API key request for ${deleteDialogState.request?.spec.apiProductRef?.name || "this API"}?`,
553
+ title: "Delete API Key",
554
+ description: `Are you sure you want to delete this API key for ${deleteDialogState.request?.spec.apiProductRef?.name || "this API"}?`,
440
555
  deleting: deleting !== null,
441
556
  onConfirm: handleDeleteConfirm,
442
557
  onCancel: handleDeleteCancel
@@ -469,7 +584,10 @@ const MyApiKeysCard = () => {
469
584
  color: "primary",
470
585
  onClick: () => {
471
586
  if (pendingKeyReveal) {
472
- fetchApiKeyFromSecret(pendingKeyReveal.namespace, pendingKeyReveal.name);
587
+ fetchApiKeyFromSecret(
588
+ pendingKeyReveal.namespace,
589
+ pendingKeyReveal.name
590
+ );
473
591
  toggleKeyVisibility(pendingKeyReveal.name);
474
592
  }
475
593
  setShowOnceWarningOpen(false);
@@ -481,5 +599,5 @@ const MyApiKeysCard = () => {
481
599
  ));
482
600
  };
483
601
 
484
- export { MyApiKeysCard };
485
- //# sourceMappingURL=MyApiKeysCard.esm.js.map
602
+ export { MyApiKeysTable };
603
+ //# sourceMappingURL=MyApiKeysTable.esm.js.map