@nospt/backstage-plugin-apigee 1.1.0-rc2 → 1.1.0-rc4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @nospt/backstage-plugin-apigee might be problematic. Click here for more details.
- package/README.md +19 -19
- package/config.d.ts +25 -25
- package/dist/components/ApiCard.esm.js +67 -125
- package/dist/components/ApiCard.esm.js.map +1 -1
- package/dist/components/ApiCard.module.css.esm.js +8 -0
- package/dist/components/ApiCard.module.css.esm.js.map +1 -0
- package/dist/components/ApiCardGrid.esm.js +12 -46
- package/dist/components/ApiCardGrid.esm.js.map +1 -1
- package/dist/components/ApiTable.esm.js +162 -0
- package/dist/components/ApiTable.esm.js.map +1 -0
- package/dist/components/ApiTable.module.css.esm.js +8 -0
- package/dist/components/ApiTable.module.css.esm.js.map +1 -0
- package/dist/components/ApigeeApiCatalogPage.esm.js +80 -60
- package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -1
- package/dist/components/ApigeeNavContent.esm.js +2 -2
- package/dist/components/ApigeeNavContent.esm.js.map +1 -1
- package/dist/components/CountCards.esm.js +122 -0
- package/dist/components/CountCards.esm.js.map +1 -0
- package/dist/components/CountCards.module.css.esm.js +8 -0
- package/dist/components/CountCards.module.css.esm.js.map +1 -0
- package/dist/components/FilterToolbar.esm.js +75 -142
- package/dist/components/FilterToolbar.esm.js.map +1 -1
- package/dist/components/FilterToolbar.module.css.esm.js +8 -0
- package/dist/components/FilterToolbar.module.css.esm.js.map +1 -0
- package/dist/components/SearchBar.esm.js +7 -35
- package/dist/components/SearchBar.esm.js.map +1 -1
- package/dist/components/apiCardViewModel.esm.js +16 -2
- package/dist/components/apiCardViewModel.esm.js.map +1 -1
- package/dist/components/apigeeFacetFilters.esm.js.map +1 -1
- package/dist/components/apigeeOrgFilter.esm.js.map +1 -1
- package/dist/components/apigeeSearchParams.esm.js +14 -1
- package/dist/components/apigeeSearchParams.esm.js.map +1 -1
- package/dist/components/categoryIcons.esm.js +12 -22
- package/dist/components/categoryIcons.esm.js.map +1 -1
- package/dist/components/facetModel.esm.js +30 -10
- package/dist/components/facetModel.esm.js.map +1 -1
- package/dist/components/grouping.esm.js +1 -26
- package/dist/components/grouping.esm.js.map +1 -1
- package/dist/components/states.esm.js +19 -49
- package/dist/components/states.esm.js.map +1 -1
- package/dist/components/useApigeeFilters.esm.js +18 -2
- package/dist/components/useApigeeFilters.esm.js.map +1 -1
- package/dist/components/useDebouncedValue.esm.js.map +1 -1
- package/dist/constants.esm.js +3 -2
- package/dist/constants.esm.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
- package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
- package/dist/plugin.esm.js +2 -2
- package/dist/plugin.esm.js.map +1 -1
- package/dist/routes.esm.js.map +1 -1
- package/package.json +17 -15
|
@@ -1,156 +1,87 @@
|
|
|
1
|
-
import { jsxs, jsx
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, 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
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
7
|
function FacetDropdown({
|
|
56
8
|
facet,
|
|
57
9
|
selected,
|
|
58
10
|
onToggle,
|
|
59
11
|
onClear
|
|
60
12
|
}) {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const open = Boolean(anchorEl);
|
|
13
|
+
const [open, setOpen] = useState(false);
|
|
14
|
+
const ref = useRef(null);
|
|
64
15
|
const menuId = `facet-menu-${facet.key}`;
|
|
65
16
|
const count = selected.length;
|
|
66
|
-
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!open) {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
const onDocMouseDown = (event) => {
|
|
22
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
23
|
+
setOpen(false);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const onKeyDown = (event) => {
|
|
27
|
+
if (event.key === "Escape") {
|
|
28
|
+
setOpen(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
document.addEventListener("mousedown", onDocMouseDown);
|
|
32
|
+
document.addEventListener("keydown", onKeyDown);
|
|
33
|
+
return () => {
|
|
34
|
+
document.removeEventListener("mousedown", onDocMouseDown);
|
|
35
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
36
|
+
};
|
|
37
|
+
}, [open]);
|
|
38
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: styles.facet, children: [
|
|
67
39
|
/* @__PURE__ */ jsxs(
|
|
68
40
|
Button,
|
|
69
41
|
{
|
|
70
|
-
|
|
71
|
-
variant: "outlined",
|
|
42
|
+
variant: count > 0 ? "primary" : "secondary",
|
|
72
43
|
size: "small",
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
color: count > 0 ? "primary" : "default",
|
|
44
|
+
isDisabled: !facet.enabled,
|
|
45
|
+
iconEnd: /* @__PURE__ */ jsx(RiArrowDropDownLine, {}),
|
|
76
46
|
"aria-haspopup": "true",
|
|
77
47
|
"aria-expanded": open,
|
|
78
48
|
"aria-controls": open ? menuId : void 0,
|
|
79
49
|
"aria-label": count > 0 ? `${facet.label} filter, ${count} selected` : `${facet.label} filter`,
|
|
80
|
-
|
|
50
|
+
onPress: () => setOpen((prev) => !prev),
|
|
81
51
|
children: [
|
|
82
52
|
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
|
|
53
|
+
count > 0 ? /* @__PURE__ */ jsx("span", { className: styles.countChip, children: count }) : null
|
|
92
54
|
]
|
|
93
55
|
}
|
|
94
56
|
),
|
|
95
|
-
/* @__PURE__ */ jsxs(
|
|
96
|
-
|
|
57
|
+
open ? /* @__PURE__ */ jsxs(
|
|
58
|
+
"div",
|
|
97
59
|
{
|
|
98
60
|
id: menuId,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
getContentAnchorEl: null,
|
|
103
|
-
anchorOrigin: { vertical: "bottom", horizontal: "left" },
|
|
104
|
-
transformOrigin: { vertical: "top", horizontal: "left" },
|
|
61
|
+
className: styles.menu,
|
|
62
|
+
role: "group",
|
|
63
|
+
"aria-label": `${facet.label} options`,
|
|
105
64
|
children: [
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
65
|
+
facet.options.map((option) => {
|
|
66
|
+
const OptionIcon = facet.icons?.[option.value];
|
|
67
|
+
return /* @__PURE__ */ jsx(
|
|
68
|
+
Checkbox,
|
|
69
|
+
{
|
|
70
|
+
className: styles.option,
|
|
71
|
+
isSelected: selected.includes(option.value),
|
|
72
|
+
onChange: () => onToggle(option.value),
|
|
73
|
+
children: OptionIcon ? /* @__PURE__ */ jsxs("span", { className: styles.optionLabel, children: [
|
|
74
|
+
/* @__PURE__ */ jsx(OptionIcon, { "aria-hidden": true, size: 16 }),
|
|
75
|
+
option.label
|
|
76
|
+
] }) : option.label
|
|
77
|
+
},
|
|
78
|
+
option.value
|
|
79
|
+
);
|
|
80
|
+
}),
|
|
81
|
+
count > 0 ? /* @__PURE__ */ jsx("div", { className: styles.menuFooter, children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "small", onPress: onClear, children: "Clear" }) }) : null
|
|
151
82
|
]
|
|
152
83
|
}
|
|
153
|
-
)
|
|
84
|
+
) : null
|
|
154
85
|
] });
|
|
155
86
|
}
|
|
156
87
|
function FilterToolbar({
|
|
@@ -161,11 +92,11 @@ function FilterToolbar({
|
|
|
161
92
|
onClearAll,
|
|
162
93
|
active,
|
|
163
94
|
resultCount,
|
|
164
|
-
searchBar
|
|
95
|
+
searchBar,
|
|
96
|
+
viewToggle
|
|
165
97
|
}) {
|
|
166
|
-
const classes = useStyles();
|
|
167
98
|
return /* @__PURE__ */ jsxs("div", { role: "search", "aria-label": "Filter and search APIs", children: [
|
|
168
|
-
/* @__PURE__ */ jsxs(
|
|
99
|
+
/* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
|
|
169
100
|
facets.map((facet) => /* @__PURE__ */ jsx(
|
|
170
101
|
FacetDropdown,
|
|
171
102
|
{
|
|
@@ -176,20 +107,22 @@ function FilterToolbar({
|
|
|
176
107
|
},
|
|
177
108
|
facet.key
|
|
178
109
|
)),
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
110
|
+
/* @__PURE__ */ jsxs("div", { className: styles.searchRow, children: [
|
|
111
|
+
searchBar,
|
|
112
|
+
active ? /* @__PURE__ */ jsx(
|
|
113
|
+
Button,
|
|
114
|
+
{
|
|
115
|
+
variant: "tertiary",
|
|
116
|
+
size: "small",
|
|
117
|
+
iconStart: /* @__PURE__ */ jsx(RiCloseLine, {}),
|
|
118
|
+
onPress: onClearAll,
|
|
119
|
+
children: "Clear all"
|
|
120
|
+
}
|
|
121
|
+
) : null,
|
|
122
|
+
viewToggle
|
|
123
|
+
] })
|
|
191
124
|
] }),
|
|
192
|
-
/* @__PURE__ */ jsx("span", { className:
|
|
125
|
+
/* @__PURE__ */ jsx("span", { className: styles.srOnly, "aria-live": "polite", children: `${resultCount} APIs match your filters` })
|
|
193
126
|
] });
|
|
194
127
|
}
|
|
195
128
|
|
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"FilterToolbar.esm.js","sources":["../../src/components/FilterToolbar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Button, Checkbox } from '@backstage/ui';\nimport { RiArrowDropDownLine, RiCloseLine } from '@remixicon/react';\nimport type { DerivedFacet, FacetKey } from './facetModel';\nimport styles from './FilterToolbar.module.css';\n\nexport interface FilterToolbarProps {\n /** Derived facets from `deriveFacets()`; ALL render (revised D-05) — one with no values is disabled. */\n facets: DerivedFacet[];\n /** Current selections per facet (hydrated from the URL state in 06-04). */\n selected: Record<FacetKey, string[]>;\n onToggleFacet: (key: FacetKey, value: string) => void;\n /** Per-facet menu-footer \"Clear\" (clears just that facet). */\n onClearFacet: (key: FacetKey) => void;\n /** Toolbar \"Clear all\" (resets every facet + search). */\n onClearAll: () => void;\n /** `isActive(state)` — gates the \"Clear all\" affordance. */\n active: boolean;\n /** Filtered result count for the polite live region (UI-07). */\n resultCount: number;\n /** `SearchBar` mounted inline in the toolbar row. */\n searchBar: ReactNode;\n /** 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 onToggle,\n onClear,\n}: {\n facet: DerivedFacet;\n selected: string[];\n onToggle: (value: string) => void;\n onClear: () => void;\n}) {\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n const menuId = `facet-menu-${facet.key}`;\n const count = selected.length;\n\n useEffect(() => {\n if (!open) {\n return undefined;\n }\n const onDocMouseDown = (event: MouseEvent) => {\n if (ref.current && !ref.current.contains(event.target as Node)) {\n setOpen(false);\n }\n };\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', onDocMouseDown);\n document.addEventListener('keydown', onKeyDown);\n return () => {\n document.removeEventListener('mousedown', onDocMouseDown);\n document.removeEventListener('keydown', onKeyDown);\n };\n }, [open]);\n\n return (\n <div ref={ref} className={styles.facet}>\n <Button\n variant={count > 0 ? 'primary' : 'secondary'}\n size=\"small\"\n isDisabled={!facet.enabled}\n iconEnd={<RiArrowDropDownLine />}\n aria-haspopup=\"true\"\n aria-expanded={open}\n aria-controls={open ? menuId : undefined}\n aria-label={\n count > 0\n ? `${facet.label} filter, ${count} selected`\n : `${facet.label} filter`\n }\n onPress={() => setOpen(prev => !prev)}\n >\n {facet.label}\n {count > 0 ? <span className={styles.countChip}>{count}</span> : null}\n </Button>\n {open ? (\n <div\n id={menuId}\n className={styles.menu}\n role=\"group\"\n aria-label={`${facet.label} options`}\n >\n {facet.options.map(option => {\n const OptionIcon = facet.icons?.[option.value];\n return (\n <Checkbox\n key={option.value}\n className={styles.option}\n isSelected={selected.includes(option.value)}\n onChange={() => onToggle(option.value)}\n >\n {OptionIcon ? (\n <span className={styles.optionLabel}>\n <OptionIcon aria-hidden size={16} />\n {option.label}\n </span>\n ) : (\n option.label\n )}\n </Checkbox>\n );\n })}\n {count > 0 ? (\n <div className={styles.menuFooter}>\n <Button variant=\"tertiary\" size=\"small\" onPress={onClear}>\n Clear\n </Button>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Presentational Phase 6 toolbar (D-01 top toolbar, D-02 multi-select dropdowns). Renders EVERY\n * facet in `FACET_ORDER` (revised D-05 — a facet with no values renders disabled) 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 return (\n <div role=\"search\" aria-label=\"Filter and search APIs\">\n <div className={styles.toolbar}>\n {facets.map(facet => (\n <FacetDropdown\n key={facet.key}\n facet={facet}\n selected={selected[facet.key]}\n onToggle={value => onToggleFacet(facet.key, value)}\n onClear={() => onClearFacet(facet.key)}\n />\n ))}\n <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__0a37ccc2c0 {\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__0a37ccc2c0 {\n position: relative;\n}\n\n.FilterToolbar_countChip__0a37ccc2c0 {\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__0a37ccc2c0 {\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__0a37ccc2c0 {\n padding: var(--bui-space-1) var(--bui-space-3);\n}\n\n.FilterToolbar_optionLabel__0a37ccc2c0 {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n}\n\n.FilterToolbar_menuFooter__0a37ccc2c0 {\n display: flex;\n justify-content: flex-end;\n padding: var(--bui-space-1) var(--bui-space-2) 0;\n}\n\n.FilterToolbar_searchRow__0a37ccc2c0 {\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__0a37ccc2c0 {\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__0a37ccc2c0","facet":"FilterToolbar_facet__0a37ccc2c0","countChip":"FilterToolbar_countChip__0a37ccc2c0","menu":"FilterToolbar_menu__0a37ccc2c0","option":"FilterToolbar_option__0a37ccc2c0","optionLabel":"FilterToolbar_optionLabel__0a37ccc2c0","menuFooter":"FilterToolbar_menuFooter__0a37ccc2c0","searchRow":"FilterToolbar_searchRow__0a37ccc2c0","srOnly":"FilterToolbar_srOnly__0a37ccc2c0"};
|
|
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":";;;;;;;;"}
|
|
@@ -1,45 +1,17 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import Search from '@material-ui/icons/Search';
|
|
2
|
+
import { SearchField } from '@backstage/ui';
|
|
3
|
+
import { RiSearchLine } from '@remixicon/react';
|
|
5
4
|
|
|
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
5
|
function SearchBar({ value, onChange }) {
|
|
20
|
-
const classes = useStyles();
|
|
21
6
|
return /* @__PURE__ */ jsx(
|
|
22
|
-
|
|
7
|
+
SearchField,
|
|
23
8
|
{
|
|
24
|
-
|
|
25
|
-
variant: "outlined",
|
|
26
|
-
size: "small",
|
|
9
|
+
"aria-label": "Search APIs",
|
|
27
10
|
placeholder: "Search APIs\u2026",
|
|
28
11
|
value,
|
|
29
|
-
onChange
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}
|
|
12
|
+
onChange,
|
|
13
|
+
icon: /* @__PURE__ */ jsx(RiSearchLine, {}),
|
|
14
|
+
style: { flex: "1 1 auto", minWidth: 240 }
|
|
43
15
|
}
|
|
44
16
|
);
|
|
45
17
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchBar.esm.js","sources":["../../src/components/SearchBar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\
|
|
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;;;;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseEntityRef } from '@backstage/catalog-model';
|
|
2
2
|
import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
|
|
3
|
-
import { ANNOTATION_CATEGORY,
|
|
3
|
+
import { ANNOTATION_CATEGORY, ANNOTATION_VERSION, ANNOTATION_PROXY_NAME, ANNOTATION_HAS_SPEC, LABEL_DEPARTMENT, ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants.esm.js';
|
|
4
4
|
|
|
5
5
|
function toBadge(lifecycle) {
|
|
6
6
|
switch (lifecycle) {
|
|
@@ -14,6 +14,18 @@ function toBadge(lifecycle) {
|
|
|
14
14
|
return { label: "Unknown", kind: "warning" };
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
function toVisibilities(annotations) {
|
|
18
|
+
const raw = annotations[ANNOTATION_APIGEE_ORGS] ?? annotations[ANNOTATION_APIGEE_ORG] ?? "";
|
|
19
|
+
const orgs = raw.split(",").map((org) => org.trim().toLowerCase()).filter(Boolean);
|
|
20
|
+
const visibilities = [];
|
|
21
|
+
if (orgs.some((org) => org.includes("private"))) visibilities.push("private");
|
|
22
|
+
if (orgs.some((org) => org.includes("public"))) visibilities.push("public");
|
|
23
|
+
return visibilities;
|
|
24
|
+
}
|
|
25
|
+
function versionFromProxyName(proxyName) {
|
|
26
|
+
const match = proxyName?.match(/-v(\d+)$/i);
|
|
27
|
+
return match ? `${Number(match[1])}.0.0` : void 0;
|
|
28
|
+
}
|
|
17
29
|
function toApiCardViewModel(entity) {
|
|
18
30
|
const annotations = entity.metadata.annotations ?? {};
|
|
19
31
|
const owner = entity.spec?.owner;
|
|
@@ -24,9 +36,11 @@ function toApiCardViewModel(entity) {
|
|
|
24
36
|
parseEntityRef(owner, { defaultKind: "group", defaultNamespace: "default" }),
|
|
25
37
|
{ defaultKind: "group" }
|
|
26
38
|
) : "unknown",
|
|
39
|
+
department: entity.metadata.labels?.[LABEL_DEPARTMENT] || void 0,
|
|
40
|
+
visibilities: toVisibilities(annotations),
|
|
27
41
|
badge: toBadge(entity.spec?.lifecycle),
|
|
28
42
|
hasSpec: annotations[ANNOTATION_HAS_SPEC] === "true",
|
|
29
|
-
version: annotations[
|
|
43
|
+
version: annotations[ANNOTATION_VERSION] || versionFromProxyName(annotations[ANNOTATION_PROXY_NAME]) || void 0,
|
|
30
44
|
category: annotations[ANNOTATION_CATEGORY]?.split(",")[0]?.trim() || void 0,
|
|
31
45
|
description,
|
|
32
46
|
targetRef: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiCardViewModel.esm.js","sources":["../../src/components/apiCardViewModel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\
|
|
1
|
+
{"version":3,"file":"apiCardViewModel.esm.js","sources":["../../src/components/apiCardViewModel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\nimport {\n ANNOTATION_HAS_SPEC,\n ANNOTATION_CATEGORY,\n ANNOTATION_VERSION,\n ANNOTATION_PROXY_NAME,\n ANNOTATION_APIGEE_ORGS,\n ANNOTATION_APIGEE_ORG,\n LABEL_DEPARTMENT,\n} from '../constants';\n\nexport type LifecycleBadgeKind = 'ok' | 'warning' | 'error';\n\n/** Apigee org exposure shown on the card, derived from the org name. */\nexport type ApiVisibility = 'private' | 'public';\n\nexport interface ApiCardViewModel {\n /** metadata.title ?? metadata.name */\n name: string;\n /** humanizeEntityRef(spec.owner, { defaultKind: 'group' }) — 'unknown' when absent */\n ownerLabel: string;\n /** metadata.labels.department — omitted when absent */\n department?: string;\n /** Exposure derived from apigee.com/orgs — 'private', 'public', or both; [] when none. */\n visibilities: ApiVisibility[];\n badge: { label: string; kind: LifecycleBadgeKind };\n /** annotations['apigee.com/has-spec'] === 'true' */\n hasSpec: boolean;\n /** annotations['apigee.com/version'], else the proxy name's -v<major> as x.0.0 — omitted when absent (D-05) */\n version?: string;\n /** first comma-separated slug of apigee.com/category — undefined => 'Other' bucket */\n category?: string;\n /** metadata.description — omitted when empty */\n description?: string;\n /** for entityRouteRef navigation (05-04) */\n targetRef: { kind: 'API'; namespace: string; name: string };\n}\n\n/** D-04 lifecycle -> badge mapping. Unknown/undeployed/missing default to warning. */\nfunction toBadge(lifecycle: string | undefined): {\n label: string;\n kind: LifecycleBadgeKind;\n} {\n switch (lifecycle) {\n case 'production':\n return { label: 'Production', kind: 'ok' };\n case 'deprecated':\n return { label: 'Deprecated', kind: 'error' };\n case 'experimental':\n return { label: 'Experimental', kind: 'warning' };\n default:\n return { label: 'Unknown', kind: 'warning' };\n }\n}\n\n/** Derive exposure from the Apigee org annotation(s). Org names follow\n * `apigee-x-{private|public}-p-<id>`; an API can be published to one or both. */\nfunction toVisibilities(annotations: Record<string, string>): ApiVisibility[] {\n const raw =\n annotations[ANNOTATION_APIGEE_ORGS] ?? annotations[ANNOTATION_APIGEE_ORG] ?? '';\n const orgs = raw\n .split(',')\n .map(org => org.trim().toLowerCase())\n .filter(Boolean);\n const visibilities: ApiVisibility[] = [];\n if (orgs.some(org => org.includes('private'))) visibilities.push('private');\n if (orgs.some(org => org.includes('public'))) visibilities.push('public');\n return visibilities;\n}\n\n/** Extract a semantic version from a proxy name's trailing `-v<major>` segment\n * (e.g. `...-v4` -> `4.0.0`). Undefined when the name has no version suffix. */\nfunction versionFromProxyName(proxyName: string | undefined): string | undefined {\n const match = proxyName?.match(/-v(\\d+)$/i);\n return match ? `${Number(match[1])}.0.0` : undefined;\n}\n\n/** PURE mapper: an Apigee `kind: API` entity -> the view-model each tile renders. */\nexport function toApiCardViewModel(entity: Entity): ApiCardViewModel {\n const annotations = entity.metadata.annotations ?? {};\n const owner = entity.spec?.owner as string | undefined;\n const description = entity.metadata.description?.trim()\n ? entity.metadata.description\n : undefined;\n\n return {\n name: entity.metadata.title ?? entity.metadata.name,\n ownerLabel: owner\n ? humanizeEntityRef(\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\n { defaultKind: 'group' },\n )\n : 'unknown',\n department: entity.metadata.labels?.[LABEL_DEPARTMENT] || undefined,\n visibilities: toVisibilities(annotations),\n badge: toBadge(entity.spec?.lifecycle as string | undefined),\n hasSpec: annotations[ANNOTATION_HAS_SPEC] === 'true',\n version:\n annotations[ANNOTATION_VERSION] ||\n versionFromProxyName(annotations[ANNOTATION_PROXY_NAME]) ||\n undefined,\n category: annotations[ANNOTATION_CATEGORY]?.split(',')[0]?.trim() || undefined,\n description,\n targetRef: {\n kind: 'API',\n namespace: entity.metadata.namespace ?? 'default',\n name: entity.metadata.name,\n },\n };\n}\n"],"names":[],"mappings":";;;;AAyCA,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;AAIA,SAAS,eAAe,WAAA,EAAsD;AAC5E,EAAA,MAAM,MACJ,WAAA,CAAY,sBAAsB,CAAA,IAAK,WAAA,CAAY,qBAAqB,CAAA,IAAK,EAAA;AAC/E,EAAA,MAAM,IAAA,GAAO,GAAA,CACV,KAAA,CAAM,GAAG,EACT,GAAA,CAAI,CAAA,GAAA,KAAO,GAAA,CAAI,IAAA,EAAK,CAAE,WAAA,EAAa,CAAA,CACnC,OAAO,OAAO,CAAA;AACjB,EAAA,MAAM,eAAgC,EAAC;AACvC,EAAA,IAAI,IAAA,CAAK,IAAA,CAAK,CAAA,GAAA,KAAO,GAAA,CAAI,QAAA,CAAS,SAAS,CAAC,CAAA,EAAG,YAAA,CAAa,IAAA,CAAK,SAAS,CAAA;AAC1E,EAAA,IAAI,IAAA,CAAK,IAAA,CAAK,CAAA,GAAA,KAAO,GAAA,CAAI,QAAA,CAAS,QAAQ,CAAC,CAAA,EAAG,YAAA,CAAa,IAAA,CAAK,QAAQ,CAAA;AACxE,EAAA,OAAO,YAAA;AACT;AAIA,SAAS,qBAAqB,SAAA,EAAmD;AAC/E,EAAA,MAAM,KAAA,GAAQ,SAAA,EAAW,KAAA,CAAM,WAAW,CAAA;AAC1C,EAAA,OAAO,QAAQ,CAAA,EAAG,MAAA,CAAO,MAAM,CAAC,CAAC,CAAC,CAAA,IAAA,CAAA,GAAS,MAAA;AAC7C;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,UAAA,EAAY,MAAA,CAAO,QAAA,CAAS,MAAA,GAAS,gBAAgB,CAAA,IAAK,MAAA;AAAA,IAC1D,YAAA,EAAc,eAAe,WAAW,CAAA;AAAA,IACxC,KAAA,EAAO,OAAA,CAAQ,MAAA,CAAO,IAAA,EAAM,SAA+B,CAAA;AAAA,IAC3D,OAAA,EAAS,WAAA,CAAY,mBAAmB,CAAA,KAAM,MAAA;AAAA,IAC9C,OAAA,EACE,YAAY,kBAAkB,CAAA,IAC9B,qBAAqB,WAAA,CAAY,qBAAqB,CAAC,CAAA,IACvD,MAAA;AAAA,IACF,QAAA,EAAU,WAAA,CAAY,mBAAmB,CAAA,EAAG,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,EAAG,IAAA,EAAK,IAAK,MAAA;AAAA,IACrE,WAAA;AAAA,IACA,SAAA,EAAW;AAAA,MACT,IAAA,EAAM,KAAA;AAAA,MACN,SAAA,EAAW,MAAA,CAAO,QAAA,CAAS,SAAA,IAAa,SAAA;AAAA,MACxC,IAAA,EAAM,OAAO,QAAA,CAAS;AAAA;AACxB,GACF;AACF;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apigeeFacetFilters.esm.js","sources":["../../src/components/apigeeFacetFilters.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\
|
|
1
|
+
{"version":3,"file":"apigeeFacetFilters.esm.js","sources":["../../src/components/apigeeFacetFilters.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\nimport type { EntityFilter } from '@backstage/plugin-catalog-react';\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\nimport {\n ANNOTATION_APIGEE_ORGS,\n ANNOTATION_APIGEE_ORG,\n ANNOTATION_CATEGORY,\n LABEL_DEPARTMENT,\n} from '../constants';\n\n/** Dual-key is-Apigee gate (same rule as the existing ApigeeOrgFilter). */\nexport function isApigeeEntity(e: Entity): boolean {\n const a = e.metadata.annotations ?? {};\n return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;\n}\n\n// --- Pure field extractors: the ONE place each facet's value is defined. Reused by\n// deriveFacets(), the facet predicates, and the search haystack. Unit-testable. ---\nexport const entityLifecycle = (e: Entity): string | undefined =>\n (e.spec?.lifecycle as string | undefined)?.trim() || undefined;\n\nexport const entityOwnerLabel = (e: Entity): string | undefined => {\n const owner = e.spec?.owner as string | undefined;\n return owner\n ? humanizeEntityRef(\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\n { defaultKind: 'group' },\n ) // 'group:default/payments-team' -> 'payments-team'\n : undefined;\n};\n\nexport const entityOrg = (e: Entity): string | undefined => {\n const a = e.metadata.annotations ?? {};\n return (\n (a[ANNOTATION_APIGEE_ORGS] ?? a[ANNOTATION_APIGEE_ORG])?.split(',')[0]?.trim() ||\n undefined\n );\n};\n\nexport const entityCategory = (e: Entity): string | undefined =>\n (e.metadata.annotations ?? {})[ANNOTATION_CATEGORY]?.split(',')[0]?.trim() ||\n undefined;\n\n/**\n * Facet-only category value: maps an uncategorised entity to the 'other' bucket\n * (LOCKED UI-SPEC Facet Data). Search uses `entityCategory` (the REAL slug) instead,\n * so a search for \"other\" does not match uncategorised APIs.\n */\nexport const entityCategoryFacet = (e: Entity): string =>\n entityCategory(e) ?? 'other';\n\n/** Department facet value: the `department` label written from the API Hub \"Business unit\". */\nexport const entityDepartment = (e: Entity): string | undefined =>\n (e.metadata.labels ?? {})[LABEL_DEPARTMENT]?.trim() || undefined;\n\n/** Generic single-field multi-value facet. Within-facet OR. No URL seam / getCatalogFilters. */\nexport class FieldFacetFilter implements EntityFilter {\n constructor(\n private readonly extract: (e: Entity) => string | undefined,\n public readonly values: string[],\n ) {}\n filterEntity(entity: Entity): boolean {\n const v = this.extract(entity);\n return v !== undefined && this.values.includes(v); // OR across selected values\n }\n}\n\n/** Broad \"find anything\" search: name + description + owner + org + category (D-09). */\nexport function apiSearchHaystack(e: Entity): string {\n return [\n e.metadata.title ?? e.metadata.name,\n e.metadata.description ?? '',\n entityOwnerLabel(e) ?? '',\n entityOrg(e) ?? '',\n entityCategory(e) ?? '',\n ]\n .join(' ')\n .toLowerCase();\n}\n\nexport class ApiSearchFilter implements EntityFilter {\n private readonly needle: string;\n constructor(query: string) {\n this.needle = query.trim().toLowerCase();\n }\n filterEntity(entity: Entity): boolean {\n // String.includes ONLY — never build a RegExp from user input (ReDoS guard).\n return this.needle === '' || apiSearchHaystack(entity).includes(this.needle);\n }\n}\n\n/**\n * Replaces EntityKindFilter('api','API'): keeps the backend kind=API filter but exposes no\n * URL seam, so the provider does not serialize `filters[kind]=API` into the URL (D-06).\n */\nexport class ApiKindFilter implements EntityFilter {\n readonly value = 'API';\n getCatalogFilters(): Record<string, string> {\n return { kind: this.value };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apigeeOrgFilter.esm.js","sources":["../../src/components/apigeeOrgFilter.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\
|
|
1
|
+
{"version":3,"file":"apigeeOrgFilter.esm.js","sources":["../../src/components/apigeeOrgFilter.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport type { Entity } from '@backstage/catalog-model';\nimport type { EntityFilter } from '@backstage/plugin-catalog-react';\nimport { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants';\n\n/**\n * Frontend predicate that keeps an entity when it carries EITHER Apigee org key.\n * MUST match both: proxy-derived APIs use the plural key, hub-only APIs the singular\n * (RESEARCH Pitfall 1 / CAT-05). Uses filterEntity (not getCatalogFilters) because a\n * single backend filter record cannot OR two different annotation keys.\n */\nexport class ApigeeOrgFilter implements EntityFilter {\n filterEntity(entity: Entity): boolean {\n const a = entity.metadata.annotations ?? {};\n return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;\n }\n}\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;;;;"}
|
|
@@ -16,6 +16,19 @@ function parseSearchParams(sp) {
|
|
|
16
16
|
function isActive(state) {
|
|
17
17
|
return state.q.trim() !== "" || FACET_ORDER.some((k) => state.selected[k].length > 0);
|
|
18
18
|
}
|
|
19
|
+
function selectOnlyFacetParams(prev, key, value) {
|
|
20
|
+
const current = prev.getAll(QKEY[key]);
|
|
21
|
+
const isSoleSelection = current.length === 1 && current[0] === value && FACET_ORDER.every((k) => k === key || prev.getAll(QKEY[k]).length === 0);
|
|
22
|
+
const next = new URLSearchParams();
|
|
23
|
+
const q = prev.get("q");
|
|
24
|
+
if (q) {
|
|
25
|
+
next.set("q", q);
|
|
26
|
+
}
|
|
27
|
+
if (!isSoleSelection) {
|
|
28
|
+
next.append(QKEY[key], value);
|
|
29
|
+
}
|
|
30
|
+
return next;
|
|
31
|
+
}
|
|
19
32
|
|
|
20
|
-
export { isActive, parseSearchParams };
|
|
33
|
+
export { isActive, parseSearchParams, selectOnlyFacetParams };
|
|
21
34
|
//# sourceMappingURL=apigeeSearchParams.esm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apigeeSearchParams.esm.js","sources":["../../src/components/apigeeSearchParams.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\
|
|
1
|
+
{"version":3,"file":"apigeeSearchParams.esm.js","sources":["../../src/components/apigeeSearchParams.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { FACET_ORDER, type FacetKey } from './facetModel';\n\nexport interface ApigeeFilterState {\n selected: Record<FacetKey, string[]>;\n q: string;\n}\n\n/** Clean URL keys (D-06): one query key per facet, no native `filters[...]` wrapper. */\nconst QKEY: Record<FacetKey, string> = {\n lifecycle: 'lifecycle',\n org: 'org',\n owner: 'owner',\n category: 'category',\n department: 'department',\n};\n\n/**\n * URL → state. Repeated keys become arrays (D-07) via `getAll`; unknown/extra keys are\n * ignored. Values are treated as opaque strings — no eval, RegExp, or HTML (T-06-02-01);\n * an unknown facet value simply matches nothing downstream (derived allow-list).\n */\nexport function parseSearchParams(sp: URLSearchParams): ApigeeFilterState {\n const selected = Object.fromEntries(\n FACET_ORDER.map(k => [k, sp.getAll(QKEY[k])]),\n ) as Record<FacetKey, string[]>;\n return { selected, q: sp.get('q') ?? '' };\n}\n\n/**\n * state → URL. Deterministic order (FACET_ORDER, then `q` last) reproduces the exact SC #3\n * shape `?lifecycle=deprecated&q=payment`. Only the known clean keys are appended and empties\n * are omitted (T-06-02-02) — no injection surface. Multi-value facets serialize as repeated\n * keys (D-07) via `append`.\n */\nexport function buildSearchParams(state: ApigeeFilterState): URLSearchParams {\n const sp = new URLSearchParams();\n for (const k of FACET_ORDER) {\n for (const v of state.selected[k]) {\n sp.append(QKEY[k], v);\n }\n }\n if (state.q.trim()) {\n sp.set('q', state.q.trim());\n }\n return sp;\n}\n\n/** True when any facet is selected or the search box holds a non-blank query. */\nexport function isActive(state: ApigeeFilterState): boolean {\n return (\n state.q.trim() !== '' || FACET_ORDER.some(k => state.selected[k].length > 0)\n );\n}\n\n/**\n * Single-select URL update for the count cards: the clicked value becomes the ONLY active facet\n * selection (every other facet is cleared), so each card click filters to exactly that value\n * instead of accumulating like the toolbar's multi-select toggle. Clicking the current sole\n * selection clears it. The `q` search query is always preserved.\n */\nexport function selectOnlyFacetParams(\n prev: URLSearchParams,\n key: FacetKey,\n value: string,\n): URLSearchParams {\n const current = prev.getAll(QKEY[key]);\n const isSoleSelection =\n current.length === 1 &&\n current[0] === value &&\n FACET_ORDER.every(k => k === key || prev.getAll(QKEY[k]).length === 0);\n const next = new URLSearchParams();\n const q = prev.get('q');\n if (q) {\n next.set('q', q);\n }\n if (!isSoleSelection) {\n next.append(QKEY[key], value);\n }\n return next;\n}\n"],"names":[],"mappings":";;AASA,MAAM,IAAA,GAAiC;AAAA,EACrC,SAAA,EAAW,WAAA;AAAA,EACX,GAAA,EAAK,KAAA;AAAA,EACL,KAAA,EAAO,OAAA;AAAA,EACP,QAAA,EAAU,UAAA;AAAA,EACV,UAAA,EAAY;AACd,CAAA;AAOO,SAAS,kBAAkB,EAAA,EAAwC;AACxE,EAAA,MAAM,WAAW,MAAA,CAAO,WAAA;AAAA,IACtB,WAAA,CAAY,GAAA,CAAI,CAAA,CAAA,KAAK,CAAC,CAAA,EAAG,EAAA,CAAG,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAC,CAAC;AAAA,GAC9C;AACA,EAAA,OAAO,EAAE,QAAA,EAAU,CAAA,EAAG,GAAG,GAAA,CAAI,GAAG,KAAK,EAAA,EAAG;AAC1C;AAsBO,SAAS,SAAS,KAAA,EAAmC;AAC1D,EAAA,OACE,KAAA,CAAM,CAAA,CAAE,IAAA,EAAK,KAAM,EAAA,IAAM,WAAA,CAAY,IAAA,CAAK,CAAA,CAAA,KAAK,KAAA,CAAM,QAAA,CAAS,CAAC,CAAA,CAAE,SAAS,CAAC,CAAA;AAE/E;AAQO,SAAS,qBAAA,CACd,IAAA,EACA,GAAA,EACA,KAAA,EACiB;AACjB,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAC,CAAA;AACrC,EAAA,MAAM,eAAA,GACJ,QAAQ,MAAA,KAAW,CAAA,IACnB,QAAQ,CAAC,CAAA,KAAM,SACf,WAAA,CAAY,KAAA,CAAM,OAAK,CAAA,KAAM,GAAA,IAAO,KAAK,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAA,CAAE,WAAW,CAAC,CAAA;AACvE,EAAA,MAAM,IAAA,GAAO,IAAI,eAAA,EAAgB;AACjC,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AACtB,EAAA,IAAI,CAAA,EAAG;AACL,IAAA,IAAA,CAAK,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EACjB;AACA,EAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAAG,KAAK,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,IAAA;AACT;;;;"}
|
|
@@ -1,27 +1,17 @@
|
|
|
1
|
-
import
|
|
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';
|
|
1
|
+
import { RiMapPinLine, RiTvLine, RiSmartphoneLine, RiRouterLine, RiBarChartBoxLine, RiNotification3Line, RiShoppingCartLine, RiTeamLine, RiBankCardLine, RiShieldKeyholeLine, RiAppsLine } from '@remixicon/react';
|
|
12
2
|
|
|
13
3
|
const ICONS = {
|
|
14
|
-
Category,
|
|
15
|
-
Security,
|
|
16
|
-
Payment,
|
|
17
|
-
People,
|
|
18
|
-
ShoppingCart,
|
|
19
|
-
Notifications,
|
|
20
|
-
Assessment,
|
|
21
|
-
SettingsEthernet,
|
|
22
|
-
Devices,
|
|
23
|
-
Tv,
|
|
24
|
-
Place
|
|
4
|
+
Category: RiAppsLine,
|
|
5
|
+
Security: RiShieldKeyholeLine,
|
|
6
|
+
Payment: RiBankCardLine,
|
|
7
|
+
People: RiTeamLine,
|
|
8
|
+
ShoppingCart: RiShoppingCartLine,
|
|
9
|
+
Notifications: RiNotification3Line,
|
|
10
|
+
Assessment: RiBarChartBoxLine,
|
|
11
|
+
SettingsEthernet: RiRouterLine,
|
|
12
|
+
Devices: RiSmartphoneLine,
|
|
13
|
+
Tv: RiTvLine,
|
|
14
|
+
Place: RiMapPinLine
|
|
25
15
|
};
|
|
26
16
|
const DEFAULT_MAP = [
|
|
27
17
|
{ match: /security|auth|identity|oauth/i, key: "Security" },
|