@nospt/backstage-plugin-apigee 1.1.1

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 (74) hide show
  1. package/README.md +98 -0
  2. package/config.d.ts +30 -0
  3. package/dist/components/ApiCard.esm.js +105 -0
  4. package/dist/components/ApiCard.esm.js.map +1 -0
  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 +14 -0
  8. package/dist/components/ApiCardGrid.esm.js.map +1 -0
  9. package/dist/components/ApiDetailsDrawer.esm.js +709 -0
  10. package/dist/components/ApiDetailsDrawer.esm.js.map +1 -0
  11. package/dist/components/ApiDetailsDrawer.module.css.esm.js +8 -0
  12. package/dist/components/ApiDetailsDrawer.module.css.esm.js.map +1 -0
  13. package/dist/components/ApiTable.esm.js +184 -0
  14. package/dist/components/ApiTable.esm.js.map +1 -0
  15. package/dist/components/ApiTable.module.css.esm.js +8 -0
  16. package/dist/components/ApiTable.module.css.esm.js.map +1 -0
  17. package/dist/components/ApigeeApiCatalogPage.esm.js +245 -0
  18. package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -0
  19. package/dist/components/ApigeeHomePage.esm.js +190 -0
  20. package/dist/components/ApigeeHomePage.esm.js.map +1 -0
  21. package/dist/components/ApigeeHomePage.module.css.esm.js +8 -0
  22. package/dist/components/ApigeeHomePage.module.css.esm.js.map +1 -0
  23. package/dist/components/ApigeeNavContent.esm.js +28 -0
  24. package/dist/components/ApigeeNavContent.esm.js.map +1 -0
  25. package/dist/components/CountCards.esm.js +120 -0
  26. package/dist/components/CountCards.esm.js.map +1 -0
  27. package/dist/components/CountCards.module.css.esm.js +8 -0
  28. package/dist/components/CountCards.module.css.esm.js.map +1 -0
  29. package/dist/components/FilterToolbar.esm.js +131 -0
  30. package/dist/components/FilterToolbar.esm.js.map +1 -0
  31. package/dist/components/FilterToolbar.module.css.esm.js +8 -0
  32. package/dist/components/FilterToolbar.module.css.esm.js.map +1 -0
  33. package/dist/components/SearchBar.esm.js +20 -0
  34. package/dist/components/SearchBar.esm.js.map +1 -0
  35. package/dist/components/apiCardViewModel.esm.js +57 -0
  36. package/dist/components/apiCardViewModel.esm.js.map +1 -0
  37. package/dist/components/apiDetailViewModel.esm.js +98 -0
  38. package/dist/components/apiDetailViewModel.esm.js.map +1 -0
  39. package/dist/components/apiOperations.esm.js +196 -0
  40. package/dist/components/apiOperations.esm.js.map +1 -0
  41. package/dist/components/apigeeFacetFilters.esm.js +62 -0
  42. package/dist/components/apigeeFacetFilters.esm.js.map +1 -0
  43. package/dist/components/apigeeOrgFilter.esm.js +11 -0
  44. package/dist/components/apigeeOrgFilter.esm.js.map +1 -0
  45. package/dist/components/apigeeSearchParams.esm.js +55 -0
  46. package/dist/components/apigeeSearchParams.esm.js.map +1 -0
  47. package/dist/components/brandIcons.esm.js +36 -0
  48. package/dist/components/brandIcons.esm.js.map +1 -0
  49. package/dist/components/codeHighlight.esm.js +71 -0
  50. package/dist/components/codeHighlight.esm.js.map +1 -0
  51. package/dist/components/codeSnippet.esm.js +128 -0
  52. package/dist/components/codeSnippet.esm.js.map +1 -0
  53. package/dist/components/facetModel.esm.js +86 -0
  54. package/dist/components/facetModel.esm.js.map +1 -0
  55. package/dist/components/grouping.esm.js +6 -0
  56. package/dist/components/grouping.esm.js.map +1 -0
  57. package/dist/components/states.esm.js +65 -0
  58. package/dist/components/states.esm.js.map +1 -0
  59. package/dist/components/useApigeeFilters.esm.js +191 -0
  60. package/dist/components/useApigeeFilters.esm.js.map +1 -0
  61. package/dist/components/useDebouncedValue.esm.js +13 -0
  62. package/dist/components/useDebouncedValue.esm.js.map +1 -0
  63. package/dist/constants.esm.js +9 -0
  64. package/dist/constants.esm.js.map +1 -0
  65. package/dist/index.d.ts +203 -0
  66. package/dist/index.esm.js +2 -0
  67. package/dist/index.esm.js.map +1 -0
  68. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
  69. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
  70. package/dist/plugin.esm.js +59 -0
  71. package/dist/plugin.esm.js.map +1 -0
  72. package/dist/routes.esm.js +8 -0
  73. package/dist/routes.esm.js.map +1 -0
  74. package/package.json +95 -0
@@ -0,0 +1,120 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { Text } from '@backstage/ui';
3
+ import { RiStackLine, RiFileTextLine, RiBuildingLine, RiTeamLine, RiGlobalLine, RiPulseLine } from '@remixicon/react';
4
+ import styles from './CountCards.module.css.esm.js';
5
+
6
+ const FACET_ICON = {
7
+ lifecycle: RiPulseLine,
8
+ org: RiGlobalLine,
9
+ owner: RiTeamLine,
10
+ department: RiBuildingLine
11
+ };
12
+ function CardTitle({
13
+ icon: Icon,
14
+ children
15
+ }) {
16
+ return /* @__PURE__ */ jsxs("div", { className: styles.cardTitleRow, children: [
17
+ /* @__PURE__ */ jsx(Icon, { "aria-hidden": true, size: 20, className: styles.cardTitleIcon }),
18
+ /* @__PURE__ */ jsx(
19
+ Text,
20
+ {
21
+ as: "h3",
22
+ variant: "body-medium",
23
+ color: "secondary",
24
+ className: styles.cardTitle,
25
+ children
26
+ }
27
+ )
28
+ ] });
29
+ }
30
+ function CountCards({
31
+ total,
32
+ specCount,
33
+ counts,
34
+ selected,
35
+ onSelectFacet
36
+ }) {
37
+ return /* @__PURE__ */ jsxs("div", { className: styles.row, children: [
38
+ /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": "APIs", className: styles.card, children: [
39
+ /* @__PURE__ */ jsx(
40
+ Text,
41
+ {
42
+ as: "p",
43
+ variant: "title-large",
44
+ weight: "bold",
45
+ className: styles.total,
46
+ children: total
47
+ }
48
+ ),
49
+ /* @__PURE__ */ jsx(CardTitle, { icon: RiStackLine, children: "APIs" })
50
+ ] }),
51
+ /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": "With specs", className: styles.card, children: [
52
+ /* @__PURE__ */ jsx(
53
+ Text,
54
+ {
55
+ as: "p",
56
+ variant: "title-large",
57
+ weight: "bold",
58
+ className: styles.total,
59
+ children: specCount
60
+ }
61
+ ),
62
+ /* @__PURE__ */ jsx(CardTitle, { icon: RiFileTextLine, children: "With specs" })
63
+ ] }),
64
+ counts.map((count) => {
65
+ const heading = `By ${count.label.toLowerCase()}`;
66
+ return /* @__PURE__ */ jsxs(
67
+ "div",
68
+ {
69
+ role: "group",
70
+ "aria-label": heading,
71
+ className: styles.card,
72
+ children: [
73
+ /* @__PURE__ */ jsx(CardTitle, { icon: FACET_ICON[count.key], children: heading }),
74
+ count.buckets.length === 0 ? /* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", children: "No data" }) : /* @__PURE__ */ jsx("div", { className: styles.list, children: count.buckets.map((bucket) => {
75
+ const isSelected = selected[count.key].includes(bucket.value);
76
+ return /* @__PURE__ */ jsxs(
77
+ "button",
78
+ {
79
+ type: "button",
80
+ className: `${styles.bucket} ${isSelected ? styles.bucketSelected : ""}`,
81
+ "aria-pressed": isSelected,
82
+ "aria-label": `${bucket.label}, ${bucket.count} ${bucket.count === 1 ? "API" : "APIs"}`,
83
+ onClick: () => onSelectFacet(count.key, bucket.value),
84
+ children: [
85
+ /* @__PURE__ */ jsx(
86
+ Text,
87
+ {
88
+ as: "span",
89
+ variant: "body-small",
90
+ truncate: true,
91
+ title: bucket.label,
92
+ className: styles.bucketLabel,
93
+ children: bucket.label
94
+ }
95
+ ),
96
+ /* @__PURE__ */ jsx(
97
+ Text,
98
+ {
99
+ as: "span",
100
+ variant: "body-small",
101
+ weight: "bold",
102
+ className: styles.bucketCount,
103
+ children: bucket.count
104
+ }
105
+ )
106
+ ]
107
+ },
108
+ bucket.value
109
+ );
110
+ }) })
111
+ ]
112
+ },
113
+ count.key
114
+ );
115
+ })
116
+ ] });
117
+ }
118
+
119
+ export { CountCards };
120
+ //# sourceMappingURL=CountCards.esm.js.map
@@ -0,0 +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 {\n RiBuildingLine,\n RiFileTextLine,\n RiGlobalLine,\n RiPulseLine,\n RiStackLine,\n RiTeamLine,\n type RemixiconComponentType,\n} from '@remixicon/react';\nimport type { DerivedCount, FacetKey } from './facetModel';\nimport styles from './CountCards.module.css';\n\n/**\n * Facet β†’ summary-card icon. Mirrors the field icons on the API cards (owner = team,\n * department = building) so the cards and the tiles read consistently.\n */\nconst FACET_ICON: Record<FacetKey, RemixiconComponentType> = {\n lifecycle: RiPulseLine,\n org: RiGlobalLine,\n owner: RiTeamLine,\n department: RiBuildingLine,\n};\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/** Small uppercase card heading with a leading stat/facet icon. */\nfunction CardTitle({\n icon: Icon,\n children,\n}: {\n icon: RemixiconComponentType;\n children: string;\n}) {\n return (\n <div className={styles.cardTitleRow}>\n <Icon aria-hidden size={20} className={styles.cardTitleIcon} />\n <Text\n as=\"h3\"\n variant=\"body-medium\"\n color=\"secondary\"\n className={styles.cardTitle}\n >\n {children}\n </Text>\n </div>\n );\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 <CardTitle icon={RiStackLine}>APIs</CardTitle>\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 <CardTitle icon={RiFileTextLine}>With specs</CardTitle>\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 <CardTitle icon={FACET_ICON[count.key]}>{heading}</CardTitle>\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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"}
@@ -0,0 +1,8 @@
1
+ import styleInject from '../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
+
3
+ var css_248z = ".CountCards_row__12eaeb89b1 {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-4);\n margin-bottom: var(--bui-space-4);\n}\n\n.CountCards_card__12eaeb89b1 {\n flex: 1 1 200px;\n min-width: 200px;\n padding: var(--bui-space-2) var(--bui-space-4);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-3);\n background-color: var(--bui-bg-neutral-1);\n}\n\n.CountCards_total__12eaeb89b1 {\n line-height: 1.1;\n}\n\n.CountCards_cardTitleRow__12eaeb89b1 {\n display: flex;\n align-items: center;\n gap: var(--bui-space-1);\n margin-bottom: var(--bui-space-2);\n}\n\n.CountCards_cardTitleIcon__12eaeb89b1 {\n flex-shrink: 0;\n color: var(--bui-fg-secondary);\n}\n\n.CountCards_cardTitle__12eaeb89b1 {\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.CountCards_list__12eaeb89b1 {\n max-height: 80px;\n overflow-y: auto;\n}\n\n.CountCards_bucket__12eaeb89b1 {\n display: flex;\n gap: var(--bui-space-2);\n align-items: baseline;\n justify-content: space-between;\n width: 100%;\n padding: var(--bui-space-1) var(--bui-space-2);\n border: none;\n border-radius: var(--bui-radius-2);\n background: none;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.CountCards_bucket__12eaeb89b1:hover {\n background-color: var(--bui-bg-neutral-1-hover);\n}\n\n.CountCards_bucket__12eaeb89b1:focus-visible {\n outline: 2px solid var(--bui-ring);\n outline-offset: -2px;\n}\n\n.CountCards_bucketSelected__12eaeb89b1 {\n background-color: var(--bui-bg-neutral-1-pressed);\n}\n\n.CountCards_bucketLabel__12eaeb89b1 {\n flex: 1;\n min-width: 0;\n}\n\n.CountCards_bucketCount__12eaeb89b1 {\n font-variant-numeric: tabular-nums;\n}\n";
4
+ var styles = {"row":"CountCards_row__12eaeb89b1","card":"CountCards_card__12eaeb89b1","total":"CountCards_total__12eaeb89b1","cardTitleRow":"CountCards_cardTitleRow__12eaeb89b1","cardTitleIcon":"CountCards_cardTitleIcon__12eaeb89b1","cardTitle":"CountCards_cardTitle__12eaeb89b1","list":"CountCards_list__12eaeb89b1","bucket":"CountCards_bucket__12eaeb89b1","bucketSelected":"CountCards_bucketSelected__12eaeb89b1","bucketLabel":"CountCards_bucketLabel__12eaeb89b1","bucketCount":"CountCards_bucketCount__12eaeb89b1"};
5
+ styleInject(css_248z);
6
+
7
+ export { styles as default };
8
+ //# sourceMappingURL=CountCards.module.css.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CountCards.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,131 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useState, useCallback, useRef, useEffect } from 'react';
3
+ import { Button, Checkbox } from '@backstage/ui';
4
+ import { RiCloseLine, RiArrowDropDownLine } from '@remixicon/react';
5
+ import styles from './FilterToolbar.module.css.esm.js';
6
+
7
+ function FacetDropdown({
8
+ facet,
9
+ selected,
10
+ isOpen,
11
+ onOpenToggle,
12
+ onClose,
13
+ onToggle,
14
+ onClear
15
+ }) {
16
+ const ref = useRef(null);
17
+ const menuId = `facet-menu-${facet.key}`;
18
+ const count = selected.length;
19
+ useEffect(() => {
20
+ if (!isOpen) {
21
+ return void 0;
22
+ }
23
+ const onDocMouseDown = (event) => {
24
+ if (ref.current && !ref.current.contains(event.target)) {
25
+ onClose();
26
+ }
27
+ };
28
+ const onKeyDown = (event) => {
29
+ if (event.key === "Escape") {
30
+ onClose();
31
+ }
32
+ };
33
+ document.addEventListener("mousedown", onDocMouseDown);
34
+ document.addEventListener("keydown", onKeyDown);
35
+ return () => {
36
+ document.removeEventListener("mousedown", onDocMouseDown);
37
+ document.removeEventListener("keydown", onKeyDown);
38
+ };
39
+ }, [isOpen, onClose]);
40
+ return /* @__PURE__ */ jsxs("div", { ref, className: styles.facet, children: [
41
+ /* @__PURE__ */ jsxs(
42
+ Button,
43
+ {
44
+ variant: count > 0 ? "primary" : "secondary",
45
+ size: "small",
46
+ isDisabled: !facet.enabled,
47
+ iconEnd: /* @__PURE__ */ jsx(RiArrowDropDownLine, {}),
48
+ "aria-haspopup": "true",
49
+ "aria-expanded": isOpen,
50
+ "aria-controls": isOpen ? menuId : void 0,
51
+ "aria-label": count > 0 ? `${facet.label} filter, ${count} selected` : `${facet.label} filter`,
52
+ onPress: onOpenToggle,
53
+ children: [
54
+ facet.label,
55
+ count > 0 ? /* @__PURE__ */ jsx("span", { className: styles.countChip, children: count }) : null
56
+ ]
57
+ }
58
+ ),
59
+ isOpen ? /* @__PURE__ */ jsxs(
60
+ "div",
61
+ {
62
+ id: menuId,
63
+ className: styles.menu,
64
+ role: "group",
65
+ "aria-label": `${facet.label} options`,
66
+ children: [
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
+ )),
77
+ count > 0 ? /* @__PURE__ */ jsx("div", { className: styles.menuFooter, children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "small", onPress: onClear, children: "Clear" }) }) : null
78
+ ]
79
+ }
80
+ ) : null
81
+ ] });
82
+ }
83
+ function FilterToolbar({
84
+ facets,
85
+ selected,
86
+ onToggleFacet,
87
+ onClearFacet,
88
+ onClearAll,
89
+ active,
90
+ resultCount,
91
+ searchBar,
92
+ viewToggle
93
+ }) {
94
+ const [openKey, setOpenKey] = useState(null);
95
+ const closeMenus = useCallback(() => setOpenKey(null), []);
96
+ return /* @__PURE__ */ jsxs("div", { role: "search", "aria-label": "Filter and search APIs", children: [
97
+ /* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
98
+ facets.map((facet) => /* @__PURE__ */ jsx(
99
+ FacetDropdown,
100
+ {
101
+ facet,
102
+ selected: selected[facet.key],
103
+ isOpen: openKey === facet.key,
104
+ onOpenToggle: () => setOpenKey((prev) => prev === facet.key ? null : facet.key),
105
+ onClose: closeMenus,
106
+ onToggle: (value) => onToggleFacet(facet.key, value),
107
+ onClear: () => onClearFacet(facet.key)
108
+ },
109
+ facet.key
110
+ )),
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
+ ] })
125
+ ] }),
126
+ /* @__PURE__ */ jsx("span", { className: styles.srOnly, "aria-live": "polite", children: `${resultCount} APIs match your filters` })
127
+ ] });
128
+ }
129
+
130
+ export { FilterToolbar };
131
+ //# sourceMappingURL=FilterToolbar.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilterToolbar.esm.js","sources":["../../src/components/FilterToolbar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n} 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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"}
@@ -0,0 +1,8 @@
1
+ import styleInject from '../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
+
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
+ styleInject(css_248z);
6
+
7
+ export { styles as default };
8
+ //# sourceMappingURL=FilterToolbar.module.css.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilterToolbar.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,20 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { SearchField } from '@backstage/ui';
3
+ import { RiSearchLine } from '@remixicon/react';
4
+
5
+ function SearchBar({ value, onChange }) {
6
+ return /* @__PURE__ */ jsx(
7
+ SearchField,
8
+ {
9
+ "aria-label": "Search APIs",
10
+ placeholder: "Search APIs\u2026",
11
+ value,
12
+ onChange,
13
+ icon: /* @__PURE__ */ jsx(RiSearchLine, {}),
14
+ style: { flex: "1 1 auto", minWidth: 240 }
15
+ }
16
+ );
17
+ }
18
+
19
+ export { SearchBar };
20
+ //# sourceMappingURL=SearchBar.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchBar.esm.js","sources":["../../src/components/SearchBar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { SearchField } from '@backstage/ui';\nimport { RiSearchLine } from '@remixicon/react';\n\nexport interface SearchBarProps {\n /** Controlled input value β€” echoes keystrokes instantly (no debounce here). */\n value: string;\n /** Reports every change; the 200 ms trailing debounce lives in `useApigeeFilters` (06-04). */\n onChange: (next: string) => void;\n}\n\n/**\n * Stateless, controlled search field (UI-SPEC Β§Component Inventory #2). Holds NO state\n * and NO debounce β€” it purely echoes `value` and reports changes; the 200 ms trailing debounce\n * lives in the `useApigeeFilters` hook (06-04). Leading search icon plus a built-in Clear (βœ•)\n * button that @backstage/ui `SearchField` shows only when `value` is non-empty. The `value`\n * renders only as a controlled prop (React auto-escapes) β€” never raw HTML (T-06-03-01).\n */\nexport function SearchBar({ value, onChange }: SearchBarProps) {\n return (\n <SearchField\n aria-label=\"Search APIs\"\n placeholder=\"Search APIs…\"\n value={value}\n onChange={onChange}\n icon={<RiSearchLine />}\n style={{ flex: '1 1 auto', minWidth: 240 }}\n />\n );\n}\n"],"names":[],"mappings":";;;;AAkBO,SAAS,SAAA,CAAU,EAAE,KAAA,EAAO,QAAA,EAAS,EAAmB;AAC7D,EAAA,uBACE,GAAA;AAAA,IAAC,WAAA;AAAA,IAAA;AAAA,MACC,YAAA,EAAW,aAAA;AAAA,MACX,WAAA,EAAY,mBAAA;AAAA,MACZ,KAAA;AAAA,MACA,QAAA;AAAA,MACA,IAAA,sBAAO,YAAA,EAAA,EAAa,CAAA;AAAA,MACpB,KAAA,EAAO,EAAE,IAAA,EAAM,UAAA,EAAY,UAAU,GAAA;AAAI;AAAA,GAC3C;AAEJ;;;;"}
@@ -0,0 +1,57 @@
1
+ import { parseEntityRef } from '@backstage/catalog-model';
2
+ import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
3
+ import { ANNOTATION_VERSION, ANNOTATION_PROXY_NAME, ANNOTATION_HAS_SPEC, LABEL_DEPARTMENT, ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants.esm.js';
4
+
5
+ function toBadge(lifecycle) {
6
+ switch (lifecycle) {
7
+ case "production":
8
+ return { label: "Production", kind: "ok" };
9
+ case "deprecated":
10
+ return { label: "Deprecated", kind: "error" };
11
+ case "experimental":
12
+ return { label: "Experimental", kind: "warning" };
13
+ default:
14
+ return { label: "Unknown", kind: "warning" };
15
+ }
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
+ }
29
+ function toApiCardViewModel(entity) {
30
+ const annotations = entity.metadata.annotations ?? {};
31
+ const owner = entity.spec?.owner;
32
+ const description = entity.metadata.description?.trim() ? entity.metadata.description : void 0;
33
+ return {
34
+ name: entity.metadata.title ?? entity.metadata.name,
35
+ ownerLabel: owner ? humanizeEntityRef(
36
+ parseEntityRef(owner, {
37
+ defaultKind: "group",
38
+ defaultNamespace: "default"
39
+ }),
40
+ { defaultKind: "group" }
41
+ ) : "unknown",
42
+ department: entity.metadata.labels?.[LABEL_DEPARTMENT] || void 0,
43
+ visibilities: toVisibilities(annotations),
44
+ badge: toBadge(entity.spec?.lifecycle),
45
+ hasSpec: annotations[ANNOTATION_HAS_SPEC] === "true",
46
+ version: annotations[ANNOTATION_VERSION] || versionFromProxyName(annotations[ANNOTATION_PROXY_NAME]) || void 0,
47
+ description,
48
+ targetRef: {
49
+ kind: "API",
50
+ namespace: entity.metadata.namespace ?? "default",
51
+ name: entity.metadata.name
52
+ }
53
+ };
54
+ }
55
+
56
+ export { toApiCardViewModel };
57
+ //# sourceMappingURL=apiCardViewModel.esm.js.map
@@ -0,0 +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_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 β€” undefined when absent (ApiCard renders 'unknown') */\n department?: string;\n /** Exposure derived from cloud.google.com/orgs β€” 'private', 'public', or both; [] when none. */\n visibilities: ApiVisibility[];\n badge: { label: string; kind: LifecycleBadgeKind };\n /** annotations['cloud.google.com/has-spec'] === 'true' */\n hasSpec: boolean;\n /** annotations['cloud.google.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] ??\n annotations[ANNOTATION_APIGEE_ORG] ??\n '';\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(\n proxyName: string | undefined,\n): 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, {\n defaultKind: 'group',\n defaultNamespace: 'default',\n }),\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,98 @@
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 = /^cloud\.google\.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(
12
+ component?.metadata.annotations ?? {}
13
+ )) {
14
+ const match = ENV_REVISION_KEY.exec(key);
15
+ if (!match) {
16
+ continue;
17
+ }
18
+ const [, orgSlug, envSlug] = match;
19
+ const list = byOrg.get(orgSlug) ?? [];
20
+ list.push({ name: envSlug, revision });
21
+ byOrg.set(orgSlug, list);
22
+ }
23
+ return byOrg;
24
+ }
25
+ function toRuntimeOrgs(api, component, orgLabels) {
26
+ const annotations = api.metadata.annotations ?? {};
27
+ const raw = annotations[ANNOTATION_APIGEE_ORGS] ?? annotations[ANNOTATION_APIGEE_ORG] ?? "";
28
+ const orgValues = raw.split(",").map((value) => value.trim()).filter(Boolean);
29
+ const envMap = envsByOrgSlug(component);
30
+ return orgValues.map((value) => ({
31
+ value,
32
+ label: orgLabels?.[value] ?? humanise(value),
33
+ environments: (envMap.get(slugify(value)) ?? []).sort(
34
+ (a, b) => a.name.localeCompare(b.name)
35
+ )
36
+ }));
37
+ }
38
+ function toDeploymentMatrix(runtimeOrgs) {
39
+ const environments = Array.from(
40
+ new Set(runtimeOrgs.flatMap((org) => org.environments.map((env) => env.name)))
41
+ ).sort((a, b) => a.localeCompare(b));
42
+ const rows = runtimeOrgs.map((org) => ({
43
+ value: org.value,
44
+ label: org.label,
45
+ revisions: Object.fromEntries(
46
+ org.environments.map((env) => [env.name, env.revision])
47
+ )
48
+ }));
49
+ return { environments, rows };
50
+ }
51
+ function categoriseLinks(entity) {
52
+ const httpLinks = (entity.metadata.links ?? []).filter(
53
+ (l) => /^https?:\/\//i.test(l.url)
54
+ );
55
+ const apiHubLink = httpLinks.find(
56
+ (l) => l.title === "Apigee API Hub" || /\/apigee\/api-hub\//i.test(l.url)
57
+ );
58
+ const sourceRepoLink = httpLinks.find(
59
+ (l) => l !== apiHubLink && (l.title === "Source repository" || /github\.com/i.test(l.url))
60
+ ) ?? httpLinks.find((l) => l !== apiHubLink);
61
+ return {
62
+ sourceRepo: sourceRepoLink ? {
63
+ url: sourceRepoLink.url,
64
+ title: sourceRepoLink.title || "Source repository",
65
+ icon: "github"
66
+ } : void 0,
67
+ apiHub: apiHubLink ? {
68
+ url: apiHubLink.url,
69
+ title: apiHubLink.title || "Apigee API Hub",
70
+ icon: "apigee"
71
+ } : void 0
72
+ };
73
+ }
74
+ function specFileName(baseName, definition) {
75
+ const ext = definition.trim().startsWith("{") ? "json" : "yaml";
76
+ return `${baseName}.${ext}`;
77
+ }
78
+ function toApiDetailViewModel(api, component, orgLabels) {
79
+ const base = toApiCardViewModel(api);
80
+ const spec = api.spec ?? {};
81
+ const runtimeOrgs = toRuntimeOrgs(api, component, orgLabels);
82
+ const { sourceRepo, apiHub } = categoriseLinks(api);
83
+ return {
84
+ ...base,
85
+ kind: api.kind,
86
+ type: spec.type || void 0,
87
+ ownerRef: spec.owner || void 0,
88
+ tags: api.metadata.tags ?? [],
89
+ definition: typeof spec.definition === "string" ? spec.definition : "",
90
+ sourceRepo,
91
+ apiHub,
92
+ runtimeOrgs,
93
+ deployments: toDeploymentMatrix(runtimeOrgs)
94
+ };
95
+ }
96
+
97
+ export { specFileName, toApiDetailViewModel, toDeploymentMatrix, toRuntimeOrgs };
98
+ //# 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 `cloud.google.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/** One org's row in the pivoted deployments table: its label plus the revision deployed in each\n * environment column (a missing entry means the org is not deployed in that environment). */\nexport interface ApiDeploymentRow {\n value: string;\n label: string;\n /** Environment name β†’ deployed revision. */\n revisions: Record<string, string>;\n}\n\n/** Pivoted deployments: environment columns (the sorted union across orgs) Γ— one row per org. */\nexport interface ApiDeploymentMatrix {\n environments: string[];\n rows: ApiDeploymentRow[];\n}\n\n/** A source-code repository link, read from `metadata.links`. */\nexport interface ApiSourceRepo {\n url: string;\n title: string;\n icon: 'github' | 'apigee';\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 /** Source-repository link (GitHub) from metadata.links. */\n sourceRepo?: ApiSourceRepo;\n /** Apigee API Hub deep link from metadata.links. */\n apiHub?: ApiSourceRepo;\n /** Per-org runtime (environments + revisions). */\n runtimeOrgs: ApiRuntimeOrg[];\n /** Pivoted deployments (environment columns Γ— org rows) for the Deployments card. */\n deployments: ApiDeploymentMatrix;\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 * `cloud.google.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/** `cloud.google.com/<orgSlug>.env.<envSlug>.revision` β€” slugs never contain '.', so this is unambiguous. */\nconst ENV_REVISION_KEY =\n /^cloud\\.google\\.com\\/([a-z0-9-]+)\\.env\\.([a-z0-9-]+)\\.revision$/;\n\n/** Group a Component's per-environment revision annotations by org slug. */\nfunction envsByOrgSlug(\n component: Entity | undefined,\n): Map<string, ApiRuntimeEnv[]> {\n const byOrg = new Map<string, ApiRuntimeEnv[]>();\n for (const [key, revision] of Object.entries(\n component?.metadata.annotations ?? {},\n )) {\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 * `cloud.google.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] ??\n annotations[ANNOTATION_APIGEE_ORG] ??\n '';\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/**\n * Pivot the per-org runtime into an environment-columns Γ— org-rows matrix for the Deployments\n * table. Columns are the sorted union of every org's environment names; each row carries the org\n * label and a nameβ†’revision lookup, so a cell with no entry renders as \"not deployed\".\n */\nexport function toDeploymentMatrix(\n runtimeOrgs: ApiRuntimeOrg[],\n): ApiDeploymentMatrix {\n const environments = Array.from(\n new Set(runtimeOrgs.flatMap(org => org.environments.map(env => env.name))),\n ).sort((a, b) => a.localeCompare(b));\n const rows = runtimeOrgs.map(org => ({\n value: org.value,\n label: org.label,\n revisions: Object.fromEntries(\n org.environments.map(env => [env.name, env.revision]),\n ),\n }));\n return { environments, rows };\n}\n\n/**\n * Split metadata.links into the two the drawer surfaces separately: the source repository (GitHub)\n * and the Apigee API Hub deep link. Matched by the backend's stable titles with a URL-shape\n * fallback; the source repo additionally falls back to the first non-hub http link (back-compat).\n */\nfunction categoriseLinks(entity: Entity): {\n sourceRepo?: ApiSourceRepo;\n apiHub?: ApiSourceRepo;\n} {\n const httpLinks = (entity.metadata.links ?? []).filter(l =>\n /^https?:\\/\\//i.test(l.url),\n );\n const apiHubLink = httpLinks.find(\n l => l.title === 'Apigee API Hub' || /\\/apigee\\/api-hub\\//i.test(l.url),\n );\n const sourceRepoLink =\n httpLinks.find(\n l =>\n l !== apiHubLink &&\n (l.title === 'Source repository' || /github\\.com/i.test(l.url)),\n ) ?? httpLinks.find(l => l !== apiHubLink);\n return {\n sourceRepo: sourceRepoLink\n ? {\n url: sourceRepoLink.url,\n title: sourceRepoLink.title || 'Source repository',\n icon: 'github',\n }\n : undefined,\n apiHub: apiHubLink\n ? {\n url: apiHubLink.url,\n title: apiHubLink.title || 'Apigee API Hub',\n icon: 'apigee',\n }\n : undefined,\n };\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 const runtimeOrgs = toRuntimeOrgs(api, component, orgLabels);\n const { sourceRepo, apiHub } = categoriseLinks(api);\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,\n apiHub,\n runtimeOrgs,\n deployments: toDeploymentMatrix(runtimeOrgs),\n };\n}\n"],"names":[],"mappings":"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"}