@nospt/backstage-plugin-apigee 1.1.0-rc2 → 1.1.0-rc3

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 (47) hide show
  1. package/README.md +19 -19
  2. package/config.d.ts +25 -25
  3. package/dist/components/ApiCard.esm.js +41 -104
  4. package/dist/components/ApiCard.esm.js.map +1 -1
  5. package/dist/components/ApiCard.module.css.esm.js +8 -0
  6. package/dist/components/ApiCard.module.css.esm.js.map +1 -0
  7. package/dist/components/ApiCardGrid.esm.js +7 -21
  8. package/dist/components/ApiCardGrid.esm.js.map +1 -1
  9. package/dist/components/ApiCardGrid.module.css.esm.js +8 -0
  10. package/dist/components/ApiCardGrid.module.css.esm.js.map +1 -0
  11. package/dist/components/ApigeeApiCatalogPage.esm.js +71 -57
  12. package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -1
  13. package/dist/components/ApigeeNavContent.esm.js +2 -2
  14. package/dist/components/ApigeeNavContent.esm.js.map +1 -1
  15. package/dist/components/CountCards.esm.js +99 -0
  16. package/dist/components/CountCards.esm.js.map +1 -0
  17. package/dist/components/CountCards.module.css.esm.js +8 -0
  18. package/dist/components/CountCards.module.css.esm.js.map +1 -0
  19. package/dist/components/FilterToolbar.esm.js +63 -133
  20. package/dist/components/FilterToolbar.esm.js.map +1 -1
  21. package/dist/components/FilterToolbar.module.css.esm.js +8 -0
  22. package/dist/components/FilterToolbar.module.css.esm.js.map +1 -0
  23. package/dist/components/SearchBar.esm.js +7 -35
  24. package/dist/components/SearchBar.esm.js.map +1 -1
  25. package/dist/components/apiCardViewModel.esm.js.map +1 -1
  26. package/dist/components/apigeeFacetFilters.esm.js.map +1 -1
  27. package/dist/components/apigeeOrgFilter.esm.js.map +1 -1
  28. package/dist/components/apigeeSearchParams.esm.js +14 -1
  29. package/dist/components/apigeeSearchParams.esm.js.map +1 -1
  30. package/dist/components/categoryIcons.esm.js +12 -22
  31. package/dist/components/categoryIcons.esm.js.map +1 -1
  32. package/dist/components/facetModel.esm.js +30 -10
  33. package/dist/components/facetModel.esm.js.map +1 -1
  34. package/dist/components/grouping.esm.js.map +1 -1
  35. package/dist/components/states.esm.js +19 -49
  36. package/dist/components/states.esm.js.map +1 -1
  37. package/dist/components/useApigeeFilters.esm.js +18 -2
  38. package/dist/components/useApigeeFilters.esm.js.map +1 -1
  39. package/dist/components/useDebouncedValue.esm.js.map +1 -1
  40. package/dist/constants.esm.js.map +1 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
  43. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
  44. package/dist/plugin.esm.js +2 -2
  45. package/dist/plugin.esm.js.map +1 -1
  46. package/dist/routes.esm.js.map +1 -1
  47. package/package.json +17 -15
package/README.md CHANGED
@@ -1,19 +1,19 @@
1
- # @nospt/backstage-plugin-apigee
2
-
3
- Frontend package for the bootstrap stage of the Backstage Apigee plugin.
4
-
5
- ## Included
6
-
7
- - New Frontend System plugin registration
8
- - Placeholder page under `/apigee/bootstrap`
9
- - Shared permission exports from the common package
10
-
11
- ## Development
12
-
13
- ```bash
14
- cd plugins/apigee
15
- yarn start
16
- yarn test
17
- yarn lint
18
- yarn build
19
- ```
1
+ # @nospt/backstage-plugin-apigee
2
+
3
+ Frontend package for the bootstrap stage of the Backstage Apigee plugin.
4
+
5
+ ## Included
6
+
7
+ - New Frontend System plugin registration
8
+ - Placeholder page under `/apigee/bootstrap`
9
+ - Shared permission exports from the common package
10
+
11
+ ## Development
12
+
13
+ ```bash
14
+ cd plugins/apigee
15
+ yarn start
16
+ yarn test
17
+ yarn lint
18
+ yarn build
19
+ ```
package/config.d.ts CHANGED
@@ -1,25 +1,25 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- export interface Config {
3
- apigee?: {
4
- catalog?: {
5
- /**
6
- * Optional override of the default category -> icon map (UI-10 / D-08).
7
- * Keys are category slugs (matching the apigee.com/category annotation);
8
- * values are icon KEYS from the fixed allow-list resolved by
9
- * src/components/categoryIcons.tsx. Unknown/disallowed values fall back to
10
- * the `Category` icon (a value is NEVER a module path or component — see
11
- * Registry Safety in 05-UI-SPEC.md).
12
- * @visibility frontend
13
- */
14
- categoryIcons?: { [categorySlug: string]: string };
15
- /**
16
- * Optional org-id -> display-label map for the Org facet.
17
- * Keys are raw annotation values (for example `apigee-x-private-p-495866`);
18
- * values are user-facing labels (for example `Private`).
19
- * Unmapped org IDs fall back to the default humanised label.
20
- * @visibility frontend
21
- */
22
- orgLabels?: { [orgId: string]: string };
23
- };
24
- };
25
- }
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ export interface Config {
3
+ apigee?: {
4
+ catalog?: {
5
+ /**
6
+ * Optional override of the default category -> icon map (UI-10 / D-08).
7
+ * Keys are category slugs (matching the apigee.com/category annotation);
8
+ * values are icon KEYS from the fixed allow-list resolved by
9
+ * src/components/categoryIcons.tsx. Unknown/disallowed values fall back to
10
+ * the `Category` icon (a value is NEVER a module path or component — see
11
+ * Registry Safety in 05-UI-SPEC.md).
12
+ * @visibility frontend
13
+ */
14
+ categoryIcons?: { [categorySlug: string]: string };
15
+ /**
16
+ * Optional org-id -> display-label map for the Org facet.
17
+ * Keys are raw annotation values (for example `apigee-x-private-p-495866`);
18
+ * values are user-facing labels (for example `Private`).
19
+ * Unmapped org IDs fall back to the default humanised label.
20
+ * @visibility frontend
21
+ */
22
+ orgLabels?: { [orgId: string]: string };
23
+ };
24
+ };
25
+ }
@@ -1,84 +1,11 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import { Link, StatusOK, StatusError, StatusWarning } from '@backstage/core-components';
3
3
  import { useRouteRef } from '@backstage/frontend-plugin-api';
4
4
  import { entityRouteRef } from '@backstage/plugin-catalog-react';
5
- import { CardContent, Typography, Tooltip, Card, CardActionArea, makeStyles } from '@material-ui/core';
6
- import DescriptionOutlined from '@material-ui/icons/DescriptionOutlined';
7
- import GroupOutlined from '@material-ui/icons/GroupOutlined';
5
+ import { Text } from '@backstage/ui';
6
+ import { RiTeamLine, RiFileTextLine } from '@remixicon/react';
7
+ import styles from './ApiCard.module.css.esm.js';
8
8
 
9
- const useStyles = makeStyles((theme) => ({
10
- card: {
11
- height: "100%"
12
- },
13
- action: {
14
- height: "100%",
15
- minHeight: 172,
16
- alignItems: "stretch",
17
- transition: theme.transitions.create(["box-shadow"]),
18
- "&:focus-visible": {
19
- outline: `2px solid ${theme.palette.primary.main}`,
20
- outlineOffset: 2
21
- },
22
- "&:hover": {
23
- boxShadow: theme.shadows[4]
24
- },
25
- "@media (prefers-reduced-motion: reduce)": {
26
- transition: "none"
27
- }
28
- },
29
- content: {
30
- display: "flex",
31
- flexDirection: "column",
32
- gap: theme.spacing(1),
33
- height: "100%"
34
- },
35
- header: {
36
- display: "flex",
37
- alignItems: "flex-start",
38
- gap: theme.spacing(1)
39
- },
40
- categoryIcon: {
41
- color: theme.palette.text.secondary,
42
- marginTop: 2
43
- },
44
- name: {
45
- flex: 1,
46
- display: "-webkit-box",
47
- WebkitLineClamp: 2,
48
- WebkitBoxOrient: "vertical",
49
- overflow: "hidden",
50
- wordBreak: "break-word"
51
- },
52
- meta: {
53
- display: "flex",
54
- alignItems: "center",
55
- gap: theme.spacing(0.5),
56
- color: theme.palette.text.secondary
57
- },
58
- metaIcon: {
59
- fontSize: 16
60
- },
61
- version: {
62
- marginLeft: "auto"
63
- },
64
- description: {
65
- display: "-webkit-box",
66
- WebkitLineClamp: 2,
67
- WebkitBoxOrient: "vertical",
68
- overflow: "hidden",
69
- color: theme.palette.text.secondary
70
- },
71
- footer: {
72
- marginTop: "auto",
73
- display: "flex",
74
- alignItems: "center",
75
- gap: theme.spacing(1)
76
- },
77
- specIcon: {
78
- display: "inline-flex",
79
- color: theme.palette.text.secondary
80
- }
81
- }));
82
9
  function LifecycleBadge({
83
10
  kind,
84
11
  label
@@ -92,26 +19,35 @@ function LifecycleBadge({
92
19
  return /* @__PURE__ */ jsx(StatusWarning, { children: label });
93
20
  }
94
21
  function ApiCard({ vm, icon: Icon }) {
95
- const classes = useStyles();
96
22
  const entityRoute = useRouteRef(entityRouteRef);
97
23
  const { kind, namespace, name } = vm.targetRef;
98
24
  const href = entityRoute ? entityRoute({ kind: kind.toLowerCase(), namespace, name }) : void 0;
99
25
  const ariaLabel = `Open ${vm.name} API details`;
100
- const content = /* @__PURE__ */ jsxs(CardContent, { className: classes.content, children: [
101
- /* @__PURE__ */ jsxs("div", { className: classes.header, children: [
102
- Icon ? /* @__PURE__ */ jsx(Icon, { "aria-hidden": true, className: classes.categoryIcon, fontSize: "small" }) : null,
103
- /* @__PURE__ */ jsx(Typography, { variant: "subtitle1", component: "h3", className: classes.name, children: vm.name }),
26
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
27
+ /* @__PURE__ */ jsxs("div", { className: styles.header, children: [
28
+ Icon ? /* @__PURE__ */ jsx(Icon, { "aria-hidden": true, className: styles.categoryIcon, size: 20 }) : null,
29
+ /* @__PURE__ */ jsx(
30
+ Text,
31
+ {
32
+ variant: "body-medium",
33
+ weight: "bold",
34
+ as: "h3",
35
+ className: styles.name,
36
+ children: vm.name
37
+ }
38
+ ),
104
39
  /* @__PURE__ */ jsx(LifecycleBadge, { kind: vm.badge.kind, label: vm.badge.label })
105
40
  ] }),
106
- /* @__PURE__ */ jsxs("div", { className: classes.meta, children: [
107
- /* @__PURE__ */ jsx(GroupOutlined, { "aria-hidden": true, className: classes.metaIcon }),
108
- /* @__PURE__ */ jsx(Typography, { variant: "body2", component: "span", children: vm.ownerLabel }),
41
+ /* @__PURE__ */ jsxs("div", { className: styles.meta, children: [
42
+ /* @__PURE__ */ jsx(RiTeamLine, { "aria-hidden": true, size: 16 }),
43
+ /* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", as: "span", children: vm.ownerLabel }),
109
44
  vm.version ? /* @__PURE__ */ jsxs(
110
- Typography,
45
+ Text,
111
46
  {
112
- variant: "body2",
113
- component: "span",
114
- className: classes.version,
47
+ variant: "body-small",
48
+ color: "secondary",
49
+ as: "span",
50
+ className: styles.version,
115
51
  children: [
116
52
  "v",
117
53
  vm.version
@@ -119,27 +55,28 @@ function ApiCard({ vm, icon: Icon }) {
119
55
  }
120
56
  ) : null
121
57
  ] }),
122
- vm.description ? /* @__PURE__ */ jsx(Typography, { variant: "body2", className: classes.description, children: vm.description }) : null,
123
- vm.hasSpec ? /* @__PURE__ */ jsx("div", { className: classes.footer, children: /* @__PURE__ */ jsx(Tooltip, { title: "OpenAPI spec available", children: /* @__PURE__ */ jsx(
58
+ vm.description ? /* @__PURE__ */ jsx(
59
+ Text,
60
+ {
61
+ variant: "body-small",
62
+ color: "secondary",
63
+ as: "p",
64
+ className: styles.description,
65
+ children: vm.description
66
+ }
67
+ ) : null,
68
+ vm.hasSpec ? /* @__PURE__ */ jsx("div", { className: styles.footer, children: /* @__PURE__ */ jsx(
124
69
  "span",
125
70
  {
126
71
  role: "img",
127
72
  "aria-label": "OpenAPI spec available",
128
- className: classes.specIcon,
129
- children: /* @__PURE__ */ jsx(DescriptionOutlined, { "aria-hidden": true, fontSize: "small" })
73
+ title: "OpenAPI spec available",
74
+ className: styles.specIcon,
75
+ children: /* @__PURE__ */ jsx(RiFileTextLine, { "aria-hidden": true, size: 16 })
130
76
  }
131
- ) }) }) : null
77
+ ) }) : null
132
78
  ] });
133
- return /* @__PURE__ */ jsx(Card, { className: classes.card, variant: "outlined", children: href ? /* @__PURE__ */ jsx(
134
- CardActionArea,
135
- {
136
- component: Link,
137
- to: href,
138
- className: classes.action,
139
- "aria-label": ariaLabel,
140
- children: content
141
- }
142
- ) : /* @__PURE__ */ jsx(CardActionArea, { className: classes.action, "aria-label": ariaLabel, children: content }) });
79
+ return /* @__PURE__ */ jsx("div", { className: styles.card, children: href ? /* @__PURE__ */ jsx(Link, { to: href, className: styles.action, "aria-label": ariaLabel, children: content }) : /* @__PURE__ */ jsx("div", { className: styles.action, children: content }) });
143
80
  }
144
81
 
145
82
  export { ApiCard };
@@ -1 +1 @@
1
- {"version":3,"file":"ApiCard.esm.js","sources":["../../src/components/ApiCard.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n Link,\r\n StatusError,\r\n StatusOK,\r\n StatusWarning,\r\n} from '@backstage/core-components';\r\nimport { useRouteRef } from '@backstage/frontend-plugin-api';\r\nimport { entityRouteRef } from '@backstage/plugin-catalog-react';\r\nimport {\r\n Card,\r\n CardActionArea,\r\n CardContent,\r\n Tooltip,\r\n Typography,\r\n makeStyles,\r\n} from '@material-ui/core';\r\nimport DescriptionOutlined from '@material-ui/icons/DescriptionOutlined';\r\nimport GroupOutlined from '@material-ui/icons/GroupOutlined';\r\nimport type { SvgIconComponent } from '@material-ui/icons';\r\nimport type { ApiCardViewModel, LifecycleBadgeKind } from './apiCardViewModel';\r\n\r\nexport interface ApiCardProps {\r\n vm: ApiCardViewModel;\r\n /** Category icon resolved by the parent grid (05-05); decorative. */\r\n icon?: SvgIconComponent;\r\n}\r\n\r\nconst useStyles = makeStyles(theme => ({\r\n card: {\r\n height: '100%',\r\n },\r\n action: {\r\n height: '100%',\r\n minHeight: 172,\r\n alignItems: 'stretch',\r\n transition: theme.transitions.create(['box-shadow']),\r\n '&:focus-visible': {\r\n outline: `2px solid ${theme.palette.primary.main}`,\r\n outlineOffset: 2,\r\n },\r\n '&:hover': {\r\n boxShadow: theme.shadows[4],\r\n },\r\n '@media (prefers-reduced-motion: reduce)': {\r\n transition: 'none',\r\n },\r\n },\r\n content: {\r\n display: 'flex',\r\n flexDirection: 'column',\r\n gap: theme.spacing(1),\r\n height: '100%',\r\n },\r\n header: {\r\n display: 'flex',\r\n alignItems: 'flex-start',\r\n gap: theme.spacing(1),\r\n },\r\n categoryIcon: {\r\n color: theme.palette.text.secondary,\r\n marginTop: 2,\r\n },\r\n name: {\r\n flex: 1,\r\n display: '-webkit-box',\r\n WebkitLineClamp: 2,\r\n WebkitBoxOrient: 'vertical',\r\n overflow: 'hidden',\r\n wordBreak: 'break-word',\r\n },\r\n meta: {\r\n display: 'flex',\r\n alignItems: 'center',\r\n gap: theme.spacing(0.5),\r\n color: theme.palette.text.secondary,\r\n },\r\n metaIcon: {\r\n fontSize: 16,\r\n },\r\n version: {\r\n marginLeft: 'auto',\r\n },\r\n description: {\r\n display: '-webkit-box',\r\n WebkitLineClamp: 2,\r\n WebkitBoxOrient: 'vertical',\r\n overflow: 'hidden',\r\n color: theme.palette.text.secondary,\r\n },\r\n footer: {\r\n marginTop: 'auto',\r\n display: 'flex',\r\n alignItems: 'center',\r\n gap: theme.spacing(1),\r\n },\r\n specIcon: {\r\n display: 'inline-flex',\r\n color: theme.palette.text.secondary,\r\n },\r\n}));\r\n\r\nfunction LifecycleBadge({\r\n kind,\r\n label,\r\n}: {\r\n kind: LifecycleBadgeKind;\r\n label: string;\r\n}) {\r\n if (kind === 'ok') {\r\n return <StatusOK>{label}</StatusOK>;\r\n }\r\n if (kind === 'error') {\r\n return <StatusError>{label}</StatusError>;\r\n }\r\n return <StatusWarning>{label}</StatusWarning>;\r\n}\r\n\r\n/**\r\n * A single Apigee API tile. The whole card is one anchor-based control whose href is\r\n * resolved via `entityRouteRef` (never hardcoded — TR04); it degrades to a non-link\r\n * button when the route ref is unbound (dev harness). Entity text renders as React\r\n * children only — never via raw HTML injection (T-05-03). (UI-02, UI-03)\r\n */\r\nexport function ApiCard({ vm, icon: Icon }: ApiCardProps) {\r\n const classes = useStyles();\r\n const entityRoute = useRouteRef(entityRouteRef);\r\n const { kind, namespace, name } = vm.targetRef;\r\n const href = entityRoute ? entityRoute({ kind: kind.toLowerCase(), namespace, name }) : undefined;\r\n const ariaLabel = `Open ${vm.name} API details`;\r\n\r\n const content = (\r\n <CardContent className={classes.content}>\r\n <div className={classes.header}>\r\n {Icon ? (\r\n <Icon aria-hidden className={classes.categoryIcon} fontSize=\"small\" />\r\n ) : null}\r\n <Typography variant=\"subtitle1\" component=\"h3\" className={classes.name}>\r\n {vm.name}\r\n </Typography>\r\n <LifecycleBadge kind={vm.badge.kind} label={vm.badge.label} />\r\n </div>\r\n\r\n <div className={classes.meta}>\r\n <GroupOutlined aria-hidden className={classes.metaIcon} />\r\n <Typography variant=\"body2\" component=\"span\">\r\n {vm.ownerLabel}\r\n </Typography>\r\n {vm.version ? (\r\n <Typography\r\n variant=\"body2\"\r\n component=\"span\"\r\n className={classes.version}\r\n >\r\n v{vm.version}\r\n </Typography>\r\n ) : null}\r\n </div>\r\n\r\n {vm.description ? (\r\n <Typography variant=\"body2\" className={classes.description}>\r\n {vm.description}\r\n </Typography>\r\n ) : null}\r\n\r\n {vm.hasSpec ? (\r\n <div className={classes.footer}>\r\n <Tooltip title=\"OpenAPI spec available\">\r\n <span\r\n role=\"img\"\r\n aria-label=\"OpenAPI spec available\"\r\n className={classes.specIcon}\r\n >\r\n <DescriptionOutlined aria-hidden fontSize=\"small\" />\r\n </span>\r\n </Tooltip>\r\n </div>\r\n ) : null}\r\n </CardContent>\r\n );\r\n\r\n return (\r\n <Card className={classes.card} variant=\"outlined\">\r\n {href ? (\r\n <CardActionArea\r\n component={Link}\r\n to={href}\r\n className={classes.action}\r\n aria-label={ariaLabel}\r\n >\r\n {content}\r\n </CardActionArea>\r\n ) : (\r\n <CardActionArea className={classes.action} aria-label={ariaLabel}>\r\n {content}\r\n </CardActionArea>\r\n )}\r\n </Card>\r\n );\r\n}\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ApiCard.esm.js","sources":["../../src/components/ApiCard.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n Link,\n StatusError,\n StatusOK,\n StatusWarning,\n} from '@backstage/core-components';\nimport { useRouteRef } from '@backstage/frontend-plugin-api';\nimport { entityRouteRef } from '@backstage/plugin-catalog-react';\nimport { Text } from '@backstage/ui';\nimport {\n RiFileTextLine,\n RiTeamLine,\n type RemixiconComponentType,\n} from '@remixicon/react';\nimport type { ApiCardViewModel, LifecycleBadgeKind } from './apiCardViewModel';\nimport styles from './ApiCard.module.css';\n\nexport interface ApiCardProps {\n vm: ApiCardViewModel;\n /** Category icon resolved by the parent grid (05-05); decorative. */\n icon?: RemixiconComponentType;\n}\n\nfunction LifecycleBadge({\n kind,\n label,\n}: {\n kind: LifecycleBadgeKind;\n label: string;\n}) {\n if (kind === 'ok') {\n return <StatusOK>{label}</StatusOK>;\n }\n if (kind === 'error') {\n return <StatusError>{label}</StatusError>;\n }\n return <StatusWarning>{label}</StatusWarning>;\n}\n\n/**\n * A single Apigee API tile. The whole card is one anchor-based control whose href is\n * resolved via `entityRouteRef` (never hardcoded — TR04); it degrades to a non-link\n * button when the route ref is unbound (dev harness). Entity text renders as React\n * children only — never via raw HTML injection (T-05-03). (UI-02, UI-03)\n */\nexport function ApiCard({ vm, icon: Icon }: ApiCardProps) {\n const entityRoute = useRouteRef(entityRouteRef);\n const { kind, namespace, name } = vm.targetRef;\n const href = entityRoute\n ? entityRoute({ kind: kind.toLowerCase(), namespace, name })\n : undefined;\n const ariaLabel = `Open ${vm.name} API details`;\n\n const content = (\n <>\n <div className={styles.header}>\n {Icon ? (\n <Icon aria-hidden className={styles.categoryIcon} size={20} />\n ) : null}\n <Text\n variant=\"body-medium\"\n weight=\"bold\"\n as=\"h3\"\n className={styles.name}\n >\n {vm.name}\n </Text>\n <LifecycleBadge kind={vm.badge.kind} label={vm.badge.label} />\n </div>\n\n <div className={styles.meta}>\n <RiTeamLine aria-hidden size={16} />\n <Text variant=\"body-small\" color=\"secondary\" as=\"span\">\n {vm.ownerLabel}\n </Text>\n {vm.version ? (\n <Text\n variant=\"body-small\"\n color=\"secondary\"\n as=\"span\"\n className={styles.version}\n >\n v{vm.version}\n </Text>\n ) : null}\n </div>\n\n {vm.description ? (\n <Text\n variant=\"body-small\"\n color=\"secondary\"\n as=\"p\"\n className={styles.description}\n >\n {vm.description}\n </Text>\n ) : null}\n\n {vm.hasSpec ? (\n <div className={styles.footer}>\n <span\n role=\"img\"\n aria-label=\"OpenAPI spec available\"\n title=\"OpenAPI spec available\"\n className={styles.specIcon}\n >\n <RiFileTextLine aria-hidden size={16} />\n </span>\n </div>\n ) : null}\n </>\n );\n\n return (\n <div className={styles.card}>\n {href ? (\n <Link to={href} className={styles.action} aria-label={ariaLabel}>\n {content}\n </Link>\n ) : (\n <div className={styles.action}>{content}</div>\n )}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import styleInject from '../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
+
3
+ var css_248z = ".ApiCard_card__611f3d2e93 {\n height: 100%;\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-3);\n background-color: var(--bui-bg-neutral-1);\n transition: box-shadow 200ms ease;\n}\n\n.ApiCard_card__611f3d2e93:hover {\n box-shadow: var(--bui-shadow);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ApiCard_card__611f3d2e93 {\n transition: none;\n }\n}\n\n.ApiCard_action__611f3d2e93 {\n display: flex;\n flex-direction: column;\n gap: var(--bui-space-2);\n height: 100%;\n min-height: 172px;\n padding: var(--bui-space-4);\n color: inherit;\n text-decoration: none;\n border-radius: var(--bui-radius-3);\n}\n\n.ApiCard_action__611f3d2e93:focus-visible {\n outline: 2px solid var(--bui-ring);\n outline-offset: 2px;\n}\n\n.ApiCard_header__611f3d2e93 {\n display: flex;\n align-items: flex-start;\n gap: var(--bui-space-2);\n}\n\n.ApiCard_categoryIcon__611f3d2e93 {\n margin-top: 2px;\n color: var(--bui-fg-secondary);\n}\n\n.ApiCard_name__611f3d2e93 {\n flex: 1;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n word-break: break-word;\n}\n\n.ApiCard_meta__611f3d2e93 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-1);\n color: var(--bui-fg-secondary);\n}\n\n.ApiCard_version__611f3d2e93 {\n margin-left: auto;\n}\n\n.ApiCard_description__611f3d2e93 {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.ApiCard_footer__611f3d2e93 {\n margin-top: auto;\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n}\n\n.ApiCard_specIcon__611f3d2e93 {\n display: inline-flex;\n color: var(--bui-fg-secondary);\n}\n";
4
+ var styles = {"card":"ApiCard_card__611f3d2e93","action":"ApiCard_action__611f3d2e93","header":"ApiCard_header__611f3d2e93","categoryIcon":"ApiCard_categoryIcon__611f3d2e93","name":"ApiCard_name__611f3d2e93","meta":"ApiCard_meta__611f3d2e93","version":"ApiCard_version__611f3d2e93","description":"ApiCard_description__611f3d2e93","footer":"ApiCard_footer__611f3d2e93","specIcon":"ApiCard_specIcon__611f3d2e93"};
5
+ styleInject(css_248z);
6
+
7
+ export { styles as default };
8
+ //# sourceMappingURL=ApiCard.module.css.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiCard.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -1,26 +1,12 @@
1
1
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
2
2
  import { ItemCardGrid } from '@backstage/core-components';
3
- import { Typography, Chip, makeStyles } from '@material-ui/core';
3
+ import { Text } from '@backstage/ui';
4
4
  import { ApiCard } from './ApiCard.esm.js';
5
5
  import { resolveCategoryIcon } from './categoryIcons.esm.js';
6
6
  import { groupByCategory } from './grouping.esm.js';
7
+ import styles from './ApiCardGrid.module.css.esm.js';
7
8
 
8
- const useStyles = makeStyles((theme) => ({
9
- section: {
10
- marginBottom: theme.spacing(4)
11
- },
12
- sectionHeader: {
13
- display: "flex",
14
- alignItems: "center",
15
- gap: theme.spacing(1),
16
- marginBottom: theme.spacing(2)
17
- },
18
- sectionIcon: {
19
- color: theme.palette.text.secondary
20
- }
21
- }));
22
9
  function ApiCardGrid({ vms, viewMode, overrides }) {
23
- const classes = useStyles();
24
10
  if (viewMode === "flat") {
25
11
  return /* @__PURE__ */ jsx(ItemCardGrid, { children: vms.map((vm) => /* @__PURE__ */ jsx(
26
12
  ApiCard,
@@ -34,11 +20,11 @@ function ApiCardGrid({ vms, viewMode, overrides }) {
34
20
  const groups = groupByCategory(vms);
35
21
  return /* @__PURE__ */ jsx(Fragment, { children: groups.map((group) => {
36
22
  const SectionIcon = resolveCategoryIcon(group.key, overrides);
37
- return /* @__PURE__ */ jsxs("section", { className: classes.section, children: [
38
- /* @__PURE__ */ jsxs("div", { className: classes.sectionHeader, children: [
39
- /* @__PURE__ */ jsx(SectionIcon, { "aria-hidden": true, className: classes.sectionIcon }),
40
- /* @__PURE__ */ jsx(Typography, { variant: "h5", component: "h2", children: group.label }),
41
- /* @__PURE__ */ jsx(Chip, { size: "small", label: group.items.length })
23
+ return /* @__PURE__ */ jsxs("section", { className: styles.section, children: [
24
+ /* @__PURE__ */ jsxs("div", { className: styles.sectionHeader, children: [
25
+ /* @__PURE__ */ jsx(SectionIcon, { "aria-hidden": true, className: styles.sectionIcon }),
26
+ /* @__PURE__ */ jsx(Text, { variant: "title-small", as: "h2", children: group.label }),
27
+ /* @__PURE__ */ jsx("span", { className: styles.count, children: group.items.length })
42
28
  ] }),
43
29
  /* @__PURE__ */ jsx(ItemCardGrid, { children: group.items.map((vm) => /* @__PURE__ */ jsx(
44
30
  ApiCard,
@@ -1 +1 @@
1
- {"version":3,"file":"ApiCardGrid.esm.js","sources":["../../src/components/ApiCardGrid.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { ItemCardGrid } from '@backstage/core-components';\r\nimport { Chip, Typography, makeStyles } from '@material-ui/core';\r\nimport { ApiCard } from './ApiCard';\r\nimport type { ApiCardViewModel } from './apiCardViewModel';\r\nimport { resolveCategoryIcon } from './categoryIcons';\r\nimport { groupByCategory } from './grouping';\r\n\r\nexport type ViewMode = 'flat' | 'grouped';\r\n\r\nexport interface ApiCardGridProps {\r\n vms: ApiCardViewModel[];\r\n viewMode: ViewMode;\r\n /** Raw `apigee.catalog.categoryIcons` override map (slug -> IconKey string). */\r\n overrides?: Record<string, string>;\r\n}\r\n\r\nconst useStyles = makeStyles(theme => ({\r\n section: {\r\n marginBottom: theme.spacing(4),\r\n },\r\n sectionHeader: {\r\n display: 'flex',\r\n alignItems: 'center',\r\n gap: theme.spacing(1),\r\n marginBottom: theme.spacing(2),\r\n },\r\n sectionIcon: {\r\n color: theme.palette.text.secondary,\r\n },\r\n}));\r\n\r\n/**\r\n * Presentational grid of `ApiCard` tiles. In `flat` mode a single responsive grid; in\r\n * `grouped` mode one section per category (icon + label + count chip) with the \"Other\"\r\n * bucket last (ordering enforced by `groupByCategory`). No data fetching. (UI-01, UI-11)\r\n */\r\nexport function ApiCardGrid({ vms, viewMode, overrides }: ApiCardGridProps) {\r\n const classes = useStyles();\r\n\r\n if (viewMode === 'flat') {\r\n return (\r\n <ItemCardGrid>\r\n {vms.map(vm => (\r\n <ApiCard\r\n key={vm.targetRef.name}\r\n vm={vm}\r\n icon={resolveCategoryIcon(vm.category, overrides)}\r\n />\r\n ))}\r\n </ItemCardGrid>\r\n );\r\n }\r\n\r\n const groups = groupByCategory(vms);\r\n return (\r\n <>\r\n {groups.map(group => {\r\n const SectionIcon = resolveCategoryIcon(group.key, overrides);\r\n return (\r\n <section key={group.key} className={classes.section}>\r\n <div className={classes.sectionHeader}>\r\n <SectionIcon aria-hidden className={classes.sectionIcon} />\r\n <Typography variant=\"h5\" component=\"h2\">\r\n {group.label}\r\n </Typography>\r\n <Chip size=\"small\" label={group.items.length} />\r\n </div>\r\n <ItemCardGrid>\r\n {group.items.map(vm => (\r\n <ApiCard\r\n key={vm.targetRef.name}\r\n vm={vm}\r\n icon={resolveCategoryIcon(vm.category, overrides)}\r\n />\r\n ))}\r\n </ItemCardGrid>\r\n </section>\r\n );\r\n })}\r\n </>\r\n );\r\n}\r\n"],"names":[],"mappings":";;;;;;;AAiBA,MAAM,SAAA,GAAY,WAAW,CAAA,KAAA,MAAU;AAAA,EACrC,OAAA,EAAS;AAAA,IACP,YAAA,EAAc,KAAA,CAAM,OAAA,CAAQ,CAAC;AAAA,GAC/B;AAAA,EACA,aAAA,EAAe;AAAA,IACb,OAAA,EAAS,MAAA;AAAA,IACT,UAAA,EAAY,QAAA;AAAA,IACZ,GAAA,EAAK,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAAA,IACpB,YAAA,EAAc,KAAA,CAAM,OAAA,CAAQ,CAAC;AAAA,GAC/B;AAAA,EACA,WAAA,EAAa;AAAA,IACX,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK;AAAA;AAE9B,CAAA,CAAE,CAAA;AAOK,SAAS,WAAA,CAAY,EAAE,GAAA,EAAK,QAAA,EAAU,WAAU,EAAqB;AAC1E,EAAA,MAAM,UAAU,SAAA,EAAU;AAE1B,EAAA,IAAI,aAAa,MAAA,EAAQ;AACvB,IAAA,uBACE,GAAA,CAAC,YAAA,EAAA,EACE,QAAA,EAAA,GAAA,CAAI,GAAA,CAAI,CAAA,EAAA,qBACP,GAAA;AAAA,MAAC,OAAA;AAAA,MAAA;AAAA,QAEC,EAAA;AAAA,QACA,IAAA,EAAM,mBAAA,CAAoB,EAAA,CAAG,QAAA,EAAU,SAAS;AAAA,OAAA;AAAA,MAF3C,GAAG,SAAA,CAAU;AAAA,KAIrB,CAAA,EACH,CAAA;AAAA,EAEJ;AAEA,EAAA,MAAM,MAAA,GAAS,gBAAgB,GAAG,CAAA;AAClC,EAAA,uBACE,GAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA,MAAA,CAAO,GAAA,CAAI,CAAA,KAAA,KAAS;AACnB,IAAA,MAAM,WAAA,GAAc,mBAAA,CAAoB,KAAA,CAAM,GAAA,EAAK,SAAS,CAAA;AAC5D,IAAA,uBACE,IAAA,CAAC,SAAA,EAAA,EAAwB,SAAA,EAAW,OAAA,CAAQ,OAAA,EAC1C,QAAA,EAAA;AAAA,sBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAA,CAAQ,aAAA,EACtB,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,WAAA,EAAA,EAAY,aAAA,EAAW,IAAA,EAAC,SAAA,EAAW,QAAQ,WAAA,EAAa,CAAA;AAAA,4BACxD,UAAA,EAAA,EAAW,OAAA,EAAQ,MAAK,SAAA,EAAU,IAAA,EAChC,gBAAM,KAAA,EACT,CAAA;AAAA,4BACC,IAAA,EAAA,EAAK,IAAA,EAAK,SAAQ,KAAA,EAAO,KAAA,CAAM,MAAM,MAAA,EAAQ;AAAA,OAAA,EAChD,CAAA;AAAA,sBACA,GAAA,CAAC,YAAA,EAAA,EACE,QAAA,EAAA,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA,EAAA,qBACf,GAAA;AAAA,QAAC,OAAA;AAAA,QAAA;AAAA,UAEC,EAAA;AAAA,UACA,IAAA,EAAM,mBAAA,CAAoB,EAAA,CAAG,QAAA,EAAU,SAAS;AAAA,SAAA;AAAA,QAF3C,GAAG,SAAA,CAAU;AAAA,OAIrB,CAAA,EACH;AAAA,KAAA,EAAA,EAhBY,MAAM,GAiBpB,CAAA;AAAA,EAEJ,CAAC,CAAA,EACH,CAAA;AAEJ;;;;"}
1
+ {"version":3,"file":"ApiCardGrid.esm.js","sources":["../../src/components/ApiCardGrid.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { ItemCardGrid } from '@backstage/core-components';\nimport { Text } from '@backstage/ui';\nimport { ApiCard } from './ApiCard';\nimport type { ApiCardViewModel } from './apiCardViewModel';\nimport { resolveCategoryIcon } from './categoryIcons';\nimport { groupByCategory } from './grouping';\nimport styles from './ApiCardGrid.module.css';\n\nexport type ViewMode = 'flat' | 'grouped';\n\nexport interface ApiCardGridProps {\n vms: ApiCardViewModel[];\n viewMode: ViewMode;\n /** Raw `apigee.catalog.categoryIcons` override map (slug -> IconKey string). */\n overrides?: Record<string, string>;\n}\n\n/**\n * Presentational grid of `ApiCard` tiles. In `flat` mode a single responsive grid; in\n * `grouped` mode one section per category (icon + label + count chip) with the \"Other\"\n * bucket last (ordering enforced by `groupByCategory`). No data fetching. (UI-01, UI-11)\n */\nexport function ApiCardGrid({ vms, viewMode, overrides }: ApiCardGridProps) {\n if (viewMode === 'flat') {\n return (\n <ItemCardGrid>\n {vms.map(vm => (\n <ApiCard\n key={vm.targetRef.name}\n vm={vm}\n icon={resolveCategoryIcon(vm.category, overrides)}\n />\n ))}\n </ItemCardGrid>\n );\n }\n\n const groups = groupByCategory(vms);\n return (\n <>\n {groups.map(group => {\n const SectionIcon = resolveCategoryIcon(group.key, overrides);\n return (\n <section key={group.key} className={styles.section}>\n <div className={styles.sectionHeader}>\n <SectionIcon aria-hidden className={styles.sectionIcon} />\n <Text variant=\"title-small\" as=\"h2\">\n {group.label}\n </Text>\n <span className={styles.count}>{group.items.length}</span>\n </div>\n <ItemCardGrid>\n {group.items.map(vm => (\n <ApiCard\n key={vm.targetRef.name}\n vm={vm}\n icon={resolveCategoryIcon(vm.category, overrides)}\n />\n ))}\n </ItemCardGrid>\n </section>\n );\n })}\n </>\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAuBO,SAAS,WAAA,CAAY,EAAE,GAAA,EAAK,QAAA,EAAU,WAAU,EAAqB;AAC1E,EAAA,IAAI,aAAa,MAAA,EAAQ;AACvB,IAAA,uBACE,GAAA,CAAC,YAAA,EAAA,EACE,QAAA,EAAA,GAAA,CAAI,GAAA,CAAI,CAAA,EAAA,qBACP,GAAA;AAAA,MAAC,OAAA;AAAA,MAAA;AAAA,QAEC,EAAA;AAAA,QACA,IAAA,EAAM,mBAAA,CAAoB,EAAA,CAAG,QAAA,EAAU,SAAS;AAAA,OAAA;AAAA,MAF3C,GAAG,SAAA,CAAU;AAAA,KAIrB,CAAA,EACH,CAAA;AAAA,EAEJ;AAEA,EAAA,MAAM,MAAA,GAAS,gBAAgB,GAAG,CAAA;AAClC,EAAA,uBACE,GAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA,MAAA,CAAO,GAAA,CAAI,CAAA,KAAA,KAAS;AACnB,IAAA,MAAM,WAAA,GAAc,mBAAA,CAAoB,KAAA,CAAM,GAAA,EAAK,SAAS,CAAA;AAC5D,IAAA,uBACE,IAAA,CAAC,SAAA,EAAA,EAAwB,SAAA,EAAW,MAAA,CAAO,OAAA,EACzC,QAAA,EAAA;AAAA,sBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,aAAA,EACrB,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,WAAA,EAAA,EAAY,aAAA,EAAW,IAAA,EAAC,SAAA,EAAW,OAAO,WAAA,EAAa,CAAA;AAAA,4BACvD,IAAA,EAAA,EAAK,OAAA,EAAQ,eAAc,EAAA,EAAG,IAAA,EAC5B,gBAAM,KAAA,EACT,CAAA;AAAA,4BACC,MAAA,EAAA,EAAK,SAAA,EAAW,OAAO,KAAA,EAAQ,QAAA,EAAA,KAAA,CAAM,MAAM,MAAA,EAAO;AAAA,OAAA,EACrD,CAAA;AAAA,sBACA,GAAA,CAAC,YAAA,EAAA,EACE,QAAA,EAAA,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA,EAAA,qBACf,GAAA;AAAA,QAAC,OAAA;AAAA,QAAA;AAAA,UAEC,EAAA;AAAA,UACA,IAAA,EAAM,mBAAA,CAAoB,EAAA,CAAG,QAAA,EAAU,SAAS;AAAA,SAAA;AAAA,QAF3C,GAAG,SAAA,CAAU;AAAA,OAIrB,CAAA,EACH;AAAA,KAAA,EAAA,EAhBY,MAAM,GAiBpB,CAAA;AAAA,EAEJ,CAAC,CAAA,EACH,CAAA;AAEJ;;;;"}
@@ -0,0 +1,8 @@
1
+ import styleInject from '../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
+
3
+ var css_248z = ".ApiCardGrid_section__f119e0fd3b {\n margin-bottom: var(--bui-space-8);\n}\n\n.ApiCardGrid_sectionHeader__f119e0fd3b {\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n margin-bottom: var(--bui-space-4);\n}\n\n.ApiCardGrid_sectionIcon__f119e0fd3b {\n color: var(--bui-fg-secondary);\n}\n\n.ApiCardGrid_count__f119e0fd3b {\n display: inline-flex;\n align-items: center;\n min-width: 20px;\n height: 20px;\n padding: 0 var(--bui-space-2);\n border-radius: var(--bui-radius-full);\n background-color: var(--bui-bg-neutral-2);\n color: var(--bui-fg-secondary);\n font-size: var(--bui-font-size-2);\n font-variant-numeric: tabular-nums;\n}\n";
4
+ var styles = {"section":"ApiCardGrid_section__f119e0fd3b","sectionHeader":"ApiCardGrid_sectionHeader__f119e0fd3b","sectionIcon":"ApiCardGrid_sectionIcon__f119e0fd3b","count":"ApiCardGrid_count__f119e0fd3b"};
5
+ styleInject(css_248z);
6
+
7
+ export { styles as default };
8
+ //# sourceMappingURL=ApiCardGrid.module.css.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiCardGrid.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -1,27 +1,22 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { Page, Header, Content, ContentHeader } from '@backstage/core-components';
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { Page, Content, ContentHeader } from '@backstage/core-components';
3
3
  import { useApi, configApiRef } from '@backstage/core-plugin-api';
4
4
  import { EntityListProvider, useEntityList } from '@backstage/plugin-catalog-react';
5
- import { ButtonGroup, Button, makeStyles } from '@material-ui/core';
6
- import ViewAgenda from '@material-ui/icons/ViewAgenda';
7
- import ViewModule from '@material-ui/icons/ViewModule';
5
+ import { ToggleButton } from '@backstage/ui';
6
+ import { RiLayoutGridLine, RiLayoutRowLine } from '@remixicon/react';
8
7
  import { useEffect, useMemo, useState } from 'react';
9
8
  import { toApiCardViewModel } from './apiCardViewModel.esm.js';
10
9
  import { ApiCardGrid } from './ApiCardGrid.esm.js';
11
10
  import { ApiKindFilter, isApigeeEntity } from './apigeeFacetFilters.esm.js';
12
11
  import { ApigeeOrgFilter } from './apigeeOrgFilter.esm.js';
13
12
  import { isActive } from './apigeeSearchParams.esm.js';
14
- import { deriveFacets } from './facetModel.esm.js';
13
+ import { CountCards } from './CountCards.esm.js';
14
+ import { deriveFacets, deriveCounts } from './facetModel.esm.js';
15
15
  import { FilterToolbar } from './FilterToolbar.esm.js';
16
16
  import { SearchBar } from './SearchBar.esm.js';
17
17
  import { ApiCatalogError, SkeletonGrid, NoMatchState, ApiCatalogEmptyState } from './states.esm.js';
18
18
  import { useApigeeFilters } from './useApigeeFilters.esm.js';
19
19
 
20
- const useStyles = makeStyles({
21
- toggleGroup: {
22
- marginLeft: "auto"
23
- }
24
- });
25
20
  function ApplyApigeeFilters() {
26
21
  const { updateFilters } = useEntityList();
27
22
  useEffect(() => {
@@ -33,9 +28,16 @@ function ApplyApigeeFilters() {
33
28
  return null;
34
29
  }
35
30
  function ApigeeCatalogContent() {
36
- const classes = useStyles();
37
31
  const { entities, backendEntities, loading, error } = useEntityList();
38
- const { state, inputValue, setInputValue, toggleFacet, clearFacet, clearAll } = useApigeeFilters();
32
+ const {
33
+ state,
34
+ inputValue,
35
+ setInputValue,
36
+ toggleFacet,
37
+ selectOnlyFacet,
38
+ clearFacet,
39
+ clearAll
40
+ } = useApigeeFilters();
39
41
  const configApi = useApi(configApiRef);
40
42
  const overrides = useMemo(() => {
41
43
  const config = configApi.getOptionalConfig("apigee.catalog.categoryIcons");
@@ -65,10 +67,18 @@ function ApigeeCatalogContent() {
65
67
  }
66
68
  return Object.keys(out).length > 0 ? out : void 0;
67
69
  }, [configApi]);
70
+ const apigeeEntities = useMemo(
71
+ () => backendEntities.filter(isApigeeEntity),
72
+ [backendEntities]
73
+ );
68
74
  const vms = useMemo(() => entities.map(toApiCardViewModel), [entities]);
69
75
  const facets = useMemo(
70
- () => deriveFacets(backendEntities.filter(isApigeeEntity), overrides, orgLabels),
71
- [backendEntities, overrides, orgLabels]
76
+ () => deriveFacets(apigeeEntities, overrides, orgLabels),
77
+ [apigeeEntities, overrides, orgLabels]
78
+ );
79
+ const counts = useMemo(
80
+ () => deriveCounts(apigeeEntities, ["org", "lifecycle", "department"], orgLabels),
81
+ [apigeeEntities, orgLabels]
72
82
  );
73
83
  const active = isActive(state);
74
84
  const [viewMode, setViewMode] = useState("flat");
@@ -84,48 +94,52 @@ function ApigeeCatalogContent() {
84
94
  }
85
95
  return /* @__PURE__ */ jsx(ApiCardGrid, { vms, viewMode, overrides });
86
96
  };
87
- return /* @__PURE__ */ jsxs(Page, { themeId: "apis", children: [
88
- /* @__PURE__ */ jsx(
89
- Header,
97
+ return /* @__PURE__ */ jsx(Page, { themeId: "apis", children: /* @__PURE__ */ jsxs(Content, { children: [
98
+ /* @__PURE__ */ jsx(ContentHeader, { title: "APIs", children: /* @__PURE__ */ jsxs(
99
+ "div",
90
100
  {
91
- title: "API Catalog",
92
- subtitle: "Browse APIs discovered from Apigee & API Hub"
101
+ role: "group",
102
+ "aria-label": "API view mode",
103
+ style: {
104
+ marginLeft: "auto",
105
+ display: "inline-flex",
106
+ gap: "var(--bui-space-1)"
107
+ },
108
+ children: [
109
+ /* @__PURE__ */ jsx(
110
+ ToggleButton,
111
+ {
112
+ "aria-label": "Flat grid view",
113
+ isSelected: viewMode === "flat",
114
+ onChange: () => setViewMode("flat"),
115
+ iconStart: /* @__PURE__ */ jsx(RiLayoutGridLine, {}),
116
+ children: "Flat"
117
+ }
118
+ ),
119
+ /* @__PURE__ */ jsx(
120
+ ToggleButton,
121
+ {
122
+ "aria-label": "Group by category",
123
+ isSelected: viewMode === "grouped",
124
+ onChange: () => setViewMode("grouped"),
125
+ iconStart: /* @__PURE__ */ jsx(RiLayoutRowLine, {}),
126
+ children: "Grouped"
127
+ }
128
+ )
129
+ ]
93
130
  }
94
- ),
95
- /* @__PURE__ */ jsxs(Content, { children: [
96
- /* @__PURE__ */ jsx(ContentHeader, { title: `${vms.length} APIs`, children: /* @__PURE__ */ jsx(
97
- "div",
131
+ ) }),
132
+ !loading && !error && apigeeEntities.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
133
+ /* @__PURE__ */ jsx(
134
+ CountCards,
98
135
  {
99
- role: "group",
100
- "aria-label": "API view mode",
101
- className: classes.toggleGroup,
102
- children: /* @__PURE__ */ jsxs(ButtonGroup, { size: "small", children: [
103
- /* @__PURE__ */ jsx(
104
- Button,
105
- {
106
- startIcon: /* @__PURE__ */ jsx(ViewModule, {}),
107
- "aria-label": "Flat grid view",
108
- "aria-pressed": viewMode === "flat",
109
- color: viewMode === "flat" ? "primary" : "default",
110
- onClick: () => setViewMode("flat"),
111
- children: "Flat"
112
- }
113
- ),
114
- /* @__PURE__ */ jsx(
115
- Button,
116
- {
117
- startIcon: /* @__PURE__ */ jsx(ViewAgenda, {}),
118
- "aria-label": "Group by category",
119
- "aria-pressed": viewMode === "grouped",
120
- color: viewMode === "grouped" ? "primary" : "default",
121
- onClick: () => setViewMode("grouped"),
122
- children: "Grouped"
123
- }
124
- )
125
- ] })
136
+ total: apigeeEntities.length,
137
+ counts,
138
+ selected: state.selected,
139
+ onSelectFacet: selectOnlyFacet
126
140
  }
127
- ) }),
128
- !loading && !error && backendEntities.filter(isApigeeEntity).length > 0 ? /* @__PURE__ */ jsx(
141
+ ),
142
+ /* @__PURE__ */ jsx(
129
143
  FilterToolbar,
130
144
  {
131
145
  facets,
@@ -137,10 +151,10 @@ function ApigeeCatalogContent() {
137
151
  resultCount: vms.length,
138
152
  searchBar: /* @__PURE__ */ jsx(SearchBar, { value: inputValue, onChange: setInputValue })
139
153
  }
140
- ) : null,
141
- renderBody()
142
- ] })
143
- ] });
154
+ )
155
+ ] }) : null,
156
+ renderBody()
157
+ ] }) });
144
158
  }
145
159
  function ApigeeApiCatalogPage() {
146
160
  return /* @__PURE__ */ jsxs(EntityListProvider, { children: [