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

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.

Potentially problematic release.


This version of @nospt/backstage-plugin-apigee might be problematic. Click here for more details.

Files changed (48) hide show
  1. package/config.d.ts +0 -10
  2. package/dist/components/ApiCard.esm.js +61 -47
  3. package/dist/components/ApiCard.esm.js.map +1 -1
  4. package/dist/components/ApiCard.module.css.esm.js +2 -2
  5. package/dist/components/ApiCardGrid.esm.js +6 -34
  6. package/dist/components/ApiCardGrid.esm.js.map +1 -1
  7. package/dist/components/ApiDetailsDrawer.esm.js +335 -0
  8. package/dist/components/ApiDetailsDrawer.esm.js.map +1 -0
  9. package/dist/components/ApiDetailsDrawer.module.css.esm.js +8 -0
  10. package/dist/components/ApiDetailsDrawer.module.css.esm.js.map +1 -0
  11. package/dist/components/ApiTable.esm.js +170 -0
  12. package/dist/components/ApiTable.esm.js.map +1 -0
  13. package/dist/components/ApiTable.module.css.esm.js +8 -0
  14. package/dist/components/ApiTable.module.css.esm.js.map +1 -0
  15. package/dist/components/ApigeeApiCatalogPage.esm.js +98 -79
  16. package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -1
  17. package/dist/components/CountCards.esm.js +23 -0
  18. package/dist/components/CountCards.esm.js.map +1 -1
  19. package/dist/components/FilterToolbar.esm.js +43 -39
  20. package/dist/components/FilterToolbar.esm.js.map +1 -1
  21. package/dist/components/FilterToolbar.module.css.esm.js +2 -2
  22. package/dist/components/apiCardViewModel.esm.js +16 -3
  23. package/dist/components/apiCardViewModel.esm.js.map +1 -1
  24. package/dist/components/apiDetailViewModel.esm.js +60 -0
  25. package/dist/components/apiDetailViewModel.esm.js.map +1 -0
  26. package/dist/components/apigeeFacetFilters.esm.js +3 -6
  27. package/dist/components/apigeeFacetFilters.esm.js.map +1 -1
  28. package/dist/components/apigeeSearchParams.esm.js +0 -1
  29. package/dist/components/apigeeSearchParams.esm.js.map +1 -1
  30. package/dist/components/facetModel.esm.js +3 -29
  31. package/dist/components/facetModel.esm.js.map +1 -1
  32. package/dist/components/grouping.esm.js +1 -26
  33. package/dist/components/grouping.esm.js.map +1 -1
  34. package/dist/components/useApigeeFilters.esm.js +1 -3
  35. package/dist/components/useApigeeFilters.esm.js.map +1 -1
  36. package/dist/constants.esm.js +3 -3
  37. package/dist/constants.esm.js.map +1 -1
  38. package/dist/index.d.ts +52 -1
  39. package/dist/index.esm.js +1 -1
  40. package/dist/plugin.esm.js +17 -7
  41. package/dist/plugin.esm.js.map +1 -1
  42. package/dist/routes.esm.js +2 -1
  43. package/dist/routes.esm.js.map +1 -1
  44. package/package.json +2 -2
  45. package/dist/components/ApiCardGrid.module.css.esm.js +0 -8
  46. package/dist/components/ApiCardGrid.module.css.esm.js.map +0 -1
  47. package/dist/components/categoryIcons.esm.js +0 -53
  48. package/dist/components/categoryIcons.esm.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CountCards.esm.js","sources":["../../src/components/CountCards.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { Text } from '@backstage/ui';\nimport type { DerivedCount, FacetKey } from './facetModel';\nimport styles from './CountCards.module.css';\n\nexport interface CountCardsProps {\n /** Total APIs in the Apigee catalog (static snapshot — not the filtered count). */\n total: number;\n /** Per-facet breakdowns (org / lifecycle / department) from `deriveCounts`. */\n counts: DerivedCount[];\n /** Currently selected values per facet (from the URL filter state). */\n selected: Record<FacetKey, string[]>;\n /** Select a single facet value — clicking filters the catalog to exactly that value, replacing any other card selection. */\n onSelectFacet: (key: FacetKey, value: string) => void;\n}\n\n/**\n * Summary stat row shown above the filter toolbar: a prominent total-APIs card plus one\n * breakdown card per facet (org / lifecycle / department). Counts are a fixed snapshot of the\n * full Apigee catalog — they do NOT change as filters apply. Each bucket is a single-select link:\n * clicking filters the grid to exactly that value (replacing any other card selection), and the\n * active value is highlighted. Purely presentational.\n */\nexport function CountCards({\n total,\n counts,\n selected,\n onSelectFacet,\n}: CountCardsProps) {\n return (\n <div className={styles.row}>\n <div role=\"group\" aria-label=\"APIs\" className={styles.card}>\n <Text\n as=\"p\"\n variant=\"title-large\"\n weight=\"bold\"\n className={styles.total}\n >\n {total}\n </Text>\n <Text\n as=\"h3\"\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.cardTitle}\n >\n APIs\n </Text>\n </div>\n {counts.map(count => {\n const heading = `By ${count.label.toLowerCase()}`;\n return (\n <div\n key={count.key}\n role=\"group\"\n aria-label={heading}\n className={styles.card}\n >\n <Text\n as=\"h3\"\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.cardTitle}\n >\n {heading}\n </Text>\n {count.buckets.length === 0 ? (\n <Text variant=\"body-small\" color=\"secondary\">\n No data\n </Text>\n ) : (\n <div className={styles.list}>\n {count.buckets.map(bucket => {\n const isSelected = selected[count.key].includes(bucket.value);\n return (\n <button\n key={bucket.value}\n type=\"button\"\n className={`${styles.bucket} ${\n isSelected ? styles.bucketSelected : ''\n }`}\n aria-pressed={isSelected}\n aria-label={`${bucket.label}, ${bucket.count} ${\n bucket.count === 1 ? 'API' : 'APIs'\n }`}\n onClick={() => onSelectFacet(count.key, bucket.value)}\n >\n <Text\n as=\"span\"\n variant=\"body-small\"\n truncate\n title={bucket.label}\n className={styles.bucketLabel}\n >\n {bucket.label}\n </Text>\n <Text\n as=\"span\"\n variant=\"body-small\"\n weight=\"bold\"\n className={styles.bucketCount}\n >\n {bucket.count}\n </Text>\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CountCards.esm.js","sources":["../../src/components/CountCards.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { Text } from '@backstage/ui';\nimport type { DerivedCount, FacetKey } from './facetModel';\nimport styles from './CountCards.module.css';\n\nexport interface CountCardsProps {\n /** Total APIs in the Apigee catalog (static snapshot — not the filtered count). */\n total: number;\n /** APIs in the catalog that have an OpenAPI spec (static snapshot). */\n specCount: number;\n /** Per-facet breakdowns (org / lifecycle / department) from `deriveCounts`. */\n counts: DerivedCount[];\n /** Currently selected values per facet (from the URL filter state). */\n selected: Record<FacetKey, string[]>;\n /** Select a single facet value — clicking filters the catalog to exactly that value, replacing any other card selection. */\n onSelectFacet: (key: FacetKey, value: string) => void;\n}\n\n/**\n * Summary stat row shown above the filter toolbar: the prominent total-APIs card, a with-specs\n * stat card, and one breakdown card per facet (org / lifecycle / department). Counts are a\n * fixed snapshot of the full Apigee catalog — they do NOT change as filters apply. Each bucket is\n * a single-select link: clicking filters the grid to exactly that value (replacing any other card\n * selection), and the active value is highlighted. Purely presentational.\n */\nexport function CountCards({\n total,\n specCount,\n counts,\n selected,\n onSelectFacet,\n}: CountCardsProps) {\n return (\n <div className={styles.row}>\n <div role=\"group\" aria-label=\"APIs\" className={styles.card}>\n <Text\n as=\"p\"\n variant=\"title-large\"\n weight=\"bold\"\n className={styles.total}\n >\n {total}\n </Text>\n <Text\n as=\"h3\"\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.cardTitle}\n >\n APIs\n </Text>\n </div>\n <div role=\"group\" aria-label=\"With specs\" className={styles.card}>\n <Text\n as=\"p\"\n variant=\"title-large\"\n weight=\"bold\"\n className={styles.total}\n >\n {specCount}\n </Text>\n <Text\n as=\"h3\"\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.cardTitle}\n >\n With specs\n </Text>\n </div>\n {counts.map(count => {\n const heading = `By ${count.label.toLowerCase()}`;\n return (\n <div\n key={count.key}\n role=\"group\"\n aria-label={heading}\n className={styles.card}\n >\n <Text\n as=\"h3\"\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.cardTitle}\n >\n {heading}\n </Text>\n {count.buckets.length === 0 ? (\n <Text variant=\"body-small\" color=\"secondary\">\n No data\n </Text>\n ) : (\n <div className={styles.list}>\n {count.buckets.map(bucket => {\n const isSelected = selected[count.key].includes(bucket.value);\n return (\n <button\n key={bucket.value}\n type=\"button\"\n className={`${styles.bucket} ${\n isSelected ? styles.bucketSelected : ''\n }`}\n aria-pressed={isSelected}\n aria-label={`${bucket.label}, ${bucket.count} ${\n bucket.count === 1 ? 'API' : 'APIs'\n }`}\n onClick={() => onSelectFacet(count.key, bucket.value)}\n >\n <Text\n as=\"span\"\n variant=\"body-small\"\n truncate\n title={bucket.label}\n className={styles.bucketLabel}\n >\n {bucket.label}\n </Text>\n <Text\n as=\"span\"\n variant=\"body-small\"\n weight=\"bold\"\n className={styles.bucketCount}\n >\n {bucket.count}\n </Text>\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { useState, useRef, useEffect } from 'react';
2
+ import { useState, useCallback, useRef, useEffect } from 'react';
3
3
  import { Button, Checkbox } from '@backstage/ui';
4
4
  import { RiCloseLine, RiArrowDropDownLine } from '@remixicon/react';
5
5
  import styles from './FilterToolbar.module.css.esm.js';
@@ -7,25 +7,27 @@ import styles from './FilterToolbar.module.css.esm.js';
7
7
  function FacetDropdown({
8
8
  facet,
9
9
  selected,
10
+ isOpen,
11
+ onOpenToggle,
12
+ onClose,
10
13
  onToggle,
11
14
  onClear
12
15
  }) {
13
- const [open, setOpen] = useState(false);
14
16
  const ref = useRef(null);
15
17
  const menuId = `facet-menu-${facet.key}`;
16
18
  const count = selected.length;
17
19
  useEffect(() => {
18
- if (!open) {
20
+ if (!isOpen) {
19
21
  return void 0;
20
22
  }
21
23
  const onDocMouseDown = (event) => {
22
24
  if (ref.current && !ref.current.contains(event.target)) {
23
- setOpen(false);
25
+ onClose();
24
26
  }
25
27
  };
26
28
  const onKeyDown = (event) => {
27
29
  if (event.key === "Escape") {
28
- setOpen(false);
30
+ onClose();
29
31
  }
30
32
  };
31
33
  document.addEventListener("mousedown", onDocMouseDown);
@@ -34,7 +36,7 @@ function FacetDropdown({
34
36
  document.removeEventListener("mousedown", onDocMouseDown);
35
37
  document.removeEventListener("keydown", onKeyDown);
36
38
  };
37
- }, [open]);
39
+ }, [isOpen, onClose]);
38
40
  return /* @__PURE__ */ jsxs("div", { ref, className: styles.facet, children: [
39
41
  /* @__PURE__ */ jsxs(
40
42
  Button,
@@ -44,17 +46,17 @@ function FacetDropdown({
44
46
  isDisabled: !facet.enabled,
45
47
  iconEnd: /* @__PURE__ */ jsx(RiArrowDropDownLine, {}),
46
48
  "aria-haspopup": "true",
47
- "aria-expanded": open,
48
- "aria-controls": open ? menuId : void 0,
49
+ "aria-expanded": isOpen,
50
+ "aria-controls": isOpen ? menuId : void 0,
49
51
  "aria-label": count > 0 ? `${facet.label} filter, ${count} selected` : `${facet.label} filter`,
50
- onPress: () => setOpen((prev) => !prev),
52
+ onPress: onOpenToggle,
51
53
  children: [
52
54
  facet.label,
53
55
  count > 0 ? /* @__PURE__ */ jsx("span", { className: styles.countChip, children: count }) : null
54
56
  ]
55
57
  }
56
58
  ),
57
- open ? /* @__PURE__ */ jsxs(
59
+ isOpen ? /* @__PURE__ */ jsxs(
58
60
  "div",
59
61
  {
60
62
  id: menuId,
@@ -62,22 +64,16 @@ function FacetDropdown({
62
64
  role: "group",
63
65
  "aria-label": `${facet.label} options`,
64
66
  children: [
65
- facet.options.map((option) => {
66
- const OptionIcon = facet.icons?.[option.value];
67
- return /* @__PURE__ */ jsx(
68
- Checkbox,
69
- {
70
- className: styles.option,
71
- isSelected: selected.includes(option.value),
72
- onChange: () => onToggle(option.value),
73
- children: OptionIcon ? /* @__PURE__ */ jsxs("span", { className: styles.optionLabel, children: [
74
- /* @__PURE__ */ jsx(OptionIcon, { "aria-hidden": true, size: 16 }),
75
- option.label
76
- ] }) : option.label
77
- },
78
- option.value
79
- );
80
- }),
67
+ facet.options.map((option) => /* @__PURE__ */ jsx(
68
+ Checkbox,
69
+ {
70
+ className: styles.option,
71
+ isSelected: selected.includes(option.value),
72
+ onChange: () => onToggle(option.value),
73
+ children: option.label
74
+ },
75
+ option.value
76
+ )),
81
77
  count > 0 ? /* @__PURE__ */ jsx("div", { className: styles.menuFooter, children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "small", onPress: onClear, children: "Clear" }) }) : null
82
78
  ]
83
79
  }
@@ -92,8 +88,11 @@ function FilterToolbar({
92
88
  onClearAll,
93
89
  active,
94
90
  resultCount,
95
- searchBar
91
+ searchBar,
92
+ viewToggle
96
93
  }) {
94
+ const [openKey, setOpenKey] = useState(null);
95
+ const closeMenus = useCallback(() => setOpenKey(null), []);
97
96
  return /* @__PURE__ */ jsxs("div", { role: "search", "aria-label": "Filter and search APIs", children: [
98
97
  /* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
99
98
  facets.map((facet) => /* @__PURE__ */ jsx(
@@ -101,23 +100,28 @@ function FilterToolbar({
101
100
  {
102
101
  facet,
103
102
  selected: selected[facet.key],
103
+ isOpen: openKey === facet.key,
104
+ onOpenToggle: () => setOpenKey((prev) => prev === facet.key ? null : facet.key),
105
+ onClose: closeMenus,
104
106
  onToggle: (value) => onToggleFacet(facet.key, value),
105
107
  onClear: () => onClearFacet(facet.key)
106
108
  },
107
109
  facet.key
108
110
  )),
109
- searchBar,
110
- active ? /* @__PURE__ */ jsx(
111
- Button,
112
- {
113
- className: styles.clearAll,
114
- variant: "tertiary",
115
- size: "small",
116
- iconStart: /* @__PURE__ */ jsx(RiCloseLine, {}),
117
- onPress: onClearAll,
118
- children: "Clear all"
119
- }
120
- ) : null
111
+ /* @__PURE__ */ jsxs("div", { className: styles.searchRow, children: [
112
+ searchBar,
113
+ active ? /* @__PURE__ */ jsx(
114
+ Button,
115
+ {
116
+ variant: "tertiary",
117
+ size: "small",
118
+ iconStart: /* @__PURE__ */ jsx(RiCloseLine, {}),
119
+ onPress: onClearAll,
120
+ children: "Clear all"
121
+ }
122
+ ) : null,
123
+ viewToggle
124
+ ] })
121
125
  ] }),
122
126
  /* @__PURE__ */ jsx("span", { className: styles.srOnly, "aria-live": "polite", children: `${resultCount} APIs match your filters` })
123
127
  ] });
@@ -1 +1 @@
1
- {"version":3,"file":"FilterToolbar.esm.js","sources":["../../src/components/FilterToolbar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Button, Checkbox } from '@backstage/ui';\nimport { RiArrowDropDownLine, RiCloseLine } from '@remixicon/react';\nimport type { DerivedFacet, FacetKey } from './facetModel';\nimport styles from './FilterToolbar.module.css';\n\nexport interface FilterToolbarProps {\n /** Derived facets from `deriveFacets()`; ALL render (revised D-05) — one with no values is disabled. */\n facets: DerivedFacet[];\n /** Current selections per facet (hydrated from the URL state in 06-04). */\n selected: Record<FacetKey, string[]>;\n onToggleFacet: (key: FacetKey, value: string) => void;\n /** Per-facet menu-footer \"Clear\" (clears just that facet). */\n onClearFacet: (key: FacetKey) => void;\n /** Toolbar \"Clear all\" (resets every facet + search). */\n onClearAll: () => void;\n /** `isActive(state)` — gates the \"Clear all\" affordance. */\n active: boolean;\n /** Filtered result count for the polite live region (UI-07). */\n resultCount: number;\n /** `SearchBar` mounted inline in the toolbar row. */\n searchBar: ReactNode;\n}\n\n/**\n * A single multi-select facet as an outlined disclosure button. Options are @backstage/ui\n * `Checkbox`es in a popover that stays OPEN while toggling (multi-select); the popover closes on\n * outside-click or Escape. All dynamic text (`facet.label`, `option.label`) renders as React\n * children only — never raw HTML (T-06-03-01).\n */\nfunction FacetDropdown({\n facet,\n selected,\n onToggle,\n onClear,\n}: {\n facet: DerivedFacet;\n selected: string[];\n onToggle: (value: string) => void;\n onClear: () => void;\n}) {\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n const menuId = `facet-menu-${facet.key}`;\n const count = selected.length;\n\n useEffect(() => {\n if (!open) {\n return undefined;\n }\n const onDocMouseDown = (event: MouseEvent) => {\n if (ref.current && !ref.current.contains(event.target as Node)) {\n setOpen(false);\n }\n };\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', onDocMouseDown);\n document.addEventListener('keydown', onKeyDown);\n return () => {\n document.removeEventListener('mousedown', onDocMouseDown);\n document.removeEventListener('keydown', onKeyDown);\n };\n }, [open]);\n\n return (\n <div ref={ref} className={styles.facet}>\n <Button\n variant={count > 0 ? 'primary' : 'secondary'}\n size=\"small\"\n isDisabled={!facet.enabled}\n iconEnd={<RiArrowDropDownLine />}\n aria-haspopup=\"true\"\n aria-expanded={open}\n aria-controls={open ? menuId : undefined}\n aria-label={\n count > 0\n ? `${facet.label} filter, ${count} selected`\n : `${facet.label} filter`\n }\n onPress={() => setOpen(prev => !prev)}\n >\n {facet.label}\n {count > 0 ? <span className={styles.countChip}>{count}</span> : null}\n </Button>\n {open ? (\n <div\n id={menuId}\n className={styles.menu}\n role=\"group\"\n aria-label={`${facet.label} options`}\n >\n {facet.options.map(option => {\n const OptionIcon = facet.icons?.[option.value];\n return (\n <Checkbox\n key={option.value}\n className={styles.option}\n isSelected={selected.includes(option.value)}\n onChange={() => onToggle(option.value)}\n >\n {OptionIcon ? (\n <span className={styles.optionLabel}>\n <OptionIcon aria-hidden size={16} />\n {option.label}\n </span>\n ) : (\n option.label\n )}\n </Checkbox>\n );\n })}\n {count > 0 ? (\n <div className={styles.menuFooter}>\n <Button variant=\"tertiary\" size=\"small\" onPress={onClear}>\n Clear\n </Button>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Presentational Phase 6 toolbar (D-01 top toolbar, D-02 multi-select dropdowns). Renders EVERY\n * facet in `FACET_ORDER` (revised D-05 — a facet with no values renders disabled), the inline\n * `searchBar` slot, a conditional right-aligned \"Clear all\", and a polite `aria-live` result-count\n * region. Purely props-driven — no data fetching and no URL/provider access (that wiring is 06-04).\n * (UI-06, UI-07)\n */\nexport function FilterToolbar({\n facets,\n selected,\n onToggleFacet,\n onClearFacet,\n onClearAll,\n active,\n resultCount,\n searchBar,\n}: FilterToolbarProps) {\n return (\n <div role=\"search\" aria-label=\"Filter and search APIs\">\n <div className={styles.toolbar}>\n {facets.map(facet => (\n <FacetDropdown\n key={facet.key}\n facet={facet}\n selected={selected[facet.key]}\n onToggle={value => onToggleFacet(facet.key, value)}\n onClear={() => onClearFacet(facet.key)}\n />\n ))}\n {searchBar}\n {active ? (\n <Button\n className={styles.clearAll}\n variant=\"tertiary\"\n size=\"small\"\n iconStart={<RiCloseLine />}\n onPress={onClearAll}\n >\n Clear all\n </Button>\n ) : null}\n </div>\n <span className={styles.srOnly} aria-live=\"polite\">\n {`${resultCount} APIs match your filters`}\n </span>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FilterToolbar.esm.js","sources":["../../src/components/FilterToolbar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Button, Checkbox } from '@backstage/ui';\nimport { RiArrowDropDownLine, RiCloseLine } from '@remixicon/react';\nimport type { DerivedFacet, FacetKey } from './facetModel';\nimport styles from './FilterToolbar.module.css';\n\nexport interface FilterToolbarProps {\n /** Derived facets from `deriveFacets()`; ALL render (revised D-05) — one with no values is disabled. */\n facets: DerivedFacet[];\n /** Current selections per facet (hydrated from the URL state in 06-04). */\n selected: Record<FacetKey, string[]>;\n onToggleFacet: (key: FacetKey, value: string) => void;\n /** Per-facet menu-footer \"Clear\" (clears just that facet). */\n onClearFacet: (key: FacetKey) => void;\n /** Toolbar \"Clear all\" (resets every facet + search). */\n onClearAll: () => void;\n /** `isActive(state)` — gates the \"Clear all\" affordance. */\n active: boolean;\n /** Filtered result count for the polite live region (UI-07). */\n resultCount: number;\n /** `SearchBar` mounted inline in the toolbar row. */\n searchBar: ReactNode;\n /** View-mode toggle (Flat/List) mounted at the right of the toolbar row. */\n viewToggle?: ReactNode;\n}\n\n/**\n * A single multi-select facet as an outlined disclosure button. Options are @backstage/ui\n * `Checkbox`es in a popover that stays OPEN while toggling (multi-select); the popover closes on\n * outside-click or Escape. All dynamic text (`facet.label`, `option.label`) renders as React\n * children only — never raw HTML (T-06-03-01).\n */\nfunction FacetDropdown({\n facet,\n selected,\n isOpen,\n onOpenToggle,\n onClose,\n onToggle,\n onClear,\n}: {\n facet: DerivedFacet;\n selected: string[];\n isOpen: boolean;\n onOpenToggle: () => void;\n onClose: () => void;\n onToggle: (value: string) => void;\n onClear: () => void;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const menuId = `facet-menu-${facet.key}`;\n const count = selected.length;\n\n useEffect(() => {\n if (!isOpen) {\n return undefined;\n }\n const onDocMouseDown = (event: MouseEvent) => {\n if (ref.current && !ref.current.contains(event.target as Node)) {\n onClose();\n }\n };\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n onClose();\n }\n };\n document.addEventListener('mousedown', onDocMouseDown);\n document.addEventListener('keydown', onKeyDown);\n return () => {\n document.removeEventListener('mousedown', onDocMouseDown);\n document.removeEventListener('keydown', onKeyDown);\n };\n }, [isOpen, onClose]);\n\n return (\n <div ref={ref} className={styles.facet}>\n <Button\n variant={count > 0 ? 'primary' : 'secondary'}\n size=\"small\"\n isDisabled={!facet.enabled}\n iconEnd={<RiArrowDropDownLine />}\n aria-haspopup=\"true\"\n aria-expanded={isOpen}\n aria-controls={isOpen ? menuId : undefined}\n aria-label={\n count > 0\n ? `${facet.label} filter, ${count} selected`\n : `${facet.label} filter`\n }\n onPress={onOpenToggle}\n >\n {facet.label}\n {count > 0 ? <span className={styles.countChip}>{count}</span> : null}\n </Button>\n {isOpen ? (\n <div\n id={menuId}\n className={styles.menu}\n role=\"group\"\n aria-label={`${facet.label} options`}\n >\n {facet.options.map(option => (\n <Checkbox\n key={option.value}\n className={styles.option}\n isSelected={selected.includes(option.value)}\n onChange={() => onToggle(option.value)}\n >\n {option.label}\n </Checkbox>\n ))}\n {count > 0 ? (\n <div className={styles.menuFooter}>\n <Button variant=\"tertiary\" size=\"small\" onPress={onClear}>\n Clear\n </Button>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Presentational Phase 6 toolbar (D-01 top toolbar, D-02 multi-select dropdowns). Renders EVERY\n * facet in `FACET_ORDER` (revised D-05 — a facet with no values renders disabled) followed on the\n * same row by the inline `searchBar` paired with the `viewToggle` (and a conditional \"Clear all\"),\n * plus a polite `aria-live` result-count region. Purely props-driven — no data fetching and no\n * URL/provider access (that wiring is 06-04). (UI-06, UI-07)\n */\nexport function FilterToolbar({\n facets,\n selected,\n onToggleFacet,\n onClearFacet,\n onClearAll,\n active,\n resultCount,\n searchBar,\n viewToggle,\n}: FilterToolbarProps) {\n // Only one facet menu open at a time — opening another closes the previous.\n const [openKey, setOpenKey] = useState<FacetKey | null>(null);\n const closeMenus = useCallback(() => setOpenKey(null), []);\n\n return (\n <div role=\"search\" aria-label=\"Filter and search APIs\">\n <div className={styles.toolbar}>\n {facets.map(facet => (\n <FacetDropdown\n key={facet.key}\n facet={facet}\n selected={selected[facet.key]}\n isOpen={openKey === facet.key}\n onOpenToggle={() =>\n setOpenKey(prev => (prev === facet.key ? null : facet.key))\n }\n onClose={closeMenus}\n onToggle={value => onToggleFacet(facet.key, value)}\n onClear={() => onClearFacet(facet.key)}\n />\n ))}\n <div className={styles.searchRow}>\n {searchBar}\n {active ? (\n <Button\n variant=\"tertiary\"\n size=\"small\"\n iconStart={<RiCloseLine />}\n onPress={onClearAll}\n >\n Clear all\n </Button>\n ) : null}\n {viewToggle}\n </div>\n </div>\n <span className={styles.srOnly} aria-live=\"polite\">\n {`${resultCount} APIs match your filters`}\n </span>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import styleInject from '../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
2
 
3
- var css_248z = ".FilterToolbar_toolbar__a9218d3e6c {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--bui-space-2);\n margin-bottom: var(--bui-space-4);\n}\n\n.FilterToolbar_facet__a9218d3e6c {\n position: relative;\n}\n\n.FilterToolbar_countChip__a9218d3e6c {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 18px;\n height: 18px;\n margin-left: var(--bui-space-1);\n padding: 0 var(--bui-space-1);\n border-radius: var(--bui-radius-full);\n background-color: var(--bui-bg-solid);\n color: var(--bui-fg-solid);\n font-size: var(--bui-font-size-1);\n font-variant-numeric: tabular-nums;\n}\n\n.FilterToolbar_menu__a9218d3e6c {\n position: absolute;\n top: calc(100% + var(--bui-space-1));\n left: 0;\n z-index: 10;\n min-width: 200px;\n padding: var(--bui-space-2) 0;\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-3);\n background-color: var(--bui-bg-app);\n box-shadow: var(--bui-shadow);\n}\n\n.FilterToolbar_option__a9218d3e6c {\n padding: var(--bui-space-1) var(--bui-space-3);\n}\n\n.FilterToolbar_optionLabel__a9218d3e6c {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n}\n\n.FilterToolbar_menuFooter__a9218d3e6c {\n display: flex;\n justify-content: flex-end;\n padding: var(--bui-space-1) var(--bui-space-2) 0;\n}\n\n.FilterToolbar_clearAll__a9218d3e6c {\n margin-left: auto;\n}\n\n.FilterToolbar_srOnly__a9218d3e6c {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n}\n";
4
- var styles = {"toolbar":"FilterToolbar_toolbar__a9218d3e6c","facet":"FilterToolbar_facet__a9218d3e6c","countChip":"FilterToolbar_countChip__a9218d3e6c","menu":"FilterToolbar_menu__a9218d3e6c","option":"FilterToolbar_option__a9218d3e6c","optionLabel":"FilterToolbar_optionLabel__a9218d3e6c","menuFooter":"FilterToolbar_menuFooter__a9218d3e6c","clearAll":"FilterToolbar_clearAll__a9218d3e6c","srOnly":"FilterToolbar_srOnly__a9218d3e6c"};
3
+ var css_248z = ".FilterToolbar_toolbar__56038ba351 {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--bui-space-2);\n margin-bottom: var(--bui-space-4);\n}\n\n.FilterToolbar_facet__56038ba351 {\n position: relative;\n}\n\n.FilterToolbar_countChip__56038ba351 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 18px;\n height: 18px;\n margin-left: var(--bui-space-1);\n padding: 0 var(--bui-space-1);\n border-radius: var(--bui-radius-full);\n background-color: var(--bui-bg-solid);\n color: var(--bui-fg-solid);\n font-size: var(--bui-font-size-1);\n font-variant-numeric: tabular-nums;\n}\n\n.FilterToolbar_menu__56038ba351 {\n position: absolute;\n top: calc(100% + var(--bui-space-1));\n left: 0;\n z-index: 10;\n min-width: 200px;\n padding: var(--bui-space-2) 0;\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-3);\n background-color: var(--bui-bg-app);\n box-shadow: var(--bui-shadow);\n}\n\n.FilterToolbar_option__56038ba351 {\n padding: var(--bui-space-1) var(--bui-space-3);\n}\n\n.FilterToolbar_menuFooter__56038ba351 {\n display: flex;\n justify-content: flex-end;\n padding: var(--bui-space-1) var(--bui-space-2) 0;\n}\n\n.FilterToolbar_searchRow__56038ba351 {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: var(--bui-space-2);\n}\n\n.FilterToolbar_srOnly__56038ba351 {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n}\n";
4
+ var styles = {"toolbar":"FilterToolbar_toolbar__56038ba351","facet":"FilterToolbar_facet__56038ba351","countChip":"FilterToolbar_countChip__56038ba351","menu":"FilterToolbar_menu__56038ba351","option":"FilterToolbar_option__56038ba351","menuFooter":"FilterToolbar_menuFooter__56038ba351","searchRow":"FilterToolbar_searchRow__56038ba351","srOnly":"FilterToolbar_srOnly__56038ba351"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { styles as default };
@@ -1,6 +1,6 @@
1
1
  import { parseEntityRef } from '@backstage/catalog-model';
2
2
  import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
3
- import { ANNOTATION_CATEGORY, ANNOTATION_API_HUB_VERSION, ANNOTATION_HAS_SPEC } from '../constants.esm.js';
3
+ import { ANNOTATION_VERSION, ANNOTATION_PROXY_NAME, ANNOTATION_HAS_SPEC, LABEL_DEPARTMENT, ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants.esm.js';
4
4
 
5
5
  function toBadge(lifecycle) {
6
6
  switch (lifecycle) {
@@ -14,6 +14,18 @@ function toBadge(lifecycle) {
14
14
  return { label: "Unknown", kind: "warning" };
15
15
  }
16
16
  }
17
+ function toVisibilities(annotations) {
18
+ const raw = annotations[ANNOTATION_APIGEE_ORGS] ?? annotations[ANNOTATION_APIGEE_ORG] ?? "";
19
+ const orgs = raw.split(",").map((org) => org.trim().toLowerCase()).filter(Boolean);
20
+ const visibilities = [];
21
+ if (orgs.some((org) => org.includes("private"))) visibilities.push("private");
22
+ if (orgs.some((org) => org.includes("public"))) visibilities.push("public");
23
+ return visibilities;
24
+ }
25
+ function versionFromProxyName(proxyName) {
26
+ const match = proxyName?.match(/-v(\d+)$/i);
27
+ return match ? `${Number(match[1])}.0.0` : void 0;
28
+ }
17
29
  function toApiCardViewModel(entity) {
18
30
  const annotations = entity.metadata.annotations ?? {};
19
31
  const owner = entity.spec?.owner;
@@ -24,10 +36,11 @@ function toApiCardViewModel(entity) {
24
36
  parseEntityRef(owner, { defaultKind: "group", defaultNamespace: "default" }),
25
37
  { defaultKind: "group" }
26
38
  ) : "unknown",
39
+ department: entity.metadata.labels?.[LABEL_DEPARTMENT] || void 0,
40
+ visibilities: toVisibilities(annotations),
27
41
  badge: toBadge(entity.spec?.lifecycle),
28
42
  hasSpec: annotations[ANNOTATION_HAS_SPEC] === "true",
29
- version: annotations[ANNOTATION_API_HUB_VERSION] || void 0,
30
- category: annotations[ANNOTATION_CATEGORY]?.split(",")[0]?.trim() || void 0,
43
+ version: annotations[ANNOTATION_VERSION] || versionFromProxyName(annotations[ANNOTATION_PROXY_NAME]) || void 0,
31
44
  description,
32
45
  targetRef: {
33
46
  kind: "API",
@@ -1 +1 @@
1
- {"version":3,"file":"apiCardViewModel.esm.js","sources":["../../src/components/apiCardViewModel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\nimport {\n ANNOTATION_HAS_SPEC,\n ANNOTATION_CATEGORY,\n ANNOTATION_API_HUB_VERSION,\n} from '../constants';\n\nexport type LifecycleBadgeKind = 'ok' | 'warning' | 'error';\n\nexport interface ApiCardViewModel {\n /** metadata.title ?? metadata.name */\n name: string;\n /** humanizeEntityRef(spec.owner, { defaultKind: 'group' }) — 'unknown' when absent */\n ownerLabel: string;\n badge: { label: string; kind: LifecycleBadgeKind };\n /** annotations['apigee.com/has-spec'] === 'true' */\n hasSpec: boolean;\n /** annotations['apigee.com/api-hub-version'] — omitted when absent (D-05) */\n version?: string;\n /** first comma-separated slug of apigee.com/category — undefined => 'Other' bucket */\n category?: string;\n /** metadata.description — omitted when empty */\n description?: string;\n /** for entityRouteRef navigation (05-04) */\n targetRef: { kind: 'API'; namespace: string; name: string };\n}\n\n/** D-04 lifecycle -> badge mapping. Unknown/undeployed/missing default to warning. */\nfunction toBadge(lifecycle: string | undefined): {\n label: string;\n kind: LifecycleBadgeKind;\n} {\n switch (lifecycle) {\n case 'production':\n return { label: 'Production', kind: 'ok' };\n case 'deprecated':\n return { label: 'Deprecated', kind: 'error' };\n case 'experimental':\n return { label: 'Experimental', kind: 'warning' };\n default:\n return { label: 'Unknown', kind: 'warning' };\n }\n}\n\n/** PURE mapper: an Apigee `kind: API` entity -> the view-model each tile renders. */\nexport function toApiCardViewModel(entity: Entity): ApiCardViewModel {\n const annotations = entity.metadata.annotations ?? {};\n const owner = entity.spec?.owner as string | undefined;\n const description = entity.metadata.description?.trim()\n ? entity.metadata.description\n : undefined;\n\n return {\n name: entity.metadata.title ?? entity.metadata.name,\n ownerLabel: owner\n ? humanizeEntityRef(\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\n { defaultKind: 'group' },\n )\n : 'unknown',\n badge: toBadge(entity.spec?.lifecycle as string | undefined),\n hasSpec: annotations[ANNOTATION_HAS_SPEC] === 'true',\n version: annotations[ANNOTATION_API_HUB_VERSION] || undefined,\n category: annotations[ANNOTATION_CATEGORY]?.split(',')[0]?.trim() || undefined,\n description,\n targetRef: {\n kind: 'API',\n namespace: entity.metadata.namespace ?? 'default',\n name: entity.metadata.name,\n },\n };\n}\n"],"names":[],"mappings":";;;;AA8BA,SAAS,QAAQ,SAAA,EAGf;AACA,EAAA,QAAQ,SAAA;AAAW,IACjB,KAAK,YAAA;AACH,MAAA,OAAO,EAAE,KAAA,EAAO,YAAA,EAAc,IAAA,EAAM,IAAA,EAAK;AAAA,IAC3C,KAAK,YAAA;AACH,MAAA,OAAO,EAAE,KAAA,EAAO,YAAA,EAAc,IAAA,EAAM,OAAA,EAAQ;AAAA,IAC9C,KAAK,cAAA;AACH,MAAA,OAAO,EAAE,KAAA,EAAO,cAAA,EAAgB,IAAA,EAAM,SAAA,EAAU;AAAA,IAClD;AACE,MAAA,OAAO,EAAE,KAAA,EAAO,SAAA,EAAW,IAAA,EAAM,SAAA,EAAU;AAAA;AAEjD;AAGO,SAAS,mBAAmB,MAAA,EAAkC;AACnE,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,CAAS,WAAA,IAAe,EAAC;AACpD,EAAA,MAAM,KAAA,GAAQ,OAAO,IAAA,EAAM,KAAA;AAC3B,EAAA,MAAM,WAAA,GAAc,OAAO,QAAA,CAAS,WAAA,EAAa,MAAK,GAClD,MAAA,CAAO,SAAS,WAAA,GAChB,MAAA;AAEJ,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,MAAA,CAAO,QAAA,CAAS,KAAA,IAAS,OAAO,QAAA,CAAS,IAAA;AAAA,IAC/C,YAAY,KAAA,GACR,iBAAA;AAAA,MACE,eAAe,KAAA,EAAO,EAAE,aAAa,OAAA,EAAS,gBAAA,EAAkB,WAAW,CAAA;AAAA,MAC3E,EAAE,aAAa,OAAA;AAAQ,KACzB,GACA,SAAA;AAAA,IACJ,KAAA,EAAO,OAAA,CAAQ,MAAA,CAAO,IAAA,EAAM,SAA+B,CAAA;AAAA,IAC3D,OAAA,EAAS,WAAA,CAAY,mBAAmB,CAAA,KAAM,MAAA;AAAA,IAC9C,OAAA,EAAS,WAAA,CAAY,0BAA0B,CAAA,IAAK,MAAA;AAAA,IACpD,QAAA,EAAU,WAAA,CAAY,mBAAmB,CAAA,EAAG,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,EAAG,IAAA,EAAK,IAAK,MAAA;AAAA,IACrE,WAAA;AAAA,IACA,SAAA,EAAW;AAAA,MACT,IAAA,EAAM,KAAA;AAAA,MACN,SAAA,EAAW,MAAA,CAAO,QAAA,CAAS,SAAA,IAAa,SAAA;AAAA,MACxC,IAAA,EAAM,OAAO,QAAA,CAAS;AAAA;AACxB,GACF;AACF;;;;"}
1
+ {"version":3,"file":"apiCardViewModel.esm.js","sources":["../../src/components/apiCardViewModel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\nimport {\n ANNOTATION_HAS_SPEC,\n ANNOTATION_VERSION,\n ANNOTATION_PROXY_NAME,\n ANNOTATION_APIGEE_ORGS,\n ANNOTATION_APIGEE_ORG,\n LABEL_DEPARTMENT,\n} from '../constants';\n\nexport type LifecycleBadgeKind = 'ok' | 'warning' | 'error';\n\n/** Apigee org exposure shown on the card, derived from the org name. */\nexport type ApiVisibility = 'private' | 'public';\n\nexport interface ApiCardViewModel {\n /** metadata.title ?? metadata.name */\n name: string;\n /** humanizeEntityRef(spec.owner, { defaultKind: 'group' }) — 'unknown' when absent */\n ownerLabel: string;\n /** metadata.labels.department — omitted when absent */\n department?: string;\n /** Exposure derived from apigee.com/orgs — 'private', 'public', or both; [] when none. */\n visibilities: ApiVisibility[];\n badge: { label: string; kind: LifecycleBadgeKind };\n /** annotations['apigee.com/has-spec'] === 'true' */\n hasSpec: boolean;\n /** annotations['apigee.com/version'], else the proxy name's -v<major> as x.0.0 — omitted when absent (D-05) */\n version?: string;\n /** metadata.description — omitted when empty */\n description?: string;\n /** for entityRouteRef navigation (05-04) */\n targetRef: { kind: 'API'; namespace: string; name: string };\n}\n\n/** D-04 lifecycle -> badge mapping. Unknown/undeployed/missing default to warning. */\nfunction toBadge(lifecycle: string | undefined): {\n label: string;\n kind: LifecycleBadgeKind;\n} {\n switch (lifecycle) {\n case 'production':\n return { label: 'Production', kind: 'ok' };\n case 'deprecated':\n return { label: 'Deprecated', kind: 'error' };\n case 'experimental':\n return { label: 'Experimental', kind: 'warning' };\n default:\n return { label: 'Unknown', kind: 'warning' };\n }\n}\n\n/** Derive exposure from the Apigee org annotation(s). Org names follow\n * `apigee-x-{private|public}-p-<id>`; an API can be published to one or both. */\nfunction toVisibilities(annotations: Record<string, string>): ApiVisibility[] {\n const raw =\n annotations[ANNOTATION_APIGEE_ORGS] ?? annotations[ANNOTATION_APIGEE_ORG] ?? '';\n const orgs = raw\n .split(',')\n .map(org => org.trim().toLowerCase())\n .filter(Boolean);\n const visibilities: ApiVisibility[] = [];\n if (orgs.some(org => org.includes('private'))) visibilities.push('private');\n if (orgs.some(org => org.includes('public'))) visibilities.push('public');\n return visibilities;\n}\n\n/** Extract a semantic version from a proxy name's trailing `-v<major>` segment\n * (e.g. `...-v4` -> `4.0.0`). Undefined when the name has no version suffix. */\nfunction versionFromProxyName(proxyName: string | undefined): string | undefined {\n const match = proxyName?.match(/-v(\\d+)$/i);\n return match ? `${Number(match[1])}.0.0` : undefined;\n}\n\n/** PURE mapper: an Apigee `kind: API` entity -> the view-model each tile renders. */\nexport function toApiCardViewModel(entity: Entity): ApiCardViewModel {\n const annotations = entity.metadata.annotations ?? {};\n const owner = entity.spec?.owner as string | undefined;\n const description = entity.metadata.description?.trim()\n ? entity.metadata.description\n : undefined;\n\n return {\n name: entity.metadata.title ?? entity.metadata.name,\n ownerLabel: owner\n ? humanizeEntityRef(\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\n { defaultKind: 'group' },\n )\n : 'unknown',\n department: entity.metadata.labels?.[LABEL_DEPARTMENT] || undefined,\n visibilities: toVisibilities(annotations),\n badge: toBadge(entity.spec?.lifecycle as string | undefined),\n hasSpec: annotations[ANNOTATION_HAS_SPEC] === 'true',\n version:\n annotations[ANNOTATION_VERSION] ||\n versionFromProxyName(annotations[ANNOTATION_PROXY_NAME]) ||\n undefined,\n description,\n targetRef: {\n kind: 'API',\n namespace: entity.metadata.namespace ?? 'default',\n name: entity.metadata.name,\n },\n };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants.esm.js';
2
+ import { humanise } from './grouping.esm.js';
3
+ import { toApiCardViewModel } from './apiCardViewModel.esm.js';
4
+
5
+ function slugify(name) {
6
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
7
+ }
8
+ const ENV_REVISION_KEY = /^apigee\.com\/([a-z0-9-]+)\.env\.([a-z0-9-]+)\.revision$/;
9
+ function envsByOrgSlug(component) {
10
+ const byOrg = /* @__PURE__ */ new Map();
11
+ for (const [key, revision] of Object.entries(component?.metadata.annotations ?? {})) {
12
+ const match = ENV_REVISION_KEY.exec(key);
13
+ if (!match) {
14
+ continue;
15
+ }
16
+ const [, orgSlug, envSlug] = match;
17
+ const list = byOrg.get(orgSlug) ?? [];
18
+ list.push({ name: envSlug, revision });
19
+ byOrg.set(orgSlug, list);
20
+ }
21
+ return byOrg;
22
+ }
23
+ function toRuntimeOrgs(api, component, orgLabels) {
24
+ const annotations = api.metadata.annotations ?? {};
25
+ const raw = annotations[ANNOTATION_APIGEE_ORGS] ?? annotations[ANNOTATION_APIGEE_ORG] ?? "";
26
+ const orgValues = raw.split(",").map((value) => value.trim()).filter(Boolean);
27
+ const envMap = envsByOrgSlug(component);
28
+ return orgValues.map((value) => ({
29
+ value,
30
+ label: orgLabels?.[value] ?? humanise(value),
31
+ environments: (envMap.get(slugify(value)) ?? []).sort(
32
+ (a, b) => a.name.localeCompare(b.name)
33
+ )
34
+ }));
35
+ }
36
+ function firstSourceRepo(entity) {
37
+ const link = (entity.metadata.links ?? []).find((l) => /^https?:\/\//i.test(l.url));
38
+ return link ? { url: link.url, title: link.title || "Source repository" } : void 0;
39
+ }
40
+ function specFileName(baseName, definition) {
41
+ const ext = definition.trim().startsWith("{") ? "json" : "yaml";
42
+ return `${baseName}.${ext}`;
43
+ }
44
+ function toApiDetailViewModel(api, component, orgLabels) {
45
+ const base = toApiCardViewModel(api);
46
+ const spec = api.spec ?? {};
47
+ return {
48
+ ...base,
49
+ kind: api.kind,
50
+ type: spec.type || void 0,
51
+ ownerRef: spec.owner || void 0,
52
+ tags: api.metadata.tags ?? [],
53
+ definition: typeof spec.definition === "string" ? spec.definition : "",
54
+ sourceRepo: firstSourceRepo(api),
55
+ runtimeOrgs: toRuntimeOrgs(api, component, orgLabels)
56
+ };
57
+ }
58
+
59
+ export { specFileName, toApiDetailViewModel, toRuntimeOrgs };
60
+ //# sourceMappingURL=apiDetailViewModel.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiDetailViewModel.esm.js","sources":["../../src/components/apiDetailViewModel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport type { Entity } from '@backstage/catalog-model';\nimport { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants';\nimport { humanise } from './grouping';\nimport { toApiCardViewModel, type ApiCardViewModel } from './apiCardViewModel';\n\n/** One deployed environment with the revision running there (per org). */\nexport interface ApiRuntimeEnv {\n /** Environment slug (e.g. 'prod-1'), as recorded on the Component annotation. */\n name: string;\n /** Deployed proxy revision in this environment (e.g. '3'). */\n revision: string;\n}\n\n/** An Apigee org this API is published to, with its deployed environments. */\nexport interface ApiRuntimeOrg {\n /** Raw org value — matches the `apigee.com/orgs` entry and the org facet identity. */\n value: string;\n /** Display label (config `orgLabels` override, else humanised). */\n label: string;\n /** Deployed environments with revisions; empty when nothing is deployed. */\n environments: ApiRuntimeEnv[];\n}\n\n/** A source-code repository link, read from `metadata.links`. */\nexport interface ApiSourceRepo {\n url: string;\n title: string;\n}\n\n/**\n * Everything the details drawer renders for one API. Extends the card view-model\n * (the shared header/badge/owner fields) with the richer overview, runtime and spec data.\n */\nexport interface ApiDetailViewModel extends ApiCardViewModel {\n /** entity.kind — always 'API' here; shown as an overview row. */\n kind: string;\n /** spec.type (openapi, other, …) — omitted when absent. */\n type?: string;\n /** Raw spec.owner entity ref (ownerLabel is the display text). */\n ownerRef?: string;\n /** metadata.tags — [] when none. */\n tags: string[];\n /** spec.definition — the embedded OpenAPI/AsyncAPI document; '' when not embedded. */\n definition: string;\n /** First source-repository link from metadata.links. */\n sourceRepo?: ApiSourceRepo;\n /** Per-org runtime (environments + revisions). */\n runtimeOrgs: ApiRuntimeOrg[];\n}\n\n/**\n * Frontend mirror of the backend `slugify` (entity-builder.ts): lowercase, non-alphanumeric\n * runs → single hyphen, trimmed. Used to match an org value to the Component's\n * `apigee.com/<orgSlug>.env.<envSlug>.revision` annotation keys.\n */\nfunction slugify(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/** `apigee.com/<orgSlug>.env.<envSlug>.revision` — slugs never contain '.', so this is unambiguous. */\nconst ENV_REVISION_KEY = /^apigee\\.com\\/([a-z0-9-]+)\\.env\\.([a-z0-9-]+)\\.revision$/;\n\n/** Group a Component's per-environment revision annotations by org slug. */\nfunction envsByOrgSlug(component: Entity | undefined): Map<string, ApiRuntimeEnv[]> {\n const byOrg = new Map<string, ApiRuntimeEnv[]>();\n for (const [key, revision] of Object.entries(component?.metadata.annotations ?? {})) {\n const match = ENV_REVISION_KEY.exec(key);\n if (!match) {\n continue;\n }\n const [, orgSlug, envSlug] = match;\n const list = byOrg.get(orgSlug) ?? [];\n list.push({ name: envSlug, revision });\n byOrg.set(orgSlug, list);\n }\n return byOrg;\n}\n\n/**\n * Resolve the runtime orgs for an API. Org identity + order come from the API's\n * `apigee.com/orgs` (or singular `org`) annotation — the same value the facets use — while the\n * deployed environments/revisions come from the matching Component entity (Component-only\n * annotations). Hub-only APIs have no Component, so their orgs render with empty environment lists.\n */\nexport function toRuntimeOrgs(\n api: Entity,\n component: Entity | undefined,\n orgLabels?: Record<string, string>,\n): ApiRuntimeOrg[] {\n const annotations = api.metadata.annotations ?? {};\n const raw =\n annotations[ANNOTATION_APIGEE_ORGS] ?? annotations[ANNOTATION_APIGEE_ORG] ?? '';\n const orgValues = raw\n .split(',')\n .map(value => value.trim())\n .filter(Boolean);\n const envMap = envsByOrgSlug(component);\n return orgValues.map(value => ({\n value,\n label: orgLabels?.[value] ?? humanise(value),\n environments: (envMap.get(slugify(value)) ?? []).sort((a, b) =>\n a.name.localeCompare(b.name),\n ),\n }));\n}\n\n/** First http(s) link from metadata.links — the backend records the GitHub source repository here. */\nfunction firstSourceRepo(entity: Entity): ApiSourceRepo | undefined {\n const link = (entity.metadata.links ?? []).find(l => /^https?:\\/\\//i.test(l.url));\n return link ? { url: link.url, title: link.title || 'Source repository' } : undefined;\n}\n\n/** Download filename for a spec: `.json` for JSON, else `.yaml`. */\nexport function specFileName(baseName: string, definition: string): string {\n const ext = definition.trim().startsWith('{') ? 'json' : 'yaml';\n return `${baseName}.${ext}`;\n}\n\n/**\n * PURE mapper: an Apigee `kind: API` entity (plus its optional Component sibling for runtime\n * data) → the view-model the details drawer renders. Reuses `toApiCardViewModel` for the shared\n * header/badge/owner fields.\n */\nexport function toApiDetailViewModel(\n api: Entity,\n component?: Entity,\n orgLabels?: Record<string, string>,\n): ApiDetailViewModel {\n const base = toApiCardViewModel(api);\n const spec = (api.spec ?? {}) as Record<string, unknown>;\n return {\n ...base,\n kind: api.kind,\n type: (spec.type as string | undefined) || undefined,\n ownerRef: (spec.owner as string | undefined) || undefined,\n tags: api.metadata.tags ?? [],\n definition: typeof spec.definition === 'string' ? spec.definition : '',\n sourceRepo: firstSourceRepo(api),\n runtimeOrgs: toRuntimeOrgs(api, component, orgLabels),\n };\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { parseEntityRef } from '@backstage/catalog-model';
2
2
  import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
3
- import { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG, LABEL_DEPARTMENT, ANNOTATION_CATEGORY } from '../constants.esm.js';
3
+ import { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG, LABEL_DEPARTMENT } from '../constants.esm.js';
4
4
 
5
5
  function isApigeeEntity(e) {
6
6
  const a = e.metadata.annotations ?? {};
@@ -18,8 +18,6 @@ const entityOrg = (e) => {
18
18
  const a = e.metadata.annotations ?? {};
19
19
  return (a[ANNOTATION_APIGEE_ORGS] ?? a[ANNOTATION_APIGEE_ORG])?.split(",")[0]?.trim() || void 0;
20
20
  };
21
- const entityCategory = (e) => (e.metadata.annotations ?? {})[ANNOTATION_CATEGORY]?.split(",")[0]?.trim() || void 0;
22
- const entityCategoryFacet = (e) => entityCategory(e) ?? "other";
23
21
  const entityDepartment = (e) => (e.metadata.labels ?? {})[LABEL_DEPARTMENT]?.trim() || void 0;
24
22
  class FieldFacetFilter {
25
23
  constructor(extract, values) {
@@ -38,8 +36,7 @@ function apiSearchHaystack(e) {
38
36
  e.metadata.title ?? e.metadata.name,
39
37
  e.metadata.description ?? "",
40
38
  entityOwnerLabel(e) ?? "",
41
- entityOrg(e) ?? "",
42
- entityCategory(e) ?? ""
39
+ entityOrg(e) ?? ""
43
40
  ].join(" ").toLowerCase();
44
41
  }
45
42
  class ApiSearchFilter {
@@ -58,5 +55,5 @@ class ApiKindFilter {
58
55
  }
59
56
  }
60
57
 
61
- export { ApiKindFilter, ApiSearchFilter, FieldFacetFilter, apiSearchHaystack, entityCategory, entityCategoryFacet, entityDepartment, entityLifecycle, entityOrg, entityOwnerLabel, isApigeeEntity };
58
+ export { ApiKindFilter, ApiSearchFilter, FieldFacetFilter, apiSearchHaystack, entityDepartment, entityLifecycle, entityOrg, entityOwnerLabel, isApigeeEntity };
62
59
  //# sourceMappingURL=apigeeFacetFilters.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"apigeeFacetFilters.esm.js","sources":["../../src/components/apigeeFacetFilters.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\nimport type { EntityFilter } from '@backstage/plugin-catalog-react';\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\nimport {\n ANNOTATION_APIGEE_ORGS,\n ANNOTATION_APIGEE_ORG,\n ANNOTATION_CATEGORY,\n LABEL_DEPARTMENT,\n} from '../constants';\n\n/** Dual-key is-Apigee gate (same rule as the existing ApigeeOrgFilter). */\nexport function isApigeeEntity(e: Entity): boolean {\n const a = e.metadata.annotations ?? {};\n return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;\n}\n\n// --- Pure field extractors: the ONE place each facet's value is defined. Reused by\n// deriveFacets(), the facet predicates, and the search haystack. Unit-testable. ---\nexport const entityLifecycle = (e: Entity): string | undefined =>\n (e.spec?.lifecycle as string | undefined)?.trim() || undefined;\n\nexport const entityOwnerLabel = (e: Entity): string | undefined => {\n const owner = e.spec?.owner as string | undefined;\n return owner\n ? humanizeEntityRef(\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\n { defaultKind: 'group' },\n ) // 'group:default/payments-team' -> 'payments-team'\n : undefined;\n};\n\nexport const entityOrg = (e: Entity): string | undefined => {\n const a = e.metadata.annotations ?? {};\n return (\n (a[ANNOTATION_APIGEE_ORGS] ?? a[ANNOTATION_APIGEE_ORG])?.split(',')[0]?.trim() ||\n undefined\n );\n};\n\nexport const entityCategory = (e: Entity): string | undefined =>\n (e.metadata.annotations ?? {})[ANNOTATION_CATEGORY]?.split(',')[0]?.trim() ||\n undefined;\n\n/**\n * Facet-only category value: maps an uncategorised entity to the 'other' bucket\n * (LOCKED UI-SPEC Facet Data). Search uses `entityCategory` (the REAL slug) instead,\n * so a search for \"other\" does not match uncategorised APIs.\n */\nexport const entityCategoryFacet = (e: Entity): string =>\n entityCategory(e) ?? 'other';\n\n/** Department facet value: the `department` label written from the API Hub \"Business unit\". */\nexport const entityDepartment = (e: Entity): string | undefined =>\n (e.metadata.labels ?? {})[LABEL_DEPARTMENT]?.trim() || undefined;\n\n/** Generic single-field multi-value facet. Within-facet OR. No URL seam / getCatalogFilters. */\nexport class FieldFacetFilter implements EntityFilter {\n constructor(\n private readonly extract: (e: Entity) => string | undefined,\n public readonly values: string[],\n ) {}\n filterEntity(entity: Entity): boolean {\n const v = this.extract(entity);\n return v !== undefined && this.values.includes(v); // OR across selected values\n }\n}\n\n/** Broad \"find anything\" search: name + description + owner + org + category (D-09). */\nexport function apiSearchHaystack(e: Entity): string {\n return [\n e.metadata.title ?? e.metadata.name,\n e.metadata.description ?? '',\n entityOwnerLabel(e) ?? '',\n entityOrg(e) ?? '',\n entityCategory(e) ?? '',\n ]\n .join(' ')\n .toLowerCase();\n}\n\nexport class ApiSearchFilter implements EntityFilter {\n private readonly needle: string;\n constructor(query: string) {\n this.needle = query.trim().toLowerCase();\n }\n filterEntity(entity: Entity): boolean {\n // String.includes ONLY — never build a RegExp from user input (ReDoS guard).\n return this.needle === '' || apiSearchHaystack(entity).includes(this.needle);\n }\n}\n\n/**\n * Replaces EntityKindFilter('api','API'): keeps the backend kind=API filter but exposes no\n * URL seam, so the provider does not serialize `filters[kind]=API` into the URL (D-06).\n */\nexport class ApiKindFilter implements EntityFilter {\n readonly value = 'API';\n getCatalogFilters(): Record<string, string> {\n return { kind: this.value };\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"apigeeFacetFilters.esm.js","sources":["../../src/components/apigeeFacetFilters.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\nimport type { EntityFilter } from '@backstage/plugin-catalog-react';\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\nimport {\n ANNOTATION_APIGEE_ORGS,\n ANNOTATION_APIGEE_ORG,\n LABEL_DEPARTMENT,\n} from '../constants';\n\n/** Dual-key is-Apigee gate (same rule as the existing ApigeeOrgFilter). */\nexport function isApigeeEntity(e: Entity): boolean {\n const a = e.metadata.annotations ?? {};\n return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;\n}\n\n// --- Pure field extractors: the ONE place each facet's value is defined. Reused by\n// deriveFacets(), the facet predicates, and the search haystack. Unit-testable. ---\nexport const entityLifecycle = (e: Entity): string | undefined =>\n (e.spec?.lifecycle as string | undefined)?.trim() || undefined;\n\nexport const entityOwnerLabel = (e: Entity): string | undefined => {\n const owner = e.spec?.owner as string | undefined;\n return owner\n ? humanizeEntityRef(\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\n { defaultKind: 'group' },\n ) // 'group:default/payments-team' -> 'payments-team'\n : undefined;\n};\n\nexport const entityOrg = (e: Entity): string | undefined => {\n const a = e.metadata.annotations ?? {};\n return (\n (a[ANNOTATION_APIGEE_ORGS] ?? a[ANNOTATION_APIGEE_ORG])?.split(',')[0]?.trim() ||\n undefined\n );\n};\n\n/** Department facet value: the `department` label written from the API Hub \"Business unit\". */\nexport const entityDepartment = (e: Entity): string | undefined =>\n (e.metadata.labels ?? {})[LABEL_DEPARTMENT]?.trim() || undefined;\n\n/** Generic single-field multi-value facet. Within-facet OR. No URL seam / getCatalogFilters. */\nexport class FieldFacetFilter implements EntityFilter {\n constructor(\n private readonly extract: (e: Entity) => string | undefined,\n public readonly values: string[],\n ) {}\n filterEntity(entity: Entity): boolean {\n const v = this.extract(entity);\n return v !== undefined && this.values.includes(v); // OR across selected values\n }\n}\n\n/** Broad \"find anything\" search: name + description + owner + org (D-09). */\nexport function apiSearchHaystack(e: Entity): string {\n return [\n e.metadata.title ?? e.metadata.name,\n e.metadata.description ?? '',\n entityOwnerLabel(e) ?? '',\n entityOrg(e) ?? '',\n ]\n .join(' ')\n .toLowerCase();\n}\n\nexport class ApiSearchFilter implements EntityFilter {\n private readonly needle: string;\n constructor(query: string) {\n this.needle = query.trim().toLowerCase();\n }\n filterEntity(entity: Entity): boolean {\n // String.includes ONLY — never build a RegExp from user input (ReDoS guard).\n return this.needle === '' || apiSearchHaystack(entity).includes(this.needle);\n }\n}\n\n/**\n * Replaces EntityKindFilter('api','API'): keeps the backend kind=API filter but exposes no\n * URL seam, so the provider does not serialize `filters[kind]=API` into the URL (D-06).\n */\nexport class ApiKindFilter implements EntityFilter {\n readonly value = 'API';\n getCatalogFilters(): Record<string, string> {\n return { kind: this.value };\n }\n}\n"],"names":[],"mappings":";;;;AAWO,SAAS,eAAe,CAAA,EAAoB;AACjD,EAAA,MAAM,CAAA,GAAI,CAAA,CAAE,QAAA,CAAS,WAAA,IAAe,EAAC;AACrC,EAAA,OAAO,sBAAA,IAA0B,KAAK,qBAAA,IAAyB,CAAA;AACjE;AAIO,MAAM,kBAAkB,CAAC,CAAA,KAC7B,EAAE,IAAA,EAAM,SAAA,EAAkC,MAAK,IAAK;AAEhD,MAAM,gBAAA,GAAmB,CAAC,CAAA,KAAkC;AACjE,EAAA,MAAM,KAAA,GAAQ,EAAE,IAAA,EAAM,KAAA;AACtB,EAAA,OAAO,KAAA,GACH,iBAAA;AAAA,IACE,eAAe,KAAA,EAAO,EAAE,aAAa,OAAA,EAAS,gBAAA,EAAkB,WAAW,CAAA;AAAA,IAC3E,EAAE,aAAa,OAAA;AAAQ,GACzB,GACA,MAAA;AACN;AAEO,MAAM,SAAA,GAAY,CAAC,CAAA,KAAkC;AAC1D,EAAA,MAAM,CAAA,GAAI,CAAA,CAAE,QAAA,CAAS,WAAA,IAAe,EAAC;AACrC,EAAA,OAAA,CACG,CAAA,CAAE,sBAAsB,CAAA,IAAK,CAAA,CAAE,qBAAqB,CAAA,GAAI,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,EAAG,IAAA,EAAK,IAC7E,MAAA;AAEJ;AAGO,MAAM,gBAAA,GAAmB,CAAC,CAAA,KAAA,CAC9B,CAAA,CAAE,QAAA,CAAS,MAAA,IAAU,EAAC,EAAG,gBAAgB,CAAA,EAAG,IAAA,EAAK,IAAK;AAGlD,MAAM,gBAAA,CAAyC;AAAA,EACpD,WAAA,CACmB,SACD,MAAA,EAChB;AAFiB,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AACD,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EACf;AAAA,EAFgB,OAAA;AAAA,EACD,MAAA;AAAA,EAElB,aAAa,MAAA,EAAyB;AACpC,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,OAAA,CAAQ,MAAM,CAAA;AAC7B,IAAA,OAAO,CAAA,KAAM,MAAA,IAAa,IAAA,CAAK,MAAA,CAAO,SAAS,CAAC,CAAA;AAAA,EAClD;AACF;AAGO,SAAS,kBAAkB,CAAA,EAAmB;AACnD,EAAA,OAAO;AAAA,IACL,CAAA,CAAE,QAAA,CAAS,KAAA,IAAS,CAAA,CAAE,QAAA,CAAS,IAAA;AAAA,IAC/B,CAAA,CAAE,SAAS,WAAA,IAAe,EAAA;AAAA,IAC1B,gBAAA,CAAiB,CAAC,CAAA,IAAK,EAAA;AAAA,IACvB,SAAA,CAAU,CAAC,CAAA,IAAK;AAAA,GAClB,CACG,IAAA,CAAK,GAAG,CAAA,CACR,WAAA,EAAY;AACjB;AAEO,MAAM,eAAA,CAAwC;AAAA,EAClC,MAAA;AAAA,EACjB,YAAY,KAAA,EAAe;AACzB,IAAA,IAAA,CAAK,MAAA,GAAS,KAAA,CAAM,IAAA,EAAK,CAAE,WAAA,EAAY;AAAA,EACzC;AAAA,EACA,aAAa,MAAA,EAAyB;AAEpC,IAAA,OAAO,IAAA,CAAK,WAAW,EAAA,IAAM,iBAAA,CAAkB,MAAM,CAAA,CAAE,QAAA,CAAS,KAAK,MAAM,CAAA;AAAA,EAC7E;AACF;AAMO,MAAM,aAAA,CAAsC;AAAA,EACxC,KAAA,GAAQ,KAAA;AAAA,EACjB,iBAAA,GAA4C;AAC1C,IAAA,OAAO,EAAE,IAAA,EAAM,IAAA,CAAK,KAAA,EAAM;AAAA,EAC5B;AACF;;;;"}
@@ -4,7 +4,6 @@ const QKEY = {
4
4
  lifecycle: "lifecycle",
5
5
  org: "org",
6
6
  owner: "owner",
7
- category: "category",
8
7
  department: "department"
9
8
  };
10
9
  function parseSearchParams(sp) {
@@ -1 +1 @@
1
- {"version":3,"file":"apigeeSearchParams.esm.js","sources":["../../src/components/apigeeSearchParams.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { FACET_ORDER, type FacetKey } from './facetModel';\n\nexport interface ApigeeFilterState {\n selected: Record<FacetKey, string[]>;\n q: string;\n}\n\n/** Clean URL keys (D-06): one query key per facet, no native `filters[...]` wrapper. */\nconst QKEY: Record<FacetKey, string> = {\n lifecycle: 'lifecycle',\n org: 'org',\n owner: 'owner',\n category: 'category',\n department: 'department',\n};\n\n/**\n * URL → state. Repeated keys become arrays (D-07) via `getAll`; unknown/extra keys are\n * ignored. Values are treated as opaque strings — no eval, RegExp, or HTML (T-06-02-01);\n * an unknown facet value simply matches nothing downstream (derived allow-list).\n */\nexport function parseSearchParams(sp: URLSearchParams): ApigeeFilterState {\n const selected = Object.fromEntries(\n FACET_ORDER.map(k => [k, sp.getAll(QKEY[k])]),\n ) as Record<FacetKey, string[]>;\n return { selected, q: sp.get('q') ?? '' };\n}\n\n/**\n * state → URL. Deterministic order (FACET_ORDER, then `q` last) reproduces the exact SC #3\n * shape `?lifecycle=deprecated&q=payment`. Only the known clean keys are appended and empties\n * are omitted (T-06-02-02) — no injection surface. Multi-value facets serialize as repeated\n * keys (D-07) via `append`.\n */\nexport function buildSearchParams(state: ApigeeFilterState): URLSearchParams {\n const sp = new URLSearchParams();\n for (const k of FACET_ORDER) {\n for (const v of state.selected[k]) {\n sp.append(QKEY[k], v);\n }\n }\n if (state.q.trim()) {\n sp.set('q', state.q.trim());\n }\n return sp;\n}\n\n/** True when any facet is selected or the search box holds a non-blank query. */\nexport function isActive(state: ApigeeFilterState): boolean {\n return (\n state.q.trim() !== '' || FACET_ORDER.some(k => state.selected[k].length > 0)\n );\n}\n\n/**\n * Single-select URL update for the count cards: the clicked value becomes the ONLY active facet\n * selection (every other facet is cleared), so each card click filters to exactly that value\n * instead of accumulating like the toolbar's multi-select toggle. Clicking the current sole\n * selection clears it. The `q` search query is always preserved.\n */\nexport function selectOnlyFacetParams(\n prev: URLSearchParams,\n key: FacetKey,\n value: string,\n): URLSearchParams {\n const current = prev.getAll(QKEY[key]);\n const isSoleSelection =\n current.length === 1 &&\n current[0] === value &&\n FACET_ORDER.every(k => k === key || prev.getAll(QKEY[k]).length === 0);\n const next = new URLSearchParams();\n const q = prev.get('q');\n if (q) {\n next.set('q', q);\n }\n if (!isSoleSelection) {\n next.append(QKEY[key], value);\n }\n return next;\n}\n"],"names":[],"mappings":";;AASA,MAAM,IAAA,GAAiC;AAAA,EACrC,SAAA,EAAW,WAAA;AAAA,EACX,GAAA,EAAK,KAAA;AAAA,EACL,KAAA,EAAO,OAAA;AAAA,EACP,QAAA,EAAU,UAAA;AAAA,EACV,UAAA,EAAY;AACd,CAAA;AAOO,SAAS,kBAAkB,EAAA,EAAwC;AACxE,EAAA,MAAM,WAAW,MAAA,CAAO,WAAA;AAAA,IACtB,WAAA,CAAY,GAAA,CAAI,CAAA,CAAA,KAAK,CAAC,CAAA,EAAG,EAAA,CAAG,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAC,CAAC;AAAA,GAC9C;AACA,EAAA,OAAO,EAAE,QAAA,EAAU,CAAA,EAAG,GAAG,GAAA,CAAI,GAAG,KAAK,EAAA,EAAG;AAC1C;AAsBO,SAAS,SAAS,KAAA,EAAmC;AAC1D,EAAA,OACE,KAAA,CAAM,CAAA,CAAE,IAAA,EAAK,KAAM,EAAA,IAAM,WAAA,CAAY,IAAA,CAAK,CAAA,CAAA,KAAK,KAAA,CAAM,QAAA,CAAS,CAAC,CAAA,CAAE,SAAS,CAAC,CAAA;AAE/E;AAQO,SAAS,qBAAA,CACd,IAAA,EACA,GAAA,EACA,KAAA,EACiB;AACjB,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAC,CAAA;AACrC,EAAA,MAAM,eAAA,GACJ,QAAQ,MAAA,KAAW,CAAA,IACnB,QAAQ,CAAC,CAAA,KAAM,SACf,WAAA,CAAY,KAAA,CAAM,OAAK,CAAA,KAAM,GAAA,IAAO,KAAK,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAA,CAAE,WAAW,CAAC,CAAA;AACvE,EAAA,MAAM,IAAA,GAAO,IAAI,eAAA,EAAgB;AACjC,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AACtB,EAAA,IAAI,CAAA,EAAG;AACL,IAAA,IAAA,CAAK,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EACjB;AACA,EAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAAG,KAAK,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,IAAA;AACT;;;;"}
1
+ {"version":3,"file":"apigeeSearchParams.esm.js","sources":["../../src/components/apigeeSearchParams.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { FACET_ORDER, type FacetKey } from './facetModel';\n\nexport interface ApigeeFilterState {\n selected: Record<FacetKey, string[]>;\n q: string;\n}\n\n/** Clean URL keys (D-06): one query key per facet, no native `filters[...]` wrapper. */\nconst QKEY: Record<FacetKey, string> = {\n lifecycle: 'lifecycle',\n org: 'org',\n owner: 'owner',\n department: 'department',\n};\n\n/**\n * URL → state. Repeated keys become arrays (D-07) via `getAll`; unknown/extra keys are\n * ignored. Values are treated as opaque strings — no eval, RegExp, or HTML (T-06-02-01);\n * an unknown facet value simply matches nothing downstream (derived allow-list).\n */\nexport function parseSearchParams(sp: URLSearchParams): ApigeeFilterState {\n const selected = Object.fromEntries(\n FACET_ORDER.map(k => [k, sp.getAll(QKEY[k])]),\n ) as Record<FacetKey, string[]>;\n return { selected, q: sp.get('q') ?? '' };\n}\n\n/**\n * state → URL. Deterministic order (FACET_ORDER, then `q` last) reproduces the exact SC #3\n * shape `?lifecycle=deprecated&q=payment`. Only the known clean keys are appended and empties\n * are omitted (T-06-02-02) — no injection surface. Multi-value facets serialize as repeated\n * keys (D-07) via `append`.\n */\nexport function buildSearchParams(state: ApigeeFilterState): URLSearchParams {\n const sp = new URLSearchParams();\n for (const k of FACET_ORDER) {\n for (const v of state.selected[k]) {\n sp.append(QKEY[k], v);\n }\n }\n if (state.q.trim()) {\n sp.set('q', state.q.trim());\n }\n return sp;\n}\n\n/** True when any facet is selected or the search box holds a non-blank query. */\nexport function isActive(state: ApigeeFilterState): boolean {\n return (\n state.q.trim() !== '' || FACET_ORDER.some(k => state.selected[k].length > 0)\n );\n}\n\n/**\n * Single-select URL update for the count cards: the clicked value becomes the ONLY active facet\n * selection (every other facet is cleared), so each card click filters to exactly that value\n * instead of accumulating like the toolbar's multi-select toggle. Clicking the current sole\n * selection clears it. The `q` search query is always preserved.\n */\nexport function selectOnlyFacetParams(\n prev: URLSearchParams,\n key: FacetKey,\n value: string,\n): URLSearchParams {\n const current = prev.getAll(QKEY[key]);\n const isSoleSelection =\n current.length === 1 &&\n current[0] === value &&\n FACET_ORDER.every(k => k === key || prev.getAll(QKEY[k]).length === 0);\n const next = new URLSearchParams();\n const q = prev.get('q');\n if (q) {\n next.set('q', q);\n }\n if (!isSoleSelection) {\n next.append(QKEY[key], value);\n }\n return next;\n}\n"],"names":[],"mappings":";;AASA,MAAM,IAAA,GAAiC;AAAA,EACrC,SAAA,EAAW,WAAA;AAAA,EACX,GAAA,EAAK,KAAA;AAAA,EACL,KAAA,EAAO,OAAA;AAAA,EACP,UAAA,EAAY;AACd,CAAA;AAOO,SAAS,kBAAkB,EAAA,EAAwC;AACxE,EAAA,MAAM,WAAW,MAAA,CAAO,WAAA;AAAA,IACtB,WAAA,CAAY,GAAA,CAAI,CAAA,CAAA,KAAK,CAAC,CAAA,EAAG,EAAA,CAAG,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAC,CAAC;AAAA,GAC9C;AACA,EAAA,OAAO,EAAE,QAAA,EAAU,CAAA,EAAG,GAAG,GAAA,CAAI,GAAG,KAAK,EAAA,EAAG;AAC1C;AAsBO,SAAS,SAAS,KAAA,EAAmC;AAC1D,EAAA,OACE,KAAA,CAAM,CAAA,CAAE,IAAA,EAAK,KAAM,EAAA,IAAM,WAAA,CAAY,IAAA,CAAK,CAAA,CAAA,KAAK,KAAA,CAAM,QAAA,CAAS,CAAC,CAAA,CAAE,SAAS,CAAC,CAAA;AAE/E;AAQO,SAAS,qBAAA,CACd,IAAA,EACA,GAAA,EACA,KAAA,EACiB;AACjB,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAC,CAAA;AACrC,EAAA,MAAM,eAAA,GACJ,QAAQ,MAAA,KAAW,CAAA,IACnB,QAAQ,CAAC,CAAA,KAAM,SACf,WAAA,CAAY,KAAA,CAAM,OAAK,CAAA,KAAM,GAAA,IAAO,KAAK,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAA,CAAE,WAAW,CAAC,CAAA;AACvE,EAAA,MAAM,IAAA,GAAO,IAAI,eAAA,EAAgB;AACjC,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AACtB,EAAA,IAAI,CAAA,EAAG;AACL,IAAA,IAAA,CAAK,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EACjB;AACA,EAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAAG,KAAK,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,IAAA;AACT;;;;"}
@@ -1,12 +1,10 @@
1
- import { resolveCategoryIcon } from './categoryIcons.esm.js';
2
1
  import { humanise } from './grouping.esm.js';
3
- import { entityDepartment, entityCategoryFacet, entityOwnerLabel, entityOrg, entityLifecycle } from './apigeeFacetFilters.esm.js';
2
+ import { entityDepartment, entityOwnerLabel, entityOrg, entityLifecycle } from './apigeeFacetFilters.esm.js';
4
3
 
5
4
  const FACET_ORDER = [
6
5
  "lifecycle",
7
6
  "org",
8
7
  "owner",
9
- "category",
10
8
  "department"
11
9
  ];
12
10
  const LIFECYCLE_SEVERITY = ["production", "experimental", "deprecated", "unknown"];
@@ -14,14 +12,12 @@ const FACET_TRIGGER_LABEL = {
14
12
  lifecycle: "Lifecycle",
15
13
  org: "Org",
16
14
  owner: "Owner",
17
- category: "Category",
18
15
  department: "Department"
19
16
  };
20
17
  const EXTRACT = {
21
18
  lifecycle: entityLifecycle,
22
19
  org: entityOrg,
23
20
  owner: entityOwnerLabel,
24
- category: entityCategoryFacet,
25
21
  department: entityDepartment
26
22
  };
27
23
  function labelFor(key, value, orgLabels) {
@@ -30,8 +26,6 @@ function labelFor(key, value, orgLabels) {
30
26
  return value.charAt(0).toUpperCase() + value.slice(1);
31
27
  case "owner":
32
28
  return value;
33
- case "category":
34
- return value === "other" ? "Other" : humanise(value);
35
29
  case "org":
36
30
  return orgLabels?.[value] ?? humanise(value);
37
31
  default:
@@ -46,16 +40,9 @@ function sortValues(key, values) {
46
40
  };
47
41
  return [...values].sort((a, b) => rank(a) - rank(b));
48
42
  }
49
- if (key === "category") {
50
- return [...values].sort((a, b) => {
51
- if (a === "other") return 1;
52
- if (b === "other") return -1;
53
- return a.localeCompare(b);
54
- });
55
- }
56
43
  return [...values].sort((a, b) => a.localeCompare(b));
57
44
  }
58
- function deriveFacets(entities, overrides, orgLabels) {
45
+ function deriveFacets(entities, orgLabels) {
59
46
  return FACET_ORDER.map((key) => {
60
47
  const distinct = Array.from(
61
48
  new Set(entities.map(EXTRACT[key]).filter((v) => !!v))
@@ -64,24 +51,11 @@ function deriveFacets(entities, overrides, orgLabels) {
64
51
  value,
65
52
  label: labelFor(key, value, orgLabels)
66
53
  }));
67
- const icons = (
68
- // Guard on options.length: Object.fromEntries([]) is {}, not undefined.
69
- key === "category" && options.length > 0 ? Object.fromEntries(
70
- options.map((opt) => [
71
- opt.value,
72
- resolveCategoryIcon(
73
- opt.value === "other" ? void 0 : opt.value,
74
- overrides
75
- )
76
- ])
77
- ) : void 0
78
- );
79
54
  return {
80
55
  key,
81
56
  label: FACET_TRIGGER_LABEL[key],
82
57
  options,
83
- enabled: options.length > 0,
84
- ...icons !== void 0 ? { icons } : {}
58
+ enabled: options.length > 0
85
59
  };
86
60
  });
87
61
  }