@nospt/backstage-plugin-apigee 1.1.0-rc2

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 (44) hide show
  1. package/README.md +19 -0
  2. package/config.d.ts +25 -0
  3. package/dist/components/ApiCard.esm.js +146 -0
  4. package/dist/components/ApiCard.esm.js.map +1 -0
  5. package/dist/components/ApiCardGrid.esm.js +56 -0
  6. package/dist/components/ApiCardGrid.esm.js.map +1 -0
  7. package/dist/components/ApigeeApiCatalogPage.esm.js +153 -0
  8. package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -0
  9. package/dist/components/ApigeeNavContent.esm.js +35 -0
  10. package/dist/components/ApigeeNavContent.esm.js.map +1 -0
  11. package/dist/components/FilterToolbar.esm.js +197 -0
  12. package/dist/components/FilterToolbar.esm.js.map +1 -0
  13. package/dist/components/SearchBar.esm.js +48 -0
  14. package/dist/components/SearchBar.esm.js.map +1 -0
  15. package/dist/components/apiCardViewModel.esm.js +41 -0
  16. package/dist/components/apiCardViewModel.esm.js.map +1 -0
  17. package/dist/components/apigeeFacetFilters.esm.js +62 -0
  18. package/dist/components/apigeeFacetFilters.esm.js.map +1 -0
  19. package/dist/components/apigeeOrgFilter.esm.js +11 -0
  20. package/dist/components/apigeeOrgFilter.esm.js.map +1 -0
  21. package/dist/components/apigeeSearchParams.esm.js +21 -0
  22. package/dist/components/apigeeSearchParams.esm.js.map +1 -0
  23. package/dist/components/categoryIcons.esm.js +63 -0
  24. package/dist/components/categoryIcons.esm.js.map +1 -0
  25. package/dist/components/facetModel.esm.js +87 -0
  26. package/dist/components/facetModel.esm.js.map +1 -0
  27. package/dist/components/grouping.esm.js +31 -0
  28. package/dist/components/grouping.esm.js.map +1 -0
  29. package/dist/components/states.esm.js +95 -0
  30. package/dist/components/states.esm.js.map +1 -0
  31. package/dist/components/useApigeeFilters.esm.js +88 -0
  32. package/dist/components/useApigeeFilters.esm.js.map +1 -0
  33. package/dist/components/useDebouncedValue.esm.js +13 -0
  34. package/dist/components/useDebouncedValue.esm.js.map +1 -0
  35. package/dist/constants.esm.js +9 -0
  36. package/dist/constants.esm.js.map +1 -0
  37. package/dist/index.d.ts +101 -0
  38. package/dist/index.esm.js +2 -0
  39. package/dist/index.esm.js.map +1 -0
  40. package/dist/plugin.esm.js +39 -0
  41. package/dist/plugin.esm.js.map +1 -0
  42. package/dist/routes.esm.js +6 -0
  43. package/dist/routes.esm.js.map +1 -0
  44. package/package.json +92 -0
@@ -0,0 +1,197 @@
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { useState } from 'react';
3
+ import { Box, Button, makeStyles, Chip, Menu, FormGroup, FormControlLabel, Checkbox } from '@material-ui/core';
4
+ import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
5
+ import Clear from '@material-ui/icons/Clear';
6
+
7
+ const useStyles = makeStyles((theme) => ({
8
+ toolbar: {
9
+ display: "flex",
10
+ flexWrap: "wrap",
11
+ alignItems: "center",
12
+ gap: theme.spacing(1),
13
+ marginBottom: theme.spacing(2)
14
+ },
15
+ control: {
16
+ "&:focus-visible": {
17
+ outline: `2px solid ${theme.palette.primary.main}`,
18
+ outlineOffset: 2
19
+ }
20
+ },
21
+ clearAll: {
22
+ marginLeft: "auto"
23
+ },
24
+ countChip: {
25
+ marginLeft: theme.spacing(0.5)
26
+ },
27
+ formGroup: {
28
+ padding: theme.spacing(0, 2)
29
+ },
30
+ menuFooter: {
31
+ display: "flex",
32
+ justifyContent: "flex-end",
33
+ padding: theme.spacing(0.5, 1)
34
+ },
35
+ optionLabel: {
36
+ display: "flex",
37
+ alignItems: "center",
38
+ gap: theme.spacing(0.5)
39
+ },
40
+ optionIcon: {
41
+ fontSize: 16
42
+ },
43
+ srOnly: {
44
+ position: "absolute",
45
+ width: 1,
46
+ height: 1,
47
+ padding: 0,
48
+ margin: -1,
49
+ overflow: "hidden",
50
+ clip: "rect(0 0 0 0)",
51
+ whiteSpace: "nowrap",
52
+ border: 0
53
+ }
54
+ }));
55
+ function FacetDropdown({
56
+ facet,
57
+ selected,
58
+ onToggle,
59
+ onClear
60
+ }) {
61
+ const classes = useStyles();
62
+ const [anchorEl, setAnchorEl] = useState(null);
63
+ const open = Boolean(anchorEl);
64
+ const menuId = `facet-menu-${facet.key}`;
65
+ const count = selected.length;
66
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
67
+ /* @__PURE__ */ jsxs(
68
+ Button,
69
+ {
70
+ className: classes.control,
71
+ variant: "outlined",
72
+ size: "small",
73
+ disabled: !facet.enabled,
74
+ endIcon: /* @__PURE__ */ jsx(ArrowDropDown, {}),
75
+ color: count > 0 ? "primary" : "default",
76
+ "aria-haspopup": "true",
77
+ "aria-expanded": open,
78
+ "aria-controls": open ? menuId : void 0,
79
+ "aria-label": count > 0 ? `${facet.label} filter, ${count} selected` : `${facet.label} filter`,
80
+ onClick: (event) => setAnchorEl(event.currentTarget),
81
+ children: [
82
+ facet.label,
83
+ count > 0 ? /* @__PURE__ */ jsx(
84
+ Chip,
85
+ {
86
+ className: classes.countChip,
87
+ size: "small",
88
+ color: "primary",
89
+ label: count
90
+ }
91
+ ) : null
92
+ ]
93
+ }
94
+ ),
95
+ /* @__PURE__ */ jsxs(
96
+ Menu,
97
+ {
98
+ id: menuId,
99
+ anchorEl,
100
+ open,
101
+ onClose: () => setAnchorEl(null),
102
+ getContentAnchorEl: null,
103
+ anchorOrigin: { vertical: "bottom", horizontal: "left" },
104
+ transformOrigin: { vertical: "top", horizontal: "left" },
105
+ children: [
106
+ /* @__PURE__ */ jsx(
107
+ FormGroup,
108
+ {
109
+ className: classes.formGroup,
110
+ role: "group",
111
+ "aria-label": `${facet.label} options`,
112
+ children: facet.options.map((option) => {
113
+ const OptionIcon = facet.icons?.[option.value];
114
+ return /* @__PURE__ */ jsx(
115
+ FormControlLabel,
116
+ {
117
+ control: /* @__PURE__ */ jsx(
118
+ Checkbox,
119
+ {
120
+ color: "primary",
121
+ checked: selected.includes(option.value),
122
+ onChange: () => onToggle(option.value)
123
+ }
124
+ ),
125
+ label: OptionIcon ? /* @__PURE__ */ jsxs("span", { className: classes.optionLabel, children: [
126
+ /* @__PURE__ */ jsx(
127
+ OptionIcon,
128
+ {
129
+ className: classes.optionIcon,
130
+ "aria-hidden": true
131
+ }
132
+ ),
133
+ option.label
134
+ ] }) : option.label
135
+ },
136
+ option.value
137
+ );
138
+ })
139
+ }
140
+ ),
141
+ count > 0 ? /* @__PURE__ */ jsx("div", { className: classes.menuFooter, children: /* @__PURE__ */ jsx(
142
+ Button,
143
+ {
144
+ className: classes.control,
145
+ variant: "text",
146
+ size: "small",
147
+ onClick: onClear,
148
+ children: "Clear"
149
+ }
150
+ ) }) : null
151
+ ]
152
+ }
153
+ )
154
+ ] });
155
+ }
156
+ function FilterToolbar({
157
+ facets,
158
+ selected,
159
+ onToggleFacet,
160
+ onClearFacet,
161
+ onClearAll,
162
+ active,
163
+ resultCount,
164
+ searchBar
165
+ }) {
166
+ const classes = useStyles();
167
+ return /* @__PURE__ */ jsxs("div", { role: "search", "aria-label": "Filter and search APIs", children: [
168
+ /* @__PURE__ */ jsxs(Box, { className: classes.toolbar, children: [
169
+ facets.map((facet) => /* @__PURE__ */ jsx(
170
+ FacetDropdown,
171
+ {
172
+ facet,
173
+ selected: selected[facet.key],
174
+ onToggle: (value) => onToggleFacet(facet.key, value),
175
+ onClear: () => onClearFacet(facet.key)
176
+ },
177
+ facet.key
178
+ )),
179
+ searchBar,
180
+ active ? /* @__PURE__ */ jsx(
181
+ Button,
182
+ {
183
+ className: `${classes.control} ${classes.clearAll}`,
184
+ variant: "text",
185
+ size: "small",
186
+ startIcon: /* @__PURE__ */ jsx(Clear, {}),
187
+ onClick: onClearAll,
188
+ children: "Clear all"
189
+ }
190
+ ) : null
191
+ ] }),
192
+ /* @__PURE__ */ jsx("span", { className: classes.srOnly, "aria-live": "polite", children: `${resultCount} APIs match your filters` })
193
+ ] });
194
+ }
195
+
196
+ export { FilterToolbar };
197
+ //# 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\r\nimport { useState, type ReactNode } from 'react';\r\nimport {\r\n Box,\r\n Button,\r\n Checkbox,\r\n Chip,\r\n FormControlLabel,\r\n FormGroup,\r\n Menu,\r\n makeStyles,\r\n} from '@material-ui/core';\r\nimport ArrowDropDown from '@material-ui/icons/ArrowDropDown';\r\nimport Clear from '@material-ui/icons/Clear';\r\nimport type { DerivedFacet, FacetKey } from './facetModel';\r\n\r\nexport interface FilterToolbarProps {\r\n /** Derived facets from `deriveFacets()`; ALL render (revised D-05) — one with no values is disabled. */\r\n facets: DerivedFacet[];\r\n /** Current selections per facet (hydrated from the URL state in 06-04). */\r\n selected: Record<FacetKey, string[]>;\r\n onToggleFacet: (key: FacetKey, value: string) => void;\r\n /** Per-facet menu-footer \"Clear\" (clears just that facet). */\r\n onClearFacet: (key: FacetKey) => void;\r\n /** Toolbar \"Clear all\" (resets every facet + search). */\r\n onClearAll: () => void;\r\n /** `isActive(state)` — gates the \"Clear all\" affordance. */\r\n active: boolean;\r\n /** Filtered result count for the polite live region (UI-07). */\r\n resultCount: number;\r\n /** `SearchBar` mounted inline in the toolbar row. */\r\n searchBar: ReactNode;\r\n}\r\n\r\nconst useStyles = makeStyles(theme => ({\r\n toolbar: {\r\n display: 'flex',\r\n flexWrap: 'wrap',\r\n alignItems: 'center',\r\n gap: theme.spacing(1),\r\n marginBottom: theme.spacing(2),\r\n },\r\n control: {\r\n '&:focus-visible': {\r\n outline: `2px solid ${theme.palette.primary.main}`,\r\n outlineOffset: 2,\r\n },\r\n },\r\n clearAll: {\r\n marginLeft: 'auto',\r\n },\r\n countChip: {\r\n marginLeft: theme.spacing(0.5),\r\n },\r\n formGroup: {\r\n padding: theme.spacing(0, 2),\r\n },\r\n menuFooter: {\r\n display: 'flex',\r\n justifyContent: 'flex-end',\r\n padding: theme.spacing(0.5, 1),\r\n },\r\n optionLabel: {\r\n display: 'flex',\r\n alignItems: 'center',\r\n gap: theme.spacing(0.5),\r\n },\r\n optionIcon: {\r\n fontSize: 16,\r\n },\r\n srOnly: {\r\n position: 'absolute',\r\n width: 1,\r\n height: 1,\r\n padding: 0,\r\n margin: -1,\r\n overflow: 'hidden',\r\n clip: 'rect(0 0 0 0)',\r\n whiteSpace: 'nowrap',\r\n border: 0,\r\n },\r\n}));\r\n\r\n/**\r\n * A single multi-select facet as an outlined menu-button (verified MUI v4 pattern —\r\n * 06-RESEARCH Pattern 7). The `Menu` sets `getContentAnchorEl={null}` so it bottom-anchors,\r\n * and its options are `FormControlLabel`+`Checkbox` inside a `FormGroup` (not clickable menu\r\n * rows) so toggling a checkbox keeps the menu OPEN for multi-select. All dynamic text (`facet.label`,\r\n * `option.label`) renders as React children only — never raw HTML (T-06-03-01).\r\n */\r\nfunction FacetDropdown({\r\n facet,\r\n selected,\r\n onToggle,\r\n onClear,\r\n}: {\r\n facet: DerivedFacet;\r\n selected: string[];\r\n onToggle: (value: string) => void;\r\n onClear: () => void;\r\n}) {\r\n const classes = useStyles();\r\n const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);\r\n const open = Boolean(anchorEl);\r\n const menuId = `facet-menu-${facet.key}`;\r\n const count = selected.length;\r\n\r\n return (\r\n <>\r\n <Button\r\n className={classes.control}\r\n variant=\"outlined\"\r\n size=\"small\"\r\n disabled={!facet.enabled}\r\n endIcon={<ArrowDropDown />}\r\n color={count > 0 ? 'primary' : 'default'}\r\n aria-haspopup=\"true\"\r\n aria-expanded={open}\r\n aria-controls={open ? menuId : undefined}\r\n aria-label={\r\n count > 0\r\n ? `${facet.label} filter, ${count} selected`\r\n : `${facet.label} filter`\r\n }\r\n onClick={event => setAnchorEl(event.currentTarget)}\r\n >\r\n {facet.label}\r\n {count > 0 ? (\r\n <Chip\r\n className={classes.countChip}\r\n size=\"small\"\r\n color=\"primary\"\r\n label={count}\r\n />\r\n ) : null}\r\n </Button>\r\n <Menu\r\n id={menuId}\r\n anchorEl={anchorEl}\r\n open={open}\r\n onClose={() => setAnchorEl(null)}\r\n getContentAnchorEl={null}\r\n anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}\r\n transformOrigin={{ vertical: 'top', horizontal: 'left' }}\r\n >\r\n <FormGroup\r\n className={classes.formGroup}\r\n role=\"group\"\r\n aria-label={`${facet.label} options`}\r\n >\r\n {facet.options.map(option => {\r\n const OptionIcon = facet.icons?.[option.value];\r\n return (\r\n <FormControlLabel\r\n key={option.value}\r\n control={\r\n <Checkbox\r\n color=\"primary\"\r\n checked={selected.includes(option.value)}\r\n onChange={() => onToggle(option.value)}\r\n />\r\n }\r\n label={\r\n OptionIcon ? (\r\n <span className={classes.optionLabel}>\r\n <OptionIcon\r\n className={classes.optionIcon}\r\n aria-hidden\r\n />\r\n {option.label}\r\n </span>\r\n ) : (\r\n option.label\r\n )\r\n }\r\n />\r\n );\r\n })}\r\n </FormGroup>\r\n {count > 0 ? (\r\n <div className={classes.menuFooter}>\r\n <Button\r\n className={classes.control}\r\n variant=\"text\"\r\n size=\"small\"\r\n onClick={onClear}\r\n >\r\n Clear\r\n </Button>\r\n </div>\r\n ) : null}\r\n </Menu>\r\n </>\r\n );\r\n}\r\n\r\n/**\r\n * Presentational Phase 6 toolbar (D-01 top toolbar, D-02 multi-select dropdowns). Renders EVERY\r\n * facet in `FACET_ORDER` (revised D-05 — a facet with no values renders disabled), the inline\r\n * `searchBar` slot, a conditional right-aligned \"Clear all\", and a polite `aria-live` result-count\r\n * region. Purely props-driven — no data fetching and no URL/provider access (that wiring is 06-04).\r\n * (UI-06, UI-07)\r\n */\r\nexport function FilterToolbar({\r\n facets,\r\n selected,\r\n onToggleFacet,\r\n onClearFacet,\r\n onClearAll,\r\n active,\r\n resultCount,\r\n searchBar,\r\n}: FilterToolbarProps) {\r\n const classes = useStyles();\r\n\r\n return (\r\n <div role=\"search\" aria-label=\"Filter and search APIs\">\r\n <Box className={classes.toolbar}>\r\n {facets.map(facet => (\r\n <FacetDropdown\r\n key={facet.key}\r\n facet={facet}\r\n selected={selected[facet.key]}\r\n onToggle={value => onToggleFacet(facet.key, value)}\r\n onClear={() => onClearFacet(facet.key)}\r\n />\r\n ))}\r\n {searchBar}\r\n {active ? (\r\n <Button\r\n className={`${classes.control} ${classes.clearAll}`}\r\n variant=\"text\"\r\n size=\"small\"\r\n startIcon={<Clear />}\r\n onClick={onClearAll}\r\n >\r\n Clear all\r\n </Button>\r\n ) : null}\r\n </Box>\r\n <span className={classes.srOnly} aria-live=\"polite\">\r\n {`${resultCount} APIs match your filters`}\r\n </span>\r\n </div>\r\n );\r\n}\r\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { TextField, InputAdornment, IconButton, makeStyles } from '@material-ui/core';
3
+ import Clear from '@material-ui/icons/Clear';
4
+ import Search from '@material-ui/icons/Search';
5
+
6
+ const useStyles = makeStyles((theme) => ({
7
+ field: {
8
+ minWidth: 240,
9
+ flex: "1 1 auto",
10
+ "& input:focus-visible": {
11
+ outline: `2px solid ${theme.palette.primary.main}`,
12
+ outlineOffset: 2
13
+ }
14
+ },
15
+ searchIcon: {
16
+ color: theme.palette.text.secondary
17
+ }
18
+ }));
19
+ function SearchBar({ value, onChange }) {
20
+ const classes = useStyles();
21
+ return /* @__PURE__ */ jsx(
22
+ TextField,
23
+ {
24
+ className: classes.field,
25
+ variant: "outlined",
26
+ size: "small",
27
+ placeholder: "Search APIs\u2026",
28
+ value,
29
+ onChange: (event) => onChange(event.target.value),
30
+ inputProps: { "aria-label": "Search APIs" },
31
+ InputProps: {
32
+ startAdornment: /* @__PURE__ */ jsx(InputAdornment, { position: "start", children: /* @__PURE__ */ jsx(Search, { className: classes.searchIcon }) }),
33
+ endAdornment: value ? /* @__PURE__ */ jsx(InputAdornment, { position: "end", children: /* @__PURE__ */ jsx(
34
+ IconButton,
35
+ {
36
+ size: "small",
37
+ "aria-label": "Clear search",
38
+ onClick: () => onChange(""),
39
+ children: /* @__PURE__ */ jsx(Clear, {})
40
+ }
41
+ ) }) : void 0
42
+ }
43
+ }
44
+ );
45
+ }
46
+
47
+ export { SearchBar };
48
+ //# 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\r\nimport {\r\n IconButton,\r\n InputAdornment,\r\n TextField,\r\n makeStyles,\r\n} from '@material-ui/core';\r\nimport Clear from '@material-ui/icons/Clear';\r\nimport Search from '@material-ui/icons/Search';\r\n\r\nexport interface SearchBarProps {\r\n /** Controlled input value — echoes keystrokes instantly (no debounce here). */\r\n value: string;\r\n /** Reports every change; the 200 ms trailing debounce lives in `useApigeeFilters` (06-04). */\r\n onChange: (next: string) => void;\r\n}\r\n\r\nconst useStyles = makeStyles(theme => ({\r\n field: {\r\n minWidth: 240,\r\n flex: '1 1 auto',\r\n '& input:focus-visible': {\r\n outline: `2px solid ${theme.palette.primary.main}`,\r\n outlineOffset: 2,\r\n },\r\n },\r\n searchIcon: {\r\n color: theme.palette.text.secondary,\r\n },\r\n}));\r\n\r\n/**\r\n * Stateless, controlled outlined search field (UI-SPEC §Component Inventory #2). Holds NO state\r\n * and NO debounce — it purely echoes `value` and reports changes; the 200 ms trailing debounce\r\n * lives in the `useApigeeFilters` hook (06-04). Leading search icon, and a Clear\r\n * (✕) button shown only when `value` is non-empty. No spinner (UI-SPEC Assumption 9). The `value`\r\n * renders only as a controlled prop (React auto-escapes) — never raw HTML (T-06-03-01).\r\n */\r\nexport function SearchBar({ value, onChange }: SearchBarProps) {\r\n const classes = useStyles();\r\n\r\n return (\r\n <TextField\r\n className={classes.field}\r\n variant=\"outlined\"\r\n size=\"small\"\r\n placeholder=\"Search APIs…\"\r\n value={value}\r\n onChange={event => onChange(event.target.value)}\r\n inputProps={{ 'aria-label': 'Search APIs' }}\r\n InputProps={{\r\n startAdornment: (\r\n <InputAdornment position=\"start\">\r\n <Search className={classes.searchIcon} />\r\n </InputAdornment>\r\n ),\r\n endAdornment: value ? (\r\n <InputAdornment position=\"end\">\r\n <IconButton\r\n size=\"small\"\r\n aria-label=\"Clear search\"\r\n onClick={() => onChange('')}\r\n >\r\n <Clear />\r\n </IconButton>\r\n </InputAdornment>\r\n ) : undefined,\r\n }}\r\n />\r\n );\r\n}\r\n"],"names":[],"mappings":";;;;;AAiBA,MAAM,SAAA,GAAY,WAAW,CAAA,KAAA,MAAU;AAAA,EACrC,KAAA,EAAO;AAAA,IACL,QAAA,EAAU,GAAA;AAAA,IACV,IAAA,EAAM,UAAA;AAAA,IACN,uBAAA,EAAyB;AAAA,MACvB,OAAA,EAAS,CAAA,UAAA,EAAa,KAAA,CAAM,OAAA,CAAQ,QAAQ,IAAI,CAAA,CAAA;AAAA,MAChD,aAAA,EAAe;AAAA;AACjB,GACF;AAAA,EACA,UAAA,EAAY;AAAA,IACV,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK;AAAA;AAE9B,CAAA,CAAE,CAAA;AASK,SAAS,SAAA,CAAU,EAAE,KAAA,EAAO,QAAA,EAAS,EAAmB;AAC7D,EAAA,MAAM,UAAU,SAAA,EAAU;AAE1B,EAAA,uBACE,GAAA;AAAA,IAAC,SAAA;AAAA,IAAA;AAAA,MACC,WAAW,OAAA,CAAQ,KAAA;AAAA,MACnB,OAAA,EAAQ,UAAA;AAAA,MACR,IAAA,EAAK,OAAA;AAAA,MACL,WAAA,EAAY,mBAAA;AAAA,MACZ,KAAA;AAAA,MACA,QAAA,EAAU,CAAA,KAAA,KAAS,QAAA,CAAS,KAAA,CAAM,OAAO,KAAK,CAAA;AAAA,MAC9C,UAAA,EAAY,EAAE,YAAA,EAAc,aAAA,EAAc;AAAA,MAC1C,UAAA,EAAY;AAAA,QACV,cAAA,kBACE,GAAA,CAAC,cAAA,EAAA,EAAe,QAAA,EAAS,OAAA,EACvB,8BAAC,MAAA,EAAA,EAAO,SAAA,EAAW,OAAA,CAAQ,UAAA,EAAY,CAAA,EACzC,CAAA;AAAA,QAEF,YAAA,EAAc,KAAA,mBACZ,GAAA,CAAC,cAAA,EAAA,EAAe,UAAS,KAAA,EACvB,QAAA,kBAAA,GAAA;AAAA,UAAC,UAAA;AAAA,UAAA;AAAA,YACC,IAAA,EAAK,OAAA;AAAA,YACL,YAAA,EAAW,cAAA;AAAA,YACX,OAAA,EAAS,MAAM,QAAA,CAAS,EAAE,CAAA;AAAA,YAE1B,8BAAC,KAAA,EAAA,EAAM;AAAA;AAAA,WAEX,CAAA,GACE;AAAA;AACN;AAAA,GACF;AAEJ;;;;"}
@@ -0,0 +1,41 @@
1
+ import { parseEntityRef } from '@backstage/catalog-model';
2
+ import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
3
+ import { ANNOTATION_CATEGORY, ANNOTATION_API_HUB_VERSION, ANNOTATION_HAS_SPEC } 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 toApiCardViewModel(entity) {
18
+ const annotations = entity.metadata.annotations ?? {};
19
+ const owner = entity.spec?.owner;
20
+ const description = entity.metadata.description?.trim() ? entity.metadata.description : void 0;
21
+ return {
22
+ name: entity.metadata.title ?? entity.metadata.name,
23
+ ownerLabel: owner ? humanizeEntityRef(
24
+ parseEntityRef(owner, { defaultKind: "group", defaultNamespace: "default" }),
25
+ { defaultKind: "group" }
26
+ ) : "unknown",
27
+ badge: toBadge(entity.spec?.lifecycle),
28
+ 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,
31
+ description,
32
+ targetRef: {
33
+ kind: "API",
34
+ namespace: entity.metadata.namespace ?? "default",
35
+ name: entity.metadata.name
36
+ }
37
+ };
38
+ }
39
+
40
+ export { toApiCardViewModel };
41
+ //# 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\r\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\r\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\r\nimport {\r\n ANNOTATION_HAS_SPEC,\r\n ANNOTATION_CATEGORY,\r\n ANNOTATION_API_HUB_VERSION,\r\n} from '../constants';\r\n\r\nexport type LifecycleBadgeKind = 'ok' | 'warning' | 'error';\r\n\r\nexport interface ApiCardViewModel {\r\n /** metadata.title ?? metadata.name */\r\n name: string;\r\n /** humanizeEntityRef(spec.owner, { defaultKind: 'group' }) — 'unknown' when absent */\r\n ownerLabel: string;\r\n badge: { label: string; kind: LifecycleBadgeKind };\r\n /** annotations['apigee.com/has-spec'] === 'true' */\r\n hasSpec: boolean;\r\n /** annotations['apigee.com/api-hub-version'] — omitted when absent (D-05) */\r\n version?: string;\r\n /** first comma-separated slug of apigee.com/category — undefined => 'Other' bucket */\r\n category?: string;\r\n /** metadata.description — omitted when empty */\r\n description?: string;\r\n /** for entityRouteRef navigation (05-04) */\r\n targetRef: { kind: 'API'; namespace: string; name: string };\r\n}\r\n\r\n/** D-04 lifecycle -> badge mapping. Unknown/undeployed/missing default to warning. */\r\nfunction toBadge(lifecycle: string | undefined): {\r\n label: string;\r\n kind: LifecycleBadgeKind;\r\n} {\r\n switch (lifecycle) {\r\n case 'production':\r\n return { label: 'Production', kind: 'ok' };\r\n case 'deprecated':\r\n return { label: 'Deprecated', kind: 'error' };\r\n case 'experimental':\r\n return { label: 'Experimental', kind: 'warning' };\r\n default:\r\n return { label: 'Unknown', kind: 'warning' };\r\n }\r\n}\r\n\r\n/** PURE mapper: an Apigee `kind: API` entity -> the view-model each tile renders. */\r\nexport function toApiCardViewModel(entity: Entity): ApiCardViewModel {\r\n const annotations = entity.metadata.annotations ?? {};\r\n const owner = entity.spec?.owner as string | undefined;\r\n const description = entity.metadata.description?.trim()\r\n ? entity.metadata.description\r\n : undefined;\r\n\r\n return {\r\n name: entity.metadata.title ?? entity.metadata.name,\r\n ownerLabel: owner\r\n ? humanizeEntityRef(\r\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\r\n { defaultKind: 'group' },\r\n )\r\n : 'unknown',\r\n badge: toBadge(entity.spec?.lifecycle as string | undefined),\r\n hasSpec: annotations[ANNOTATION_HAS_SPEC] === 'true',\r\n version: annotations[ANNOTATION_API_HUB_VERSION] || undefined,\r\n category: annotations[ANNOTATION_CATEGORY]?.split(',')[0]?.trim() || undefined,\r\n description,\r\n targetRef: {\r\n kind: 'API',\r\n namespace: entity.metadata.namespace ?? 'default',\r\n name: entity.metadata.name,\r\n },\r\n };\r\n}\r\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;;;;"}
@@ -0,0 +1,62 @@
1
+ import { parseEntityRef } from '@backstage/catalog-model';
2
+ import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
3
+ import { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG, LABEL_DEPARTMENT, ANNOTATION_CATEGORY } from '../constants.esm.js';
4
+
5
+ function isApigeeEntity(e) {
6
+ const a = e.metadata.annotations ?? {};
7
+ return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;
8
+ }
9
+ const entityLifecycle = (e) => e.spec?.lifecycle?.trim() || void 0;
10
+ const entityOwnerLabel = (e) => {
11
+ const owner = e.spec?.owner;
12
+ return owner ? humanizeEntityRef(
13
+ parseEntityRef(owner, { defaultKind: "group", defaultNamespace: "default" }),
14
+ { defaultKind: "group" }
15
+ ) : void 0;
16
+ };
17
+ const entityOrg = (e) => {
18
+ const a = e.metadata.annotations ?? {};
19
+ return (a[ANNOTATION_APIGEE_ORGS] ?? a[ANNOTATION_APIGEE_ORG])?.split(",")[0]?.trim() || void 0;
20
+ };
21
+ const entityCategory = (e) => (e.metadata.annotations ?? {})[ANNOTATION_CATEGORY]?.split(",")[0]?.trim() || void 0;
22
+ const entityCategoryFacet = (e) => entityCategory(e) ?? "other";
23
+ const entityDepartment = (e) => (e.metadata.labels ?? {})[LABEL_DEPARTMENT]?.trim() || void 0;
24
+ class FieldFacetFilter {
25
+ constructor(extract, values) {
26
+ this.extract = extract;
27
+ this.values = values;
28
+ }
29
+ extract;
30
+ values;
31
+ filterEntity(entity) {
32
+ const v = this.extract(entity);
33
+ return v !== void 0 && this.values.includes(v);
34
+ }
35
+ }
36
+ function apiSearchHaystack(e) {
37
+ return [
38
+ e.metadata.title ?? e.metadata.name,
39
+ e.metadata.description ?? "",
40
+ entityOwnerLabel(e) ?? "",
41
+ entityOrg(e) ?? "",
42
+ entityCategory(e) ?? ""
43
+ ].join(" ").toLowerCase();
44
+ }
45
+ class ApiSearchFilter {
46
+ needle;
47
+ constructor(query) {
48
+ this.needle = query.trim().toLowerCase();
49
+ }
50
+ filterEntity(entity) {
51
+ return this.needle === "" || apiSearchHaystack(entity).includes(this.needle);
52
+ }
53
+ }
54
+ class ApiKindFilter {
55
+ value = "API";
56
+ getCatalogFilters() {
57
+ return { kind: this.value };
58
+ }
59
+ }
60
+
61
+ export { ApiKindFilter, ApiSearchFilter, FieldFacetFilter, apiSearchHaystack, entityCategory, entityCategoryFacet, entityDepartment, entityLifecycle, entityOrg, entityOwnerLabel, isApigeeEntity };
62
+ //# sourceMappingURL=apigeeFacetFilters.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apigeeFacetFilters.esm.js","sources":["../../src/components/apigeeFacetFilters.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\r\nimport type { EntityFilter } from '@backstage/plugin-catalog-react';\r\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\r\nimport {\r\n ANNOTATION_APIGEE_ORGS,\r\n ANNOTATION_APIGEE_ORG,\r\n ANNOTATION_CATEGORY,\r\n LABEL_DEPARTMENT,\r\n} from '../constants';\r\n\r\n/** Dual-key is-Apigee gate (same rule as the existing ApigeeOrgFilter). */\r\nexport function isApigeeEntity(e: Entity): boolean {\r\n const a = e.metadata.annotations ?? {};\r\n return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;\r\n}\r\n\r\n// --- Pure field extractors: the ONE place each facet's value is defined. Reused by\r\n// deriveFacets(), the facet predicates, and the search haystack. Unit-testable. ---\r\nexport const entityLifecycle = (e: Entity): string | undefined =>\r\n (e.spec?.lifecycle as string | undefined)?.trim() || undefined;\r\n\r\nexport const entityOwnerLabel = (e: Entity): string | undefined => {\r\n const owner = e.spec?.owner as string | undefined;\r\n return owner\r\n ? humanizeEntityRef(\r\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\r\n { defaultKind: 'group' },\r\n ) // 'group:default/payments-team' -> 'payments-team'\r\n : undefined;\r\n};\r\n\r\nexport const entityOrg = (e: Entity): string | undefined => {\r\n const a = e.metadata.annotations ?? {};\r\n return (\r\n (a[ANNOTATION_APIGEE_ORGS] ?? a[ANNOTATION_APIGEE_ORG])?.split(',')[0]?.trim() ||\r\n undefined\r\n );\r\n};\r\n\r\nexport const entityCategory = (e: Entity): string | undefined =>\r\n (e.metadata.annotations ?? {})[ANNOTATION_CATEGORY]?.split(',')[0]?.trim() ||\r\n undefined;\r\n\r\n/**\r\n * Facet-only category value: maps an uncategorised entity to the 'other' bucket\r\n * (LOCKED UI-SPEC Facet Data). Search uses `entityCategory` (the REAL slug) instead,\r\n * so a search for \"other\" does not match uncategorised APIs.\r\n */\r\nexport const entityCategoryFacet = (e: Entity): string =>\r\n entityCategory(e) ?? 'other';\r\n\r\n/** Department facet value: the `department` label written from the API Hub \"Business unit\". */\r\nexport const entityDepartment = (e: Entity): string | undefined =>\r\n (e.metadata.labels ?? {})[LABEL_DEPARTMENT]?.trim() || undefined;\r\n\r\n/** Generic single-field multi-value facet. Within-facet OR. No URL seam / getCatalogFilters. */\r\nexport class FieldFacetFilter implements EntityFilter {\r\n constructor(\r\n private readonly extract: (e: Entity) => string | undefined,\r\n public readonly values: string[],\r\n ) {}\r\n filterEntity(entity: Entity): boolean {\r\n const v = this.extract(entity);\r\n return v !== undefined && this.values.includes(v); // OR across selected values\r\n }\r\n}\r\n\r\n/** Broad \"find anything\" search: name + description + owner + org + category (D-09). */\r\nexport function apiSearchHaystack(e: Entity): string {\r\n return [\r\n e.metadata.title ?? e.metadata.name,\r\n e.metadata.description ?? '',\r\n entityOwnerLabel(e) ?? '',\r\n entityOrg(e) ?? '',\r\n entityCategory(e) ?? '',\r\n ]\r\n .join(' ')\r\n .toLowerCase();\r\n}\r\n\r\nexport class ApiSearchFilter implements EntityFilter {\r\n private readonly needle: string;\r\n constructor(query: string) {\r\n this.needle = query.trim().toLowerCase();\r\n }\r\n filterEntity(entity: Entity): boolean {\r\n // String.includes ONLY — never build a RegExp from user input (ReDoS guard).\r\n return this.needle === '' || apiSearchHaystack(entity).includes(this.needle);\r\n }\r\n}\r\n\r\n/**\r\n * Replaces EntityKindFilter('api','API'): keeps the backend kind=API filter but exposes no\r\n * URL seam, so the provider does not serialize `filters[kind]=API` into the URL (D-06).\r\n */\r\nexport class ApiKindFilter implements EntityFilter {\r\n readonly value = 'API';\r\n getCatalogFilters(): Record<string, string> {\r\n return { kind: this.value };\r\n }\r\n}\r\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants.esm.js';
2
+
3
+ class ApigeeOrgFilter {
4
+ filterEntity(entity) {
5
+ const a = entity.metadata.annotations ?? {};
6
+ return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;
7
+ }
8
+ }
9
+
10
+ export { ApigeeOrgFilter };
11
+ //# sourceMappingURL=apigeeOrgFilter.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apigeeOrgFilter.esm.js","sources":["../../src/components/apigeeOrgFilter.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport type { Entity } from '@backstage/catalog-model';\r\nimport type { EntityFilter } from '@backstage/plugin-catalog-react';\r\nimport { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants';\r\n\r\n/**\r\n * Frontend predicate that keeps an entity when it carries EITHER Apigee org key.\r\n * MUST match both: proxy-derived APIs use the plural key, hub-only APIs the singular\r\n * (RESEARCH Pitfall 1 / CAT-05). Uses filterEntity (not getCatalogFilters) because a\r\n * single backend filter record cannot OR two different annotation keys.\r\n */\r\nexport class ApigeeOrgFilter implements EntityFilter {\r\n filterEntity(entity: Entity): boolean {\r\n const a = entity.metadata.annotations ?? {};\r\n return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;\r\n }\r\n}\r\n"],"names":[],"mappings":";;AAWO,MAAM,eAAA,CAAwC;AAAA,EACnD,aAAa,MAAA,EAAyB;AACpC,IAAA,MAAM,CAAA,GAAI,MAAA,CAAO,QAAA,CAAS,WAAA,IAAe,EAAC;AAC1C,IAAA,OAAO,sBAAA,IAA0B,KAAK,qBAAA,IAAyB,CAAA;AAAA,EACjE;AACF;;;;"}
@@ -0,0 +1,21 @@
1
+ import { FACET_ORDER } from './facetModel.esm.js';
2
+
3
+ const QKEY = {
4
+ lifecycle: "lifecycle",
5
+ org: "org",
6
+ owner: "owner",
7
+ category: "category",
8
+ department: "department"
9
+ };
10
+ function parseSearchParams(sp) {
11
+ const selected = Object.fromEntries(
12
+ FACET_ORDER.map((k) => [k, sp.getAll(QKEY[k])])
13
+ );
14
+ return { selected, q: sp.get("q") ?? "" };
15
+ }
16
+ function isActive(state) {
17
+ return state.q.trim() !== "" || FACET_ORDER.some((k) => state.selected[k].length > 0);
18
+ }
19
+
20
+ export { isActive, parseSearchParams };
21
+ //# sourceMappingURL=apigeeSearchParams.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apigeeSearchParams.esm.js","sources":["../../src/components/apigeeSearchParams.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { FACET_ORDER, type FacetKey } from './facetModel';\r\n\r\nexport interface ApigeeFilterState {\r\n selected: Record<FacetKey, string[]>;\r\n q: string;\r\n}\r\n\r\n/** Clean URL keys (D-06): one query key per facet, no native `filters[...]` wrapper. */\r\nconst QKEY: Record<FacetKey, string> = {\r\n lifecycle: 'lifecycle',\r\n org: 'org',\r\n owner: 'owner',\r\n category: 'category',\r\n department: 'department',\r\n};\r\n\r\n/**\r\n * URL → state. Repeated keys become arrays (D-07) via `getAll`; unknown/extra keys are\r\n * ignored. Values are treated as opaque strings — no eval, RegExp, or HTML (T-06-02-01);\r\n * an unknown facet value simply matches nothing downstream (derived allow-list).\r\n */\r\nexport function parseSearchParams(sp: URLSearchParams): ApigeeFilterState {\r\n const selected = Object.fromEntries(\r\n FACET_ORDER.map(k => [k, sp.getAll(QKEY[k])]),\r\n ) as Record<FacetKey, string[]>;\r\n return { selected, q: sp.get('q') ?? '' };\r\n}\r\n\r\n/**\r\n * state → URL. Deterministic order (FACET_ORDER, then `q` last) reproduces the exact SC #3\r\n * shape `?lifecycle=deprecated&q=payment`. Only the known clean keys are appended and empties\r\n * are omitted (T-06-02-02) — no injection surface. Multi-value facets serialize as repeated\r\n * keys (D-07) via `append`.\r\n */\r\nexport function buildSearchParams(state: ApigeeFilterState): URLSearchParams {\r\n const sp = new URLSearchParams();\r\n for (const k of FACET_ORDER) {\r\n for (const v of state.selected[k]) {\r\n sp.append(QKEY[k], v);\r\n }\r\n }\r\n if (state.q.trim()) {\r\n sp.set('q', state.q.trim());\r\n }\r\n return sp;\r\n}\r\n\r\n/** True when any facet is selected or the search box holds a non-blank query. */\r\nexport function isActive(state: ApigeeFilterState): boolean {\r\n return (\r\n state.q.trim() !== '' || FACET_ORDER.some(k => state.selected[k].length > 0)\r\n );\r\n}\r\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;;;;"}
@@ -0,0 +1,63 @@
1
+ import Category from '@material-ui/icons/Category';
2
+ import Security from '@material-ui/icons/Security';
3
+ import Payment from '@material-ui/icons/Payment';
4
+ import People from '@material-ui/icons/People';
5
+ import ShoppingCart from '@material-ui/icons/ShoppingCart';
6
+ import Notifications from '@material-ui/icons/Notifications';
7
+ import Assessment from '@material-ui/icons/Assessment';
8
+ import SettingsEthernet from '@material-ui/icons/SettingsEthernet';
9
+ import Devices from '@material-ui/icons/Devices';
10
+ import Tv from '@material-ui/icons/Tv';
11
+ import Place from '@material-ui/icons/Place';
12
+
13
+ const ICONS = {
14
+ Category,
15
+ Security,
16
+ Payment,
17
+ People,
18
+ ShoppingCart,
19
+ Notifications,
20
+ Assessment,
21
+ SettingsEthernet,
22
+ Devices,
23
+ Tv,
24
+ Place
25
+ };
26
+ const DEFAULT_MAP = [
27
+ { match: /security|auth|identity|oauth/i, key: "Security" },
28
+ { match: /payment|billing|finance|fino/i, key: "Payment" },
29
+ { match: /customer|crm|people|partner/i, key: "People" },
30
+ { match: /order|commerce|catalog|product/i, key: "ShoppingCart" },
31
+ { match: /communication|messaging|notification/i, key: "Notifications" },
32
+ { match: /data|analytics|reporting/i, key: "Assessment" },
33
+ { match: /network|integration|common|infra/i, key: "SettingsEthernet" },
34
+ { match: /device|iot/i, key: "Devices" },
35
+ { match: /media|tv|content/i, key: "Tv" },
36
+ { match: /location|geo|gis/i, key: "Place" }
37
+ ];
38
+ function isIconKey(v) {
39
+ return Object.prototype.hasOwnProperty.call(ICONS, v);
40
+ }
41
+ function resolveCategoryIcon(slug, overrides) {
42
+ if (slug && overrides && Object.prototype.hasOwnProperty.call(overrides, slug)) {
43
+ const value = overrides[slug];
44
+ if (isIconKey(value)) {
45
+ return ICONS[value];
46
+ }
47
+ console.warn(
48
+ '[apigee] Ignoring unknown categoryIcons value "%s" for "%s"; using Category',
49
+ value,
50
+ slug
51
+ );
52
+ }
53
+ if (slug) {
54
+ const match = DEFAULT_MAP.find((entry) => entry.match.test(slug));
55
+ if (match) {
56
+ return ICONS[match.key];
57
+ }
58
+ }
59
+ return ICONS.Category;
60
+ }
61
+
62
+ export { ICONS, resolveCategoryIcon };
63
+ //# sourceMappingURL=categoryIcons.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"categoryIcons.esm.js","sources":["../../src/components/categoryIcons.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport type { SvgIconComponent } from '@material-ui/icons';\r\nimport Category from '@material-ui/icons/Category';\r\nimport Security from '@material-ui/icons/Security';\r\nimport Payment from '@material-ui/icons/Payment';\r\nimport People from '@material-ui/icons/People';\r\nimport ShoppingCart from '@material-ui/icons/ShoppingCart';\r\nimport Notifications from '@material-ui/icons/Notifications';\r\nimport Assessment from '@material-ui/icons/Assessment';\r\nimport SettingsEthernet from '@material-ui/icons/SettingsEthernet';\r\nimport Devices from '@material-ui/icons/Devices';\r\nimport Tv from '@material-ui/icons/Tv';\r\nimport Place from '@material-ui/icons/Place';\r\n\r\nexport type IconKey =\r\n | 'Category'\r\n | 'Security'\r\n | 'Payment'\r\n | 'People'\r\n | 'ShoppingCart'\r\n | 'Notifications'\r\n | 'Assessment'\r\n | 'SettingsEthernet'\r\n | 'Devices'\r\n | 'Tv'\r\n | 'Place';\r\n\r\n/**\r\n * Fixed allow-list registry. A config override may ONLY select one of these keys —\r\n * a config value is never a module path or an arbitrary component (T-05-01).\r\n */\r\nexport const ICONS: Record<IconKey, SvgIconComponent> = {\r\n Category,\r\n Security,\r\n Payment,\r\n People,\r\n ShoppingCart,\r\n Notifications,\r\n Assessment,\r\n SettingsEthernet,\r\n Devices,\r\n Tv,\r\n Place,\r\n};\r\n\r\n/** Default category-slug -> IconKey substring map (D-08, case-insensitive). */\r\nconst DEFAULT_MAP: ReadonlyArray<{ match: RegExp; key: IconKey }> = [\r\n { match: /security|auth|identity|oauth/i, key: 'Security' },\r\n { match: /payment|billing|finance|fino/i, key: 'Payment' },\r\n { match: /customer|crm|people|partner/i, key: 'People' },\r\n { match: /order|commerce|catalog|product/i, key: 'ShoppingCart' },\r\n { match: /communication|messaging|notification/i, key: 'Notifications' },\r\n { match: /data|analytics|reporting/i, key: 'Assessment' },\r\n { match: /network|integration|common|infra/i, key: 'SettingsEthernet' },\r\n { match: /device|iot/i, key: 'Devices' },\r\n { match: /media|tv|content/i, key: 'Tv' },\r\n { match: /location|geo|gis/i, key: 'Place' },\r\n];\r\n\r\n/** Type guard: is `v` an OWN key of the static ICONS allow-list? */\r\nfunction isIconKey(v: string): v is IconKey {\r\n return Object.prototype.hasOwnProperty.call(ICONS, v);\r\n}\r\n\r\n/**\r\n * Resolve a category slug to a Material icon component.\r\n *\r\n * A config `overrides` value may only select an allow-listed IconKey; any other value\r\n * (unknown key, module path, `constructor`, etc.) falls back to `Category` and warns\r\n * once. This never `import()`s a config path or renders an arbitrary component — it is\r\n * the primary mitigation for the config-driven component-injection threat (T-05-01).\r\n */\r\nexport function resolveCategoryIcon(\r\n slug: string | undefined,\r\n overrides?: Record<string, string>,\r\n): SvgIconComponent {\r\n if (slug && overrides && Object.prototype.hasOwnProperty.call(overrides, slug)) {\r\n const value = overrides[slug];\r\n if (isIconKey(value)) {\r\n return ICONS[value];\r\n }\r\n // eslint-disable-next-line no-console\r\n console.warn(\r\n '[apigee] Ignoring unknown categoryIcons value \"%s\" for \"%s\"; using Category',\r\n value,\r\n slug,\r\n );\r\n }\r\n\r\n if (slug) {\r\n const match = DEFAULT_MAP.find(entry => entry.match.test(slug));\r\n if (match) {\r\n return ICONS[match.key];\r\n }\r\n }\r\n\r\n return ICONS.Category;\r\n}\r\n"],"names":[],"mappings":";;;;;;;;;;;;AA+BO,MAAM,KAAA,GAA2C;AAAA,EACtD,QAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,MAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA,UAAA;AAAA,EACA,gBAAA;AAAA,EACA,OAAA;AAAA,EACA,EAAA;AAAA,EACA;AACF;AAGA,MAAM,WAAA,GAA8D;AAAA,EAClE,EAAE,KAAA,EAAO,+BAAA,EAAiC,GAAA,EAAK,UAAA,EAAW;AAAA,EAC1D,EAAE,KAAA,EAAO,+BAAA,EAAiC,GAAA,EAAK,SAAA,EAAU;AAAA,EACzD,EAAE,KAAA,EAAO,8BAAA,EAAgC,GAAA,EAAK,QAAA,EAAS;AAAA,EACvD,EAAE,KAAA,EAAO,iCAAA,EAAmC,GAAA,EAAK,cAAA,EAAe;AAAA,EAChE,EAAE,KAAA,EAAO,uCAAA,EAAyC,GAAA,EAAK,eAAA,EAAgB;AAAA,EACvE,EAAE,KAAA,EAAO,2BAAA,EAA6B,GAAA,EAAK,YAAA,EAAa;AAAA,EACxD,EAAE,KAAA,EAAO,mCAAA,EAAqC,GAAA,EAAK,kBAAA,EAAmB;AAAA,EACtE,EAAE,KAAA,EAAO,aAAA,EAAe,GAAA,EAAK,SAAA,EAAU;AAAA,EACvC,EAAE,KAAA,EAAO,mBAAA,EAAqB,GAAA,EAAK,IAAA,EAAK;AAAA,EACxC,EAAE,KAAA,EAAO,mBAAA,EAAqB,GAAA,EAAK,OAAA;AACrC,CAAA;AAGA,SAAS,UAAU,CAAA,EAAyB;AAC1C,EAAA,OAAO,MAAA,CAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,OAAO,CAAC,CAAA;AACtD;AAUO,SAAS,mBAAA,CACd,MACA,SAAA,EACkB;AAClB,EAAA,IAAI,IAAA,IAAQ,aAAa,MAAA,CAAO,SAAA,CAAU,eAAe,IAAA,CAAK,SAAA,EAAW,IAAI,CAAA,EAAG;AAC9E,IAAA,MAAM,KAAA,GAAQ,UAAU,IAAI,CAAA;AAC5B,IAAA,IAAI,SAAA,CAAU,KAAK,CAAA,EAAG;AACpB,MAAA,OAAO,MAAM,KAAK,CAAA;AAAA,IACpB;AAEA,IAAA,OAAA,CAAQ,IAAA;AAAA,MACN,6EAAA;AAAA,MACA,KAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,IAAI,IAAA,EAAM;AACR,IAAA,MAAM,KAAA,GAAQ,YAAY,IAAA,CAAK,CAAA,KAAA,KAAS,MAAM,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAA;AAC9D,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,OAAO,KAAA,CAAM,MAAM,GAAG,CAAA;AAAA,IACxB;AAAA,EACF;AAEA,EAAA,OAAO,KAAA,CAAM,QAAA;AACf;;;;"}
@@ -0,0 +1,87 @@
1
+ import { resolveCategoryIcon } from './categoryIcons.esm.js';
2
+ import { humanise } from './grouping.esm.js';
3
+ import { entityDepartment, entityCategoryFacet, entityOwnerLabel, entityOrg, entityLifecycle } from './apigeeFacetFilters.esm.js';
4
+
5
+ const FACET_ORDER = [
6
+ "lifecycle",
7
+ "org",
8
+ "owner",
9
+ "category",
10
+ "department"
11
+ ];
12
+ const LIFECYCLE_SEVERITY = ["production", "experimental", "deprecated", "unknown"];
13
+ const FACET_TRIGGER_LABEL = {
14
+ lifecycle: "Lifecycle",
15
+ org: "Org",
16
+ owner: "Owner",
17
+ category: "Category",
18
+ department: "Department"
19
+ };
20
+ const EXTRACT = {
21
+ lifecycle: entityLifecycle,
22
+ org: entityOrg,
23
+ owner: entityOwnerLabel,
24
+ category: entityCategoryFacet,
25
+ department: entityDepartment
26
+ };
27
+ function labelFor(key, value, orgLabels) {
28
+ switch (key) {
29
+ case "lifecycle":
30
+ return value.charAt(0).toUpperCase() + value.slice(1);
31
+ case "owner":
32
+ return value;
33
+ case "category":
34
+ return value === "other" ? "Other" : humanise(value);
35
+ case "org":
36
+ return orgLabels?.[value] ?? humanise(value);
37
+ default:
38
+ return humanise(value);
39
+ }
40
+ }
41
+ function sortValues(key, values) {
42
+ if (key === "lifecycle") {
43
+ const rank = (v) => {
44
+ const i = LIFECYCLE_SEVERITY.indexOf(v);
45
+ return i === -1 ? LIFECYCLE_SEVERITY.length : i;
46
+ };
47
+ return [...values].sort((a, b) => rank(a) - rank(b));
48
+ }
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
+ return [...values].sort((a, b) => a.localeCompare(b));
57
+ }
58
+ function deriveFacets(entities, overrides, orgLabels) {
59
+ return FACET_ORDER.map((key) => {
60
+ const distinct = Array.from(
61
+ new Set(entities.map(EXTRACT[key]).filter((v) => !!v))
62
+ );
63
+ const options = sortValues(key, distinct).map((value) => ({
64
+ value,
65
+ label: labelFor(key, value, orgLabels)
66
+ }));
67
+ const icons = key === "category" ? Object.fromEntries(
68
+ options.map((opt) => [
69
+ opt.value,
70
+ resolveCategoryIcon(
71
+ opt.value === "other" ? void 0 : opt.value,
72
+ overrides
73
+ )
74
+ ])
75
+ ) : void 0;
76
+ return {
77
+ key,
78
+ label: FACET_TRIGGER_LABEL[key],
79
+ options,
80
+ enabled: options.length > 0,
81
+ ...icons !== void 0 ? { icons } : {}
82
+ };
83
+ });
84
+ }
85
+
86
+ export { FACET_ORDER, deriveFacets };
87
+ //# sourceMappingURL=facetModel.esm.js.map