@kuadrant/kuadrant-backstage-plugin-frontend 0.0.2-dev-3d7caf4 → 0.0.2-dev-415994c

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 (76) hide show
  1. package/dist/assets/empty-state-illustration.png +0 -0
  2. package/dist/components/ApiProductDetailPage/ApiProductDetailPage.esm.js +248 -0
  3. package/dist/components/ApiProductDetailPage/ApiProductDetailPage.esm.js.map +1 -0
  4. package/dist/components/ApiProductDetailPage/index.esm.js +2 -0
  5. package/dist/components/ApiProductDetailPage/index.esm.js.map +1 -0
  6. package/dist/components/ApiProductDetails/ApiProductDetails.esm.js +86 -0
  7. package/dist/components/ApiProductDetails/ApiProductDetails.esm.js.map +1 -0
  8. package/dist/components/ApiProductInfoCard/ApiProductInfoCard.esm.js +24 -43
  9. package/dist/components/ApiProductInfoCard/ApiProductInfoCard.esm.js.map +1 -1
  10. package/dist/components/ApprovalQueueTable/ApprovalQueueTable.esm.js +2 -1
  11. package/dist/components/ApprovalQueueTable/ApprovalQueueTable.esm.js.map +1 -1
  12. package/dist/components/CreateAPIProductDialog/CreateAPIProductDialog.esm.js +148 -134
  13. package/dist/components/CreateAPIProductDialog/CreateAPIProductDialog.esm.js.map +1 -1
  14. package/dist/components/EditAPIProductDialog/EditAPIProductDialog.esm.js +104 -128
  15. package/dist/components/EditAPIProductDialog/EditAPIProductDialog.esm.js.map +1 -1
  16. package/dist/components/KuadrantPage/KuadrantPage.esm.js +328 -125
  17. package/dist/components/KuadrantPage/KuadrantPage.esm.js.map +1 -1
  18. package/dist/components/MyApiKeysTable/MyApiKeysTable.esm.js +2 -1
  19. package/dist/components/MyApiKeysTable/MyApiKeysTable.esm.js.map +1 -1
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.esm.js +1 -1
  22. package/dist/plugin.esm.js +8 -1
  23. package/dist/plugin.esm.js.map +1 -1
  24. package/dist/utils/validation.esm.js +1 -11
  25. package/dist/utils/validation.esm.js.map +1 -1
  26. package/dist-scalprum/{internal.plugin-kuadrant.793ab10dddb55e70abe2.js → internal.plugin-kuadrant.47663f119ccb78d7ec47.js} +2 -2
  27. package/dist-scalprum/internal.plugin-kuadrant.47663f119ccb78d7ec47.js.map +1 -0
  28. package/dist-scalprum/plugin-manifest.json +2 -2
  29. package/dist-scalprum/static/2759.fceb317f.chunk.js +2 -0
  30. package/dist-scalprum/static/2759.fceb317f.chunk.js.map +1 -0
  31. package/dist-scalprum/static/2928.4303c12e.chunk.js +3 -0
  32. package/dist-scalprum/static/2928.4303c12e.chunk.js.map +1 -0
  33. package/dist-scalprum/static/2967.ac3a4bee.chunk.js +2 -0
  34. package/dist-scalprum/static/2967.ac3a4bee.chunk.js.map +1 -0
  35. package/dist-scalprum/static/{6979.9699b350.chunk.js → 2987.1da15560.chunk.js} +2 -2
  36. package/dist-scalprum/static/2987.1da15560.chunk.js.map +1 -0
  37. package/dist-scalprum/static/3459.5c90b5a3.chunk.js +2 -0
  38. package/dist-scalprum/static/3459.5c90b5a3.chunk.js.map +1 -0
  39. package/dist-scalprum/static/3503.66b6e510.chunk.js +2 -0
  40. package/dist-scalprum/static/3503.66b6e510.chunk.js.map +1 -0
  41. package/dist-scalprum/static/4682.9ee4e285.chunk.js +2 -0
  42. package/dist-scalprum/static/4682.9ee4e285.chunk.js.map +1 -0
  43. package/dist-scalprum/static/5010.a4aa0f8e.chunk.js +3 -0
  44. package/dist-scalprum/static/5010.a4aa0f8e.chunk.js.map +1 -0
  45. package/dist-scalprum/static/5453.280127dd.chunk.js +2 -0
  46. package/dist-scalprum/static/5453.280127dd.chunk.js.map +1 -0
  47. package/dist-scalprum/static/6422.97baf774.chunk.js +2 -0
  48. package/dist-scalprum/static/6422.97baf774.chunk.js.map +1 -0
  49. package/dist-scalprum/static/7791.12162a71.chunk.js +2 -0
  50. package/dist-scalprum/static/7791.12162a71.chunk.js.map +1 -0
  51. package/dist-scalprum/static/8799.7c749838.chunk.js +2 -0
  52. package/dist-scalprum/static/8799.7c749838.chunk.js.map +1 -0
  53. package/dist-scalprum/static/empty-state-illustration.7e3ad5a9..png +0 -0
  54. package/dist-scalprum/static/exposed-PluginRoot.a5792fb2.chunk.js +2 -0
  55. package/dist-scalprum/static/exposed-PluginRoot.a5792fb2.chunk.js.map +1 -0
  56. package/package.json +1 -1
  57. package/dist-scalprum/internal.plugin-kuadrant.793ab10dddb55e70abe2.js.map +0 -1
  58. package/dist-scalprum/static/2120.44884438.chunk.js +0 -3
  59. package/dist-scalprum/static/2120.44884438.chunk.js.map +0 -1
  60. package/dist-scalprum/static/2967.c684efaf.chunk.js +0 -2
  61. package/dist-scalprum/static/2967.c684efaf.chunk.js.map +0 -1
  62. package/dist-scalprum/static/5010.acf9a415.chunk.js +0 -3
  63. package/dist-scalprum/static/5010.acf9a415.chunk.js.map +0 -1
  64. package/dist-scalprum/static/5453.c1f90bdf.chunk.js +0 -2
  65. package/dist-scalprum/static/5453.c1f90bdf.chunk.js.map +0 -1
  66. package/dist-scalprum/static/6813.036a322f.chunk.js +0 -2
  67. package/dist-scalprum/static/6813.036a322f.chunk.js.map +0 -1
  68. package/dist-scalprum/static/6979.9699b350.chunk.js.map +0 -1
  69. package/dist-scalprum/static/7684.3d1fc1a1.chunk.js +0 -2
  70. package/dist-scalprum/static/7684.3d1fc1a1.chunk.js.map +0 -1
  71. package/dist-scalprum/static/8416.3604a311.chunk.js +0 -2
  72. package/dist-scalprum/static/8416.3604a311.chunk.js.map +0 -1
  73. package/dist-scalprum/static/exposed-PluginRoot.16bf7897.chunk.js +0 -2
  74. package/dist-scalprum/static/exposed-PluginRoot.16bf7897.chunk.js.map +0 -1
  75. /package/dist-scalprum/static/{2120.44884438.chunk.js.LICENSE.txt → 2928.4303c12e.chunk.js.LICENSE.txt} +0 -0
  76. /package/dist-scalprum/static/{5010.acf9a415.chunk.js.LICENSE.txt → 5010.a4aa0f8e.chunk.js.LICENSE.txt} +0 -0
@@ -1,11 +1,12 @@
1
- import React, { useState } from 'react';
2
- import { Box, Typography, Grid, Button, Chip, IconButton } from '@material-ui/core';
1
+ import React, { useState, useCallback, useMemo } from 'react';
2
+ import { makeStyles, Box, CircularProgress, Typography, Button, Chip, IconButton } from '@material-ui/core';
3
3
  import AddIcon from '@material-ui/icons/Add';
4
4
  import DeleteIcon from '@material-ui/icons/Delete';
5
5
  import EditIcon from '@material-ui/icons/Edit';
6
6
  import VpnKeyIcon from '@material-ui/icons/VpnKey';
7
7
  import LockIcon from '@material-ui/icons/Lock';
8
- import { Page, Header, SupportButton, Content, Progress, ResponseErrorPanel, InfoCard, Table, Link } from '@backstage/core-components';
8
+ import { FilterPanel } from '../FilterPanel/FilterPanel.esm.js';
9
+ import { Page, Header, SupportButton, Content, ResponseErrorPanel, Table, Link } from '@backstage/core-components';
9
10
  import useAsync from 'react-use/lib/useAsync';
10
11
  import { useApi, configApiRef, fetchApiRef, alertApiRef, identityApiRef } from '@backstage/core-plugin-api';
11
12
  import { PermissionGate } from '../PermissionGate/PermissionGate.esm.js';
@@ -14,8 +15,49 @@ import { kuadrantApiProductListPermission, kuadrantApiProductCreatePermission, k
14
15
  import { useKuadrantPermission } from '../../utils/permissions.esm.js';
15
16
  import { EditAPIProductDialog } from '../EditAPIProductDialog/EditAPIProductDialog.esm.js';
16
17
  import { ConfirmDeleteDialog } from '../ConfirmDeleteDialog/ConfirmDeleteDialog.esm.js';
18
+ import emptyStateIllustration from '../../assets/empty-state-illustration.png';
17
19
 
20
+ const useStyles = makeStyles((theme) => ({
21
+ container: {
22
+ display: "flex",
23
+ height: "100%",
24
+ minHeight: 400
25
+ },
26
+ tableContainer: {
27
+ flex: 1,
28
+ overflow: "auto",
29
+ padding: 10
30
+ },
31
+ emptyState: {
32
+ display: "flex",
33
+ alignItems: "center",
34
+ justifyContent: "center",
35
+ padding: theme.spacing(6),
36
+ minHeight: 400
37
+ },
38
+ emptyStateContent: {
39
+ display: "flex",
40
+ alignItems: "center",
41
+ gap: theme.spacing(6),
42
+ maxWidth: 900
43
+ },
44
+ emptyStateText: {
45
+ flex: 1
46
+ },
47
+ emptyStateTitle: {
48
+ marginBottom: theme.spacing(2)
49
+ },
50
+ emptyStateDescription: {
51
+ marginBottom: theme.spacing(3),
52
+ color: theme.palette.text.secondary
53
+ },
54
+ emptyStateImage: {
55
+ maxWidth: 400,
56
+ height: "auto"
57
+ }
58
+ }));
18
59
  const ResourceList = () => {
60
+ const classes = useStyles();
19
61
  const config = useApi(configApiRef);
20
62
  const fetchApi = useApi(fetchApiRef);
21
63
  const alertApi = useApi(alertApiRef);
@@ -30,6 +72,14 @@ const ResourceList = () => {
30
72
  const [apiProductToEdit, setApiProductToEdit] = useState(null);
31
73
  const [deleting, setDeleting] = useState(false);
32
74
  const [deleteStats, setDeleteStats] = useState(null);
75
+ const [filters, setFilters] = useState({
76
+ status: [],
77
+ policy: [],
78
+ route: [],
79
+ namespace: [],
80
+ tags: [],
81
+ authentication: []
82
+ });
33
83
  const {
34
84
  allowed: canCreateApiProduct,
35
85
  loading: createPermissionLoading,
@@ -52,7 +102,6 @@ const ResourceList = () => {
52
102
  );
53
103
  const deletePermissionLoading = deleteOwnPermissionLoading || deleteAllPermissionLoading;
54
104
  const {
55
- allowed: canListPlanPolicies,
56
105
  loading: planPolicyPermissionLoading,
57
106
  error: planPolicyPermissionError
58
107
  } = useKuadrantPermission(kuadrantPlanPolicyListPermission);
@@ -80,13 +129,154 @@ const ResourceList = () => {
80
129
  );
81
130
  return await response.json();
82
131
  }, [backendUrl, fetchApi, refreshTrigger]);
132
+ const getPolicyForProduct = useCallback((product) => {
133
+ if (!planPolicies?.items) return null;
134
+ const targetRef = product.spec?.targetRef;
135
+ if (!targetRef) return null;
136
+ const policy = planPolicies.items.find((pp) => {
137
+ const ref = pp.targetRef;
138
+ return ref?.kind === "HTTPRoute" && ref?.name === targetRef.name && (!ref?.namespace || ref?.namespace === (targetRef.namespace || product.metadata.namespace));
139
+ });
140
+ return policy?.metadata.name || null;
141
+ }, [planPolicies]);
142
+ const getAuthSchemes = useCallback((product) => {
143
+ const authSchemes = product.status?.discoveredAuthScheme?.authentication || {};
144
+ const schemeObjects = Object.values(authSchemes);
145
+ const schemes = [];
146
+ if (schemeObjects.some((scheme) => scheme.hasOwnProperty("apiKey"))) {
147
+ schemes.push("API Key");
148
+ }
149
+ if (schemeObjects.some((scheme) => scheme.hasOwnProperty("jwt"))) {
150
+ schemes.push("OIDC");
151
+ }
152
+ if (schemes.length === 0) {
153
+ schemes.push("Unknown");
154
+ }
155
+ return schemes;
156
+ }, []);
83
157
  const loading = apiProductsLoading || planPoliciesLoading || createPermissionLoading || deletePermissionLoading || planPolicyPermissionLoading;
84
158
  const error = apiProductsError || planPoliciesError;
85
159
  const permissionError = createPermissionError || deletePermissionError || planPolicyPermissionError;
86
- const handleCreateSuccess = () => {
160
+ const allProducts = useMemo(() => {
161
+ return apiProducts?.items || [];
162
+ }, [apiProducts]);
163
+ const filterSections = useMemo(() => {
164
+ const statusCounts = { Draft: 0, Published: 0 };
165
+ const policyCounts = /* @__PURE__ */ new Map();
166
+ const routeCounts = /* @__PURE__ */ new Map();
167
+ const namespaceCounts = /* @__PURE__ */ new Map();
168
+ const tagCounts = /* @__PURE__ */ new Map();
169
+ const authCounts = /* @__PURE__ */ new Map();
170
+ allProducts.forEach((p) => {
171
+ const status = p.spec?.publishStatus || "Draft";
172
+ statusCounts[status]++;
173
+ const policy = getPolicyForProduct(p) || "N/A";
174
+ policyCounts.set(policy, (policyCounts.get(policy) || 0) + 1);
175
+ const route = p.spec?.targetRef?.name || "unknown";
176
+ routeCounts.set(route, (routeCounts.get(route) || 0) + 1);
177
+ const ns = p.metadata.namespace;
178
+ namespaceCounts.set(ns, (namespaceCounts.get(ns) || 0) + 1);
179
+ const tags = p.spec?.tags || [];
180
+ tags.forEach((tag) => {
181
+ tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
182
+ });
183
+ const authSchemes = getAuthSchemes(p);
184
+ authSchemes.forEach((scheme) => {
185
+ authCounts.set(scheme, (authCounts.get(scheme) || 0) + 1);
186
+ });
187
+ });
188
+ return [
189
+ {
190
+ id: "status",
191
+ title: "Status",
192
+ options: [
193
+ { value: "Draft", label: "Draft", count: statusCounts.Draft },
194
+ { value: "Published", label: "Published", count: statusCounts.Published }
195
+ ]
196
+ },
197
+ {
198
+ id: "authentication",
199
+ title: "Authentication",
200
+ options: Array.from(authCounts.entries()).map(([scheme, count]) => ({
201
+ value: scheme,
202
+ label: scheme,
203
+ count
204
+ }))
205
+ },
206
+ {
207
+ id: "policy",
208
+ title: "Policy",
209
+ options: Array.from(policyCounts.entries()).map(([name, count]) => ({
210
+ value: name,
211
+ label: name,
212
+ count
213
+ })),
214
+ collapsed: policyCounts.size > 5
215
+ },
216
+ {
217
+ id: "route",
218
+ title: "Route",
219
+ options: Array.from(routeCounts.entries()).map(([name, count]) => ({
220
+ value: name,
221
+ label: name,
222
+ count
223
+ })),
224
+ collapsed: routeCounts.size > 5
225
+ },
226
+ {
227
+ id: "namespace",
228
+ title: "Namespace",
229
+ options: Array.from(namespaceCounts.entries()).map(([ns, count]) => ({
230
+ value: ns,
231
+ label: ns,
232
+ count
233
+ })),
234
+ collapsed: namespaceCounts.size > 5
235
+ },
236
+ {
237
+ id: "tags",
238
+ title: "Tags",
239
+ options: Array.from(tagCounts.entries()).map(([tag, count]) => ({
240
+ value: tag,
241
+ label: tag,
242
+ count
243
+ })),
244
+ collapsed: tagCounts.size > 5
245
+ }
246
+ ];
247
+ }, [allProducts, getPolicyForProduct, getAuthSchemes]);
248
+ const filteredProducts = useMemo(() => {
249
+ return allProducts.filter((p) => {
250
+ if (filters.status.length > 0) {
251
+ const status = p.spec?.publishStatus || "Draft";
252
+ if (!filters.status.includes(status)) return false;
253
+ }
254
+ if (filters.authentication.length > 0) {
255
+ const authSchemes = getAuthSchemes(p);
256
+ if (!filters.authentication.some((a) => authSchemes.includes(a))) return false;
257
+ }
258
+ if (filters.policy.length > 0) {
259
+ const policy = getPolicyForProduct(p) || "N/A";
260
+ if (!filters.policy.includes(policy)) return false;
261
+ }
262
+ if (filters.route.length > 0) {
263
+ const route = p.spec?.targetRef?.name || "unknown";
264
+ if (!filters.route.includes(route)) return false;
265
+ }
266
+ if (filters.namespace.length > 0) {
267
+ if (!filters.namespace.includes(p.metadata.namespace)) return false;
268
+ }
269
+ if (filters.tags.length > 0) {
270
+ const tags = p.spec?.tags || [];
271
+ if (!filters.tags.some((t) => tags.includes(t))) return false;
272
+ }
273
+ return true;
274
+ });
275
+ }, [allProducts, filters, getPolicyForProduct, getAuthSchemes]);
276
+ const handleCreateSuccess = (productInfo) => {
87
277
  setRefreshTrigger((prev) => prev + 1);
88
278
  alertApi.post({
89
- message: "API Product created",
279
+ message: `"${productInfo.displayName}" created successfully`,
90
280
  severity: "success",
91
281
  display: "transient"
92
282
  });
@@ -97,8 +287,9 @@ const ResourceList = () => {
97
287
  };
98
288
  const handleEditSuccess = () => {
99
289
  setRefreshTrigger((prev) => prev + 1);
290
+ const productName = apiProductToEdit?.name || "API Product";
100
291
  alertApi.post({
101
- message: "API Product updated",
292
+ message: `"${productName}" updated successfully`,
102
293
  severity: "success",
103
294
  display: "transient"
104
295
  });
@@ -136,9 +327,10 @@ const ResourceList = () => {
136
327
  if (!response.ok) {
137
328
  throw new Error("failed to delete apiproduct");
138
329
  }
330
+ const deletedName = apiProductToDelete?.name || "API Product";
139
331
  setRefreshTrigger((prev) => prev + 1);
140
332
  alertApi.post({
141
- message: "API Product deleted",
333
+ message: `"${deletedName}" deleted successfully`,
142
334
  severity: "success",
143
335
  display: "transient"
144
336
  });
@@ -159,72 +351,89 @@ const ResourceList = () => {
159
351
  setDeleteDialogOpen(false);
160
352
  setApiProductToDelete(null);
161
353
  };
162
- const formatDate = (dateString) => {
163
- const date = new Date(dateString);
164
- return date.toLocaleDateString("en-GB", {
165
- year: "numeric",
166
- month: "short",
167
- day: "numeric"
168
- });
354
+ const handlePublishToggle = async (row) => {
355
+ const namespace = row.metadata.namespace;
356
+ const name = row.metadata.name;
357
+ const displayName = row.spec?.displayName || name;
358
+ const currentStatus = row.spec?.publishStatus || "Draft";
359
+ const newStatus = currentStatus === "Published" ? "Draft" : "Published";
360
+ try {
361
+ const response = await fetchApi.fetch(
362
+ `${backendUrl}/api/kuadrant/apiproducts/${namespace}/${name}`,
363
+ {
364
+ method: "PATCH",
365
+ headers: { "Content-Type": "application/json" },
366
+ body: JSON.stringify({
367
+ spec: { publishStatus: newStatus }
368
+ })
369
+ }
370
+ );
371
+ if (!response.ok) {
372
+ throw new Error("failed to update publish status");
373
+ }
374
+ setRefreshTrigger((prev) => prev + 1);
375
+ alertApi.post({
376
+ message: `"${displayName}" ${newStatus === "Published" ? "published" : "unpublished"} successfully`,
377
+ severity: "success",
378
+ display: "transient"
379
+ });
380
+ } catch (err) {
381
+ console.error("error updating publish status:", err);
382
+ alertApi.post({
383
+ message: "Failed to update publish status",
384
+ severity: "error",
385
+ display: "transient"
386
+ });
387
+ }
169
388
  };
170
389
  const columns = [
171
390
  {
172
391
  title: "Name",
173
392
  field: "spec.displayName",
174
393
  render: (row) => {
175
- const publishStatus = row.spec?.publishStatus;
176
- const isPublished = publishStatus === "Published";
177
394
  const displayName = row.spec?.displayName ?? row.metadata.name;
178
- if (isPublished) {
179
- return /* @__PURE__ */ React.createElement(Link, { to: `/catalog/default/api/${row.metadata.name}/api-product` }, /* @__PURE__ */ React.createElement("strong", null, displayName));
180
- }
181
- return /* @__PURE__ */ React.createElement("span", { className: "text-muted" }, /* @__PURE__ */ React.createElement("strong", null, displayName));
395
+ return /* @__PURE__ */ React.createElement(Link, { to: `/kuadrant/api-products/${row.metadata.namespace}/${row.metadata.name}` }, /* @__PURE__ */ React.createElement("strong", null, displayName));
182
396
  },
183
397
  customFilterAndSearch: (term, row) => {
184
398
  const displayName = row.spec?.displayName || row.metadata.name || "";
185
399
  return displayName.toLowerCase().includes(term.toLowerCase());
186
400
  }
187
401
  },
188
- {
189
- title: "Resource Name",
190
- field: "metadata.name"
191
- },
192
402
  {
193
403
  title: "Version",
194
404
  field: "spec.version",
195
405
  render: (row) => row.spec?.version || "-"
196
406
  },
197
407
  {
198
- title: "HTTPRoute",
408
+ title: "Route",
199
409
  field: "spec.targetRef.name",
200
410
  render: (row) => row.spec?.targetRef?.name || "-"
201
411
  },
202
412
  {
203
- title: "Publish Status",
204
- field: "spec.publishStatus",
413
+ title: "Policy",
414
+ field: "policy",
415
+ render: (row) => getPolicyForProduct(row) || "N/A"
416
+ },
417
+ {
418
+ title: "Tags",
419
+ field: "spec.tags",
205
420
  render: (row) => {
206
- const status = row.spec?.publishStatus || "Draft";
207
- return /* @__PURE__ */ React.createElement(
208
- Chip,
209
- {
210
- label: status,
211
- size: "small",
212
- color: status === "Published" ? "primary" : "default"
213
- }
214
- );
421
+ const tags = row.spec?.tags || [];
422
+ if (tags.length === 0) return "-";
423
+ return /* @__PURE__ */ React.createElement(Box, { display: "flex", style: { gap: 4, flexWrap: "wrap" } }, tags.map((tag) => /* @__PURE__ */ React.createElement(Chip, { key: tag, label: tag, size: "small", variant: "outlined" })));
215
424
  }
216
425
  },
217
426
  {
218
- title: "Approval Mode",
219
- field: "spec.approvalMode",
427
+ title: "Status",
428
+ field: "spec.publishStatus",
220
429
  render: (row) => {
221
- const mode = row.spec?.approvalMode || "manual";
430
+ const status = row.spec?.publishStatus || "Draft";
222
431
  return /* @__PURE__ */ React.createElement(
223
432
  Chip,
224
433
  {
225
- label: mode,
434
+ label: status,
226
435
  size: "small",
227
- color: mode === "automatic" ? "secondary" : "default"
436
+ color: status === "Published" ? "primary" : "default"
228
437
  }
229
438
  );
230
439
  }
@@ -267,11 +476,6 @@ const ResourceList = () => {
267
476
  title: "Namespace",
268
477
  field: "metadata.namespace"
269
478
  },
270
- {
271
- title: "Created",
272
- field: "metadata.creationTimestamp",
273
- render: (row) => formatDate(row.metadata.creationTimestamp)
274
- },
275
479
  {
276
480
  title: "Actions",
277
481
  field: "actions",
@@ -281,8 +485,17 @@ const ResourceList = () => {
281
485
  const isOwner = owner === userEntityRef;
282
486
  const canEdit = canUpdateAllApiProducts || canUpdateOwnApiProduct && isOwner;
283
487
  const canDelete = canDeleteAllApiProducts || canDeleteOwnApiProduct && isOwner;
284
- if (!canEdit && !canDelete) return null;
285
- return /* @__PURE__ */ React.createElement(Box, { display: "flex", style: { gap: 4 } }, canEdit && /* @__PURE__ */ React.createElement(
488
+ const isPublished = row.spec?.publishStatus === "Published";
489
+ return /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", style: { gap: 4 } }, canEdit && /* @__PURE__ */ React.createElement(
490
+ Button,
491
+ {
492
+ size: "small",
493
+ color: "primary",
494
+ onClick: () => handlePublishToggle(row),
495
+ style: { marginRight: 4, textTransform: "none" }
496
+ },
497
+ isPublished ? "Unpublish" : "Publish"
498
+ ), canEdit && /* @__PURE__ */ React.createElement(
286
499
  IconButton,
287
500
  {
288
501
  size: "small",
@@ -302,57 +515,6 @@ const ResourceList = () => {
302
515
  }
303
516
  }
304
517
  ];
305
- const planPolicyColumns = [
306
- {
307
- title: "Name",
308
- field: "metadata.name",
309
- render: (row) => /* @__PURE__ */ React.createElement(
310
- Link,
311
- {
312
- to: `/kuadrant/planpolicy/${row.metadata.namespace}/${row.metadata.name}`
313
- },
314
- /* @__PURE__ */ React.createElement("strong", null, row.metadata.name)
315
- )
316
- },
317
- {
318
- title: "Namespace",
319
- field: "metadata.namespace"
320
- }
321
- ];
322
- const renderResources = (resources) => {
323
- if (!resources || resources.length === 0) {
324
- return /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "No API products found");
325
- }
326
- return /* @__PURE__ */ React.createElement(
327
- Table,
328
- {
329
- options: {
330
- paging: resources.length > 5,
331
- pageSize: 20,
332
- search: true,
333
- filtering: true,
334
- debounceInterval: 300,
335
- toolbar: true,
336
- emptyRowsWhenPaging: false
337
- },
338
- columns,
339
- data: resources
340
- }
341
- );
342
- };
343
- const renderPlanPolicies = (resources) => {
344
- if (!resources || resources.length === 0) {
345
- return /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "No plan policies found");
346
- }
347
- return /* @__PURE__ */ React.createElement(
348
- Table,
349
- {
350
- options: { paging: false, search: false, toolbar: false },
351
- columns: planPolicyColumns,
352
- data: resources
353
- }
354
- );
355
- };
356
518
  return /* @__PURE__ */ React.createElement(Page, { themeId: "tool" }, /* @__PURE__ */ React.createElement(
357
519
  Header,
358
520
  {
@@ -360,33 +522,74 @@ const ResourceList = () => {
360
522
  subtitle: "Manage API products for Kubernetes"
361
523
  },
362
524
  /* @__PURE__ */ React.createElement(SupportButton, null, "Manage API products and plan policies")
363
- ), /* @__PURE__ */ React.createElement(Content, null, loading && /* @__PURE__ */ React.createElement(Progress, null), error && /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error }), permissionError && /* @__PURE__ */ React.createElement(Box, { p: 2 }, /* @__PURE__ */ React.createElement(Typography, { color: "error" }, "unable to check permissions: ", permissionError.message), /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "permission:", " ", createPermissionError ? "kuadrant.apiproduct.create" : deletePermissionError ? "kuadrant.apiproduct.delete" : planPolicyPermissionError ? "kuadrant.planpolicy.list" : "unknown"), /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "please try again or contact your administrator")), !loading && !error && !permissionError && /* @__PURE__ */ React.createElement(Grid, { container: true, spacing: 3, direction: "column" }, /* @__PURE__ */ React.createElement(Grid, { item: true }, /* @__PURE__ */ React.createElement(
364
- InfoCard,
525
+ ), /* @__PURE__ */ React.createElement(Content, null, loading && /* @__PURE__ */ React.createElement(
526
+ Box,
365
527
  {
366
- title: "API Products",
367
- action: canCreateApiProduct ? /* @__PURE__ */ React.createElement(
368
- Box,
369
- {
370
- display: "flex",
371
- alignItems: "center",
372
- height: "100%",
373
- mt: 1
374
- },
375
- /* @__PURE__ */ React.createElement(
376
- Button,
377
- {
378
- variant: "contained",
379
- color: "primary",
380
- size: "small",
381
- startIcon: /* @__PURE__ */ React.createElement(AddIcon, null),
382
- onClick: () => setCreateDialogOpen(true)
383
- },
384
- "Create API Product"
385
- )
386
- ) : undefined
528
+ display: "flex",
529
+ flexDirection: "column",
530
+ alignItems: "center",
531
+ justifyContent: "center",
532
+ minHeight: 300
387
533
  },
388
- renderResources(apiProducts?.items)
389
- )), canListPlanPolicies && /* @__PURE__ */ React.createElement(Grid, { item: true }, /* @__PURE__ */ React.createElement(InfoCard, { title: "Plan Policies" }, renderPlanPolicies(planPolicies?.items)))), /* @__PURE__ */ React.createElement(
534
+ /* @__PURE__ */ React.createElement(CircularProgress, null),
535
+ /* @__PURE__ */ React.createElement(Typography, { variant: "h6", style: { marginTop: 16 } }, "Loading data..."),
536
+ /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "Preparing your data... This should only take a moment.")
537
+ ), error && /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error }), permissionError && /* @__PURE__ */ React.createElement(Box, { p: 2 }, /* @__PURE__ */ React.createElement(Typography, { color: "error" }, "unable to check permissions: ", permissionError.message), /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "permission:", " ", createPermissionError ? "kuadrant.apiproduct.create" : deletePermissionError ? "kuadrant.apiproduct.delete" : planPolicyPermissionError ? "kuadrant.planpolicy.list" : "unknown"), /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "please try again or contact your administrator")), !loading && !error && !permissionError && allProducts.length === 0 && /* @__PURE__ */ React.createElement(Box, { className: classes.emptyState }, /* @__PURE__ */ React.createElement(Box, { className: classes.emptyStateContent }, /* @__PURE__ */ React.createElement(Box, { className: classes.emptyStateText }, /* @__PURE__ */ React.createElement(Typography, { variant: "h4", className: classes.emptyStateTitle }, "API Product"), /* @__PURE__ */ React.createElement(
538
+ Typography,
539
+ {
540
+ variant: "body1",
541
+ className: classes.emptyStateDescription
542
+ },
543
+ "Create API product by registering existing API, associate route and policy"
544
+ ), canCreateApiProduct && /* @__PURE__ */ React.createElement(
545
+ Button,
546
+ {
547
+ variant: "contained",
548
+ color: "primary",
549
+ startIcon: /* @__PURE__ */ React.createElement(AddIcon, null),
550
+ onClick: () => setCreateDialogOpen(true)
551
+ },
552
+ "Create API Product"
553
+ )), /* @__PURE__ */ React.createElement(
554
+ "img",
555
+ {
556
+ src: emptyStateIllustration,
557
+ alt: "API Product illustration",
558
+ className: classes.emptyStateImage
559
+ }
560
+ ))), !loading && !error && !permissionError && allProducts.length > 0 && /* @__PURE__ */ React.createElement(Box, { className: classes.container }, /* @__PURE__ */ React.createElement(
561
+ FilterPanel,
562
+ {
563
+ sections: filterSections,
564
+ filters,
565
+ onChange: setFilters
566
+ }
567
+ ), /* @__PURE__ */ React.createElement(Box, { className: classes.tableContainer }, /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "flex-end", mb: 2 }, canCreateApiProduct && /* @__PURE__ */ React.createElement(
568
+ Button,
569
+ {
570
+ variant: "contained",
571
+ color: "primary",
572
+ size: "small",
573
+ startIcon: /* @__PURE__ */ React.createElement(AddIcon, null),
574
+ onClick: () => setCreateDialogOpen(true)
575
+ },
576
+ "Create API Product"
577
+ )), filteredProducts.length === 0 ? /* @__PURE__ */ React.createElement(Box, { p: 4, textAlign: "center" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body1", color: "textSecondary" }, "No API products match the selected filters.")) : /* @__PURE__ */ React.createElement(
578
+ Table,
579
+ {
580
+ options: {
581
+ paging: filteredProducts.length > 10,
582
+ pageSize: 20,
583
+ search: true,
584
+ filtering: false,
585
+ debounceInterval: 300,
586
+ toolbar: true,
587
+ emptyRowsWhenPaging: false
588
+ },
589
+ columns,
590
+ data: filteredProducts
591
+ }
592
+ ))), /* @__PURE__ */ React.createElement(
390
593
  CreateAPIProductDialog,
391
594
  {
392
595
  open: createDialogOpen,