@nospt/backstage-plugin-apigee 0.1.0
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.
- package/README.md +19 -0
- package/dist/components/ApiCard.esm.js +146 -0
- package/dist/components/ApiCard.esm.js.map +1 -0
- package/dist/components/ApiCardGrid.esm.js +56 -0
- package/dist/components/ApiCardGrid.esm.js.map +1 -0
- package/dist/components/ApigeeApiCatalogPage.esm.js +139 -0
- package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -0
- package/dist/components/FilterToolbar.esm.js +197 -0
- package/dist/components/FilterToolbar.esm.js.map +1 -0
- package/dist/components/SearchBar.esm.js +48 -0
- package/dist/components/SearchBar.esm.js.map +1 -0
- package/dist/components/apiCardViewModel.esm.js +41 -0
- package/dist/components/apiCardViewModel.esm.js.map +1 -0
- package/dist/components/apigeeFacetFilters.esm.js +61 -0
- package/dist/components/apigeeFacetFilters.esm.js.map +1 -0
- package/dist/components/apigeeOrgFilter.esm.js +11 -0
- package/dist/components/apigeeOrgFilter.esm.js.map +1 -0
- package/dist/components/apigeeSearchParams.esm.js +20 -0
- package/dist/components/apigeeSearchParams.esm.js.map +1 -0
- package/dist/components/categoryIcons.esm.js +63 -0
- package/dist/components/categoryIcons.esm.js.map +1 -0
- package/dist/components/facetModel.esm.js +78 -0
- package/dist/components/facetModel.esm.js.map +1 -0
- package/dist/components/grouping.esm.js +31 -0
- package/dist/components/grouping.esm.js.map +1 -0
- package/dist/components/states.esm.js +95 -0
- package/dist/components/states.esm.js.map +1 -0
- package/dist/components/useApigeeFilters.esm.js +86 -0
- package/dist/components/useApigeeFilters.esm.js.map +1 -0
- package/dist/components/useDebouncedValue.esm.js +13 -0
- package/dist/components/useDebouncedValue.esm.js.map +1 -0
- package/dist/constants.esm.js +8 -0
- package/dist/constants.esm.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.esm.js +2 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/plugin.esm.js +33 -0
- package/dist/plugin.esm.js.map +1 -0
- package/dist/routes.esm.js +6 -0
- package/dist/routes.esm.js.map +1 -0
- package/package.json +89 -0
|
@@ -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,61 @@
|
|
|
1
|
+
import { parseEntityRef } from '@backstage/catalog-model';
|
|
2
|
+
import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
|
|
3
|
+
import { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG, 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
|
+
class FieldFacetFilter {
|
|
24
|
+
constructor(extract, values) {
|
|
25
|
+
this.extract = extract;
|
|
26
|
+
this.values = values;
|
|
27
|
+
}
|
|
28
|
+
extract;
|
|
29
|
+
values;
|
|
30
|
+
filterEntity(entity) {
|
|
31
|
+
const v = this.extract(entity);
|
|
32
|
+
return v !== void 0 && this.values.includes(v);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function apiSearchHaystack(e) {
|
|
36
|
+
return [
|
|
37
|
+
e.metadata.title ?? e.metadata.name,
|
|
38
|
+
e.metadata.description ?? "",
|
|
39
|
+
entityOwnerLabel(e) ?? "",
|
|
40
|
+
entityOrg(e) ?? "",
|
|
41
|
+
entityCategory(e) ?? ""
|
|
42
|
+
].join(" ").toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
class ApiSearchFilter {
|
|
45
|
+
needle;
|
|
46
|
+
constructor(query) {
|
|
47
|
+
this.needle = query.trim().toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
filterEntity(entity) {
|
|
50
|
+
return this.needle === "" || apiSearchHaystack(entity).includes(this.needle);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
class ApiKindFilter {
|
|
54
|
+
value = "api";
|
|
55
|
+
getCatalogFilters() {
|
|
56
|
+
return { kind: this.value };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { ApiKindFilter, ApiSearchFilter, FieldFacetFilter, apiSearchHaystack, entityCategory, entityCategoryFacet, entityLifecycle, entityOrg, entityOwnerLabel, isApigeeEntity };
|
|
61
|
+
//# 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} 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/** 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":";;;;AAWO,SAAS,eAAe,CAAA,EAAoB;AACjD,EAAA,MAAM,CAAA,GAAI,CAAA,CAAE,QAAA,CAAS,WAAA,IAAe,EAAC;AACrC,EAAA,OAAO,sBAAA,IAA0B,KAAK,qBAAA,IAAyB,CAAA;AACjE;AAIO,MAAM,kBAAkB,CAAC,CAAA,KAC7B,EAAE,IAAA,EAAM,SAAA,EAAkC,MAAK,IAAK;AAEhD,MAAM,gBAAA,GAAmB,CAAC,CAAA,KAAkC;AACjE,EAAA,MAAM,KAAA,GAAQ,EAAE,IAAA,EAAM,KAAA;AACtB,EAAA,OAAO,KAAA,GACH,iBAAA;AAAA,IACE,eAAe,KAAA,EAAO,EAAE,aAAa,OAAA,EAAS,gBAAA,EAAkB,WAAW,CAAA;AAAA,IAC3E,EAAE,aAAa,OAAA;AAAQ,GACzB,GACA,MAAA;AACN;AAEO,MAAM,SAAA,GAAY,CAAC,CAAA,KAAkC;AAC1D,EAAA,MAAM,CAAA,GAAI,CAAA,CAAE,QAAA,CAAS,WAAA,IAAe,EAAC;AACrC,EAAA,OAAA,CACG,CAAA,CAAE,sBAAsB,CAAA,IAAK,CAAA,CAAE,qBAAqB,CAAA,GAAI,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,EAAG,IAAA,EAAK,IAC7E,MAAA;AAEJ;AAEO,MAAM,iBAAiB,CAAC,CAAA,KAAA,CAC5B,CAAA,CAAE,QAAA,CAAS,eAAe,EAAC,EAAG,mBAAmB,CAAA,EAAG,MAAM,GAAG,CAAA,CAAE,CAAC,CAAA,EAAG,MAAK,IACzE;AAOK,MAAM,mBAAA,GAAsB,CAAC,CAAA,KAClC,cAAA,CAAe,CAAC,CAAA,IAAK;AAGhB,MAAM,gBAAA,CAAyC;AAAA,EACpD,WAAA,CACmB,SACD,MAAA,EAChB;AAFiB,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AACD,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EACf;AAAA,EAFgB,OAAA;AAAA,EACD,MAAA;AAAA,EAElB,aAAa,MAAA,EAAyB;AACpC,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,OAAA,CAAQ,MAAM,CAAA;AAC7B,IAAA,OAAO,CAAA,KAAM,MAAA,IAAa,IAAA,CAAK,MAAA,CAAO,SAAS,CAAC,CAAA;AAAA,EAClD;AACF;AAGO,SAAS,kBAAkB,CAAA,EAAmB;AACnD,EAAA,OAAO;AAAA,IACL,CAAA,CAAE,QAAA,CAAS,KAAA,IAAS,CAAA,CAAE,QAAA,CAAS,IAAA;AAAA,IAC/B,CAAA,CAAE,SAAS,WAAA,IAAe,EAAA;AAAA,IAC1B,gBAAA,CAAiB,CAAC,CAAA,IAAK,EAAA;AAAA,IACvB,SAAA,CAAU,CAAC,CAAA,IAAK,EAAA;AAAA,IAChB,cAAA,CAAe,CAAC,CAAA,IAAK;AAAA,GACvB,CACG,IAAA,CAAK,GAAG,CAAA,CACR,WAAA,EAAY;AACjB;AAEO,MAAM,eAAA,CAAwC;AAAA,EAClC,MAAA;AAAA,EACjB,YAAY,KAAA,EAAe;AACzB,IAAA,IAAA,CAAK,MAAA,GAAS,KAAA,CAAM,IAAA,EAAK,CAAE,WAAA,EAAY;AAAA,EACzC;AAAA,EACA,aAAa,MAAA,EAAyB;AAEpC,IAAA,OAAO,IAAA,CAAK,WAAW,EAAA,IAAM,iBAAA,CAAkB,MAAM,CAAA,CAAE,QAAA,CAAS,KAAK,MAAM,CAAA;AAAA,EAC7E;AACF;AAMO,MAAM,aAAA,CAAsC;AAAA,EACxC,KAAA,GAAQ,KAAA;AAAA,EACjB,iBAAA,GAA4C;AAC1C,IAAA,OAAO,EAAE,IAAA,EAAM,IAAA,CAAK,KAAA,EAAM;AAAA,EAC5B;AACF;;;;"}
|
|
@@ -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,20 @@
|
|
|
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
|
+
};
|
|
9
|
+
function parseSearchParams(sp) {
|
|
10
|
+
const selected = Object.fromEntries(
|
|
11
|
+
FACET_ORDER.map((k) => [k, sp.getAll(QKEY[k])])
|
|
12
|
+
);
|
|
13
|
+
return { selected, q: sp.get("q") ?? "" };
|
|
14
|
+
}
|
|
15
|
+
function isActive(state) {
|
|
16
|
+
return state.q.trim() !== "" || FACET_ORDER.some((k) => state.selected[k].length > 0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { isActive, parseSearchParams };
|
|
20
|
+
//# 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};\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;AACZ,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,78 @@
|
|
|
1
|
+
import { resolveCategoryIcon } from './categoryIcons.esm.js';
|
|
2
|
+
import { humanise } from './grouping.esm.js';
|
|
3
|
+
import { entityCategoryFacet, entityOwnerLabel, entityOrg, entityLifecycle } from './apigeeFacetFilters.esm.js';
|
|
4
|
+
|
|
5
|
+
const FACET_ORDER = ["lifecycle", "org", "owner", "category"];
|
|
6
|
+
const LIFECYCLE_SEVERITY = ["production", "experimental", "deprecated", "unknown"];
|
|
7
|
+
const FACET_TRIGGER_LABEL = {
|
|
8
|
+
lifecycle: "Lifecycle",
|
|
9
|
+
org: "Org",
|
|
10
|
+
owner: "Owner",
|
|
11
|
+
category: "Category"
|
|
12
|
+
};
|
|
13
|
+
const EXTRACT = {
|
|
14
|
+
lifecycle: entityLifecycle,
|
|
15
|
+
org: entityOrg,
|
|
16
|
+
owner: entityOwnerLabel,
|
|
17
|
+
category: entityCategoryFacet
|
|
18
|
+
};
|
|
19
|
+
function labelFor(key, value) {
|
|
20
|
+
switch (key) {
|
|
21
|
+
case "lifecycle":
|
|
22
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
23
|
+
case "owner":
|
|
24
|
+
return value;
|
|
25
|
+
case "category":
|
|
26
|
+
return value === "other" ? "Other" : humanise(value);
|
|
27
|
+
case "org":
|
|
28
|
+
default:
|
|
29
|
+
return humanise(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function sortValues(key, values) {
|
|
33
|
+
if (key === "lifecycle") {
|
|
34
|
+
const rank = (v) => {
|
|
35
|
+
const i = LIFECYCLE_SEVERITY.indexOf(v);
|
|
36
|
+
return i === -1 ? LIFECYCLE_SEVERITY.length : i;
|
|
37
|
+
};
|
|
38
|
+
return [...values].sort((a, b) => rank(a) - rank(b));
|
|
39
|
+
}
|
|
40
|
+
if (key === "category") {
|
|
41
|
+
return [...values].sort((a, b) => {
|
|
42
|
+
if (a === "other") return 1;
|
|
43
|
+
if (b === "other") return -1;
|
|
44
|
+
return a.localeCompare(b);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return [...values].sort((a, b) => a.localeCompare(b));
|
|
48
|
+
}
|
|
49
|
+
function deriveFacets(entities, overrides) {
|
|
50
|
+
return FACET_ORDER.map((key) => {
|
|
51
|
+
const distinct = Array.from(
|
|
52
|
+
new Set(entities.map(EXTRACT[key]).filter((v) => !!v))
|
|
53
|
+
);
|
|
54
|
+
const options = sortValues(key, distinct).map((value) => ({
|
|
55
|
+
value,
|
|
56
|
+
label: labelFor(key, value)
|
|
57
|
+
}));
|
|
58
|
+
const icons = key === "category" ? Object.fromEntries(
|
|
59
|
+
options.map((opt) => [
|
|
60
|
+
opt.value,
|
|
61
|
+
resolveCategoryIcon(
|
|
62
|
+
opt.value === "other" ? void 0 : opt.value,
|
|
63
|
+
overrides
|
|
64
|
+
)
|
|
65
|
+
])
|
|
66
|
+
) : void 0;
|
|
67
|
+
return {
|
|
68
|
+
key,
|
|
69
|
+
label: FACET_TRIGGER_LABEL[key],
|
|
70
|
+
options,
|
|
71
|
+
enabled: options.length > 0,
|
|
72
|
+
...icons !== void 0 ? { icons } : {}
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { FACET_ORDER, deriveFacets };
|
|
78
|
+
//# sourceMappingURL=facetModel.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facetModel.esm.js","sources":["../../src/components/facetModel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport type { Entity } from '@backstage/catalog-model';\r\nimport type { SvgIconComponent } from '@material-ui/icons';\r\nimport { resolveCategoryIcon } from './categoryIcons';\r\nimport { humanise } from './grouping';\r\nimport {\r\n entityCategoryFacet,\r\n entityLifecycle,\r\n entityOrg,\r\n entityOwnerLabel,\r\n} from './apigeeFacetFilters';\r\n\r\nexport type FacetKey = 'lifecycle' | 'org' | 'owner' | 'category';\r\n\r\nexport interface FacetOption {\r\n value: string;\r\n label: string;\r\n}\r\n\r\nexport interface DerivedFacet {\r\n key: FacetKey;\r\n label: string;\r\n options: FacetOption[];\r\n /**\r\n * True when the facet has at least one selectable value. The toolbar ALWAYS renders every\r\n * facet (revised D-05); an `enabled: false` facet (no values in the loaded data) renders as a\r\n * disabled dropdown so all four filters stay visible.\r\n */\r\n enabled: boolean;\r\n /**\r\n * Per-option icon map — present ONLY on the `category` facet (UI-10).\r\n * Keys are option values (e.g. `'payments'`, `'other'`); values are allow-listed\r\n * MUI icon components resolved via `resolveCategoryIcon`.\r\n * Not present on lifecycle / org / owner facets.\r\n */\r\n icons?: Record<string, SvgIconComponent>;\r\n}\r\n\r\n/** Locked facet order (D-02): Lifecycle → Org → Owner → Category. */\r\nexport const FACET_ORDER: FacetKey[] = ['lifecycle', 'org', 'owner', 'category'];\r\n\r\n/**\r\n * Lifecycle options are ordered by severity (most common / least risky first), NOT\r\n * alphabetically (UI-SPEC Assumption 5). Unknown values fall to the end via `indexOf` = -1\r\n * being handled below.\r\n */\r\nconst LIFECYCLE_SEVERITY = ['production', 'experimental', 'deprecated', 'unknown'];\r\n\r\n/** Fixed facet trigger labels (UI-SPEC §Component Inventory — order-locked). */\r\nconst FACET_TRIGGER_LABEL: Record<FacetKey, string> = {\r\n lifecycle: 'Lifecycle',\r\n org: 'Org',\r\n owner: 'Owner',\r\n category: 'Category',\r\n};\r\n\r\n/**\r\n * The single value extractor per facet slot. Category uses `entityCategoryFacet`\r\n * (undefined → 'other') so uncategorised APIs are selectable; the others reuse the\r\n * 06-01 extractors verbatim.\r\n */\r\nconst EXTRACT: Record<FacetKey, (e: Entity) => string | undefined> = {\r\n lifecycle: entityLifecycle,\r\n org: entityOrg,\r\n owner: entityOwnerLabel,\r\n category: entityCategoryFacet,\r\n};\r\n\r\n/**\r\n * Per-facet option label (LOCKED — UI-SPEC §Facet Data + §Assumption 5):\r\n * - lifecycle → capitalise first char ('production' → 'Production')\r\n * - owner → value AS-IS (already humanised by `entityOwnerLabel`; do NOT humanise again)\r\n * - category → humanise(value), except 'other' → 'Other'\r\n * - org → humanise(value) ('apigee-x-demo' → 'Apigee X Demo')\r\n */\r\nfunction labelFor(key: FacetKey, value: string): string {\r\n switch (key) {\r\n case 'lifecycle':\r\n return value.charAt(0).toUpperCase() + value.slice(1);\r\n case 'owner':\r\n return value;\r\n case 'category':\r\n return value === 'other' ? 'Other' : humanise(value);\r\n case 'org':\r\n default:\r\n return humanise(value);\r\n }\r\n}\r\n\r\n/**\r\n * Sort distinct facet values:\r\n * - lifecycle by fixed severity order\r\n * - category alphabetically with 'other' forced LAST (mirrors the grouped view)\r\n * - org / owner alphabetically (localeCompare)\r\n */\r\nfunction sortValues(key: FacetKey, values: string[]): string[] {\r\n if (key === 'lifecycle') {\r\n // Unrecognized lifecycles sort to the END, not the front (indexOf === -1 would\r\n // otherwise rank them before 'production'). Matches the documented intent.\r\n const rank = (v: string) => {\r\n const i = LIFECYCLE_SEVERITY.indexOf(v);\r\n return i === -1 ? LIFECYCLE_SEVERITY.length : i;\r\n };\r\n return [...values].sort((a, b) => rank(a) - rank(b));\r\n }\r\n if (key === 'category') {\r\n return [...values].sort((a, b) => {\r\n if (a === 'other') return 1;\r\n if (b === 'other') return -1;\r\n return a.localeCompare(b);\r\n });\r\n }\r\n return [...values].sort((a, b) => a.localeCompare(b));\r\n}\r\n\r\n/**\r\n * Derive stable, labelled, sorted facets from the FULL loaded entity set. Every facet in\r\n * FACET_ORDER is ALWAYS returned (revised D-05): `enabled` is true when the facet has >= 1\r\n * distinct value and false when the data has none — the toolbar renders a disabled dropdown for\r\n * the latter so all four filters stay visible even with uniform data. The caller passes the\r\n * pre-facet universe (`backendEntities.filter(isApigeeEntity)`) so options do not vanish as the\r\n * grid narrows.\r\n */\r\nexport function deriveFacets(\r\n entities: Entity[],\r\n overrides?: Record<string, string>,\r\n): DerivedFacet[] {\r\n return FACET_ORDER.map(key => {\r\n const distinct = Array.from(\r\n new Set(entities.map(EXTRACT[key]).filter((v): v is string => !!v)),\r\n );\r\n const options = sortValues(key, distinct).map(value => ({\r\n value,\r\n label: labelFor(key, value),\r\n }));\r\n const icons: Record<string, SvgIconComponent> | undefined =\r\n key === 'category'\r\n ? Object.fromEntries(\r\n options.map(opt => [\r\n opt.value,\r\n resolveCategoryIcon(\r\n opt.value === 'other' ? undefined : opt.value,\r\n overrides,\r\n ),\r\n ]),\r\n )\r\n : undefined;\r\n return {\r\n key,\r\n label: FACET_TRIGGER_LABEL[key],\r\n options,\r\n enabled: options.length > 0,\r\n ...(icons !== undefined ? { icons } : {}),\r\n };\r\n });\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function humanise(slug) {
|
|
2
|
+
return slug.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
3
|
+
}
|
|
4
|
+
function groupByCategory(items) {
|
|
5
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
const key = item.category?.trim() || "other";
|
|
8
|
+
const existing = buckets.get(key);
|
|
9
|
+
if (existing) {
|
|
10
|
+
existing.push(item);
|
|
11
|
+
} else {
|
|
12
|
+
buckets.set(key, [item]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const groups = Array.from(buckets.entries()).map(
|
|
16
|
+
([key, groupItems]) => ({
|
|
17
|
+
key,
|
|
18
|
+
label: key === "other" ? "Other" : humanise(key),
|
|
19
|
+
items: groupItems
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
groups.sort((a, b) => {
|
|
23
|
+
if (a.key === "other") return 1;
|
|
24
|
+
if (b.key === "other") return -1;
|
|
25
|
+
return a.label.localeCompare(b.label);
|
|
26
|
+
});
|
|
27
|
+
return groups;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { groupByCategory, humanise };
|
|
31
|
+
//# sourceMappingURL=grouping.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grouping.esm.js","sources":["../../src/components/grouping.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\n\r\nexport interface CategoryGroup<T> {\r\n key: string;\r\n label: string;\r\n items: T[];\r\n}\r\n\r\n/** Humanise a slug: split on `-`/`_`, drop empties, capitalise each word. */\r\nexport function humanise(slug: string): string {\r\n return slug\r\n .split(/[-_]/)\r\n .filter(Boolean)\r\n .map(word => word.charAt(0).toUpperCase() + word.slice(1))\r\n .join(' ');\r\n}\r\n\r\n/**\r\n * Bucket items by their optional `category`, humanise the label, sort groups\r\n * alphabetically, and always place the 'Other' bucket (uncategorised) last.\r\n * Generic over any `{ category?: string }` — no view-model coupling (UI-11 / D-06).\r\n */\r\nexport function groupByCategory<T extends { category?: string }>(\r\n items: T[],\r\n): CategoryGroup<T>[] {\r\n const buckets = new Map<string, T[]>();\r\n for (const item of items) {\r\n const key = item.category?.trim() || 'other';\r\n const existing = buckets.get(key);\r\n if (existing) {\r\n existing.push(item);\r\n } else {\r\n buckets.set(key, [item]);\r\n }\r\n }\r\n\r\n const groups: CategoryGroup<T>[] = Array.from(buckets.entries()).map(\r\n ([key, groupItems]) => ({\r\n key,\r\n label: key === 'other' ? 'Other' : humanise(key),\r\n items: groupItems,\r\n }),\r\n );\r\n\r\n groups.sort((a, b) => {\r\n if (a.key === 'other') return 1;\r\n if (b.key === 'other') return -1;\r\n return a.label.localeCompare(b.label);\r\n });\r\n\r\n return groups;\r\n}\r\n"],"names":[],"mappings":"AASO,SAAS,SAAS,IAAA,EAAsB;AAC7C,EAAA,OAAO,IAAA,CACJ,MAAM,MAAM,CAAA,CACZ,OAAO,OAAO,CAAA,CACd,IAAI,CAAA,IAAA,KAAQ,IAAA,CAAK,OAAO,CAAC,CAAA,CAAE,aAAY,GAAI,IAAA,CAAK,MAAM,CAAC,CAAC,CAAA,CACxD,IAAA,CAAK,GAAG,CAAA;AACb;AAOO,SAAS,gBACd,KAAA,EACoB;AACpB,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAiB;AACrC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,QAAA,EAAU,IAAA,EAAK,IAAK,OAAA;AACrC,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,GAAG,CAAA;AAChC,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,QAAA,CAAS,KAAK,IAAI,CAAA;AAAA,IACpB,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,CAAC,IAAI,CAAC,CAAA;AAAA,IACzB;AAAA,EACF;AAEA,EAAA,MAAM,SAA6B,KAAA,CAAM,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,CAAA,CAAE,GAAA;AAAA,IAC/D,CAAC,CAAC,GAAA,EAAK,UAAU,CAAA,MAAO;AAAA,MACtB,GAAA;AAAA,MACA,KAAA,EAAO,GAAA,KAAQ,OAAA,GAAU,OAAA,GAAU,SAAS,GAAG,CAAA;AAAA,MAC/C,KAAA,EAAO;AAAA,KACT;AAAA,GACF;AAEA,EAAA,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM;AACpB,IAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,OAAA,EAAS,OAAO,CAAA;AAC9B,IAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,OAAA,EAAS,OAAO,EAAA;AAC9B,IAAA,OAAO,CAAA,CAAE,KAAA,CAAM,aAAA,CAAc,CAAA,CAAE,KAAK,CAAA;AAAA,EACtC,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;;;"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { ResponseErrorPanel, ItemCardGrid, EmptyState, LinkButton } from '@backstage/core-components';
|
|
3
|
+
import { Box, Button, makeStyles } from '@material-ui/core';
|
|
4
|
+
|
|
5
|
+
const useStyles = makeStyles((theme) => ({
|
|
6
|
+
srOnly: {
|
|
7
|
+
position: "absolute",
|
|
8
|
+
width: 1,
|
|
9
|
+
height: 1,
|
|
10
|
+
padding: 0,
|
|
11
|
+
margin: -1,
|
|
12
|
+
overflow: "hidden",
|
|
13
|
+
clip: "rect(0 0 0 0)",
|
|
14
|
+
whiteSpace: "nowrap",
|
|
15
|
+
border: 0
|
|
16
|
+
},
|
|
17
|
+
card: {
|
|
18
|
+
minHeight: 172,
|
|
19
|
+
padding: theme.spacing(2),
|
|
20
|
+
borderRadius: theme.shape.borderRadius,
|
|
21
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
22
|
+
backgroundColor: theme.palette.background.paper
|
|
23
|
+
},
|
|
24
|
+
bar: {
|
|
25
|
+
backgroundColor: theme.palette.action.hover,
|
|
26
|
+
borderRadius: theme.shape.borderRadius,
|
|
27
|
+
animation: "$pulse 1.5s ease-in-out infinite",
|
|
28
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
29
|
+
animation: "none"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
titleBar: { height: 20, width: "70%", marginBottom: theme.spacing(2) },
|
|
33
|
+
metaBar: { height: 12, width: "50%", marginBottom: theme.spacing(1) },
|
|
34
|
+
badgeBar: { height: 16, width: "30%", marginTop: theme.spacing(2) },
|
|
35
|
+
"@keyframes pulse": {
|
|
36
|
+
"0%, 100%": { opacity: 1 },
|
|
37
|
+
"50%": { opacity: 0.4 }
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
function SkeletonGrid({ count = 8 } = {}) {
|
|
41
|
+
const classes = useStyles();
|
|
42
|
+
return /* @__PURE__ */ jsxs("div", { "aria-busy": "true", children: [
|
|
43
|
+
/* @__PURE__ */ jsx("span", { className: classes.srOnly, "aria-live": "polite", children: "Loading Apigee APIs\u2026" }),
|
|
44
|
+
/* @__PURE__ */ jsx(ItemCardGrid, { children: Array.from({ length: count }).map((_, index) => /* @__PURE__ */ jsxs(
|
|
45
|
+
Box,
|
|
46
|
+
{
|
|
47
|
+
className: classes.card,
|
|
48
|
+
"aria-hidden": "true",
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsx(Box, { className: `${classes.bar} ${classes.titleBar}` }),
|
|
51
|
+
/* @__PURE__ */ jsx(Box, { className: `${classes.bar} ${classes.metaBar}` }),
|
|
52
|
+
/* @__PURE__ */ jsx(Box, { className: `${classes.bar} ${classes.metaBar}` }),
|
|
53
|
+
/* @__PURE__ */ jsx(Box, { className: `${classes.bar} ${classes.badgeBar}` })
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
`apigee-skeleton-${index}`
|
|
57
|
+
)) })
|
|
58
|
+
] });
|
|
59
|
+
}
|
|
60
|
+
function ApiCatalogEmptyState() {
|
|
61
|
+
return /* @__PURE__ */ jsx(
|
|
62
|
+
EmptyState,
|
|
63
|
+
{
|
|
64
|
+
missing: "content",
|
|
65
|
+
title: "No Apigee APIs found",
|
|
66
|
+
description: "No API entities with an Apigee org annotation are in the catalog yet. If you expect to see APIs here, check that the apigee-backend sync has run.",
|
|
67
|
+
action: /* @__PURE__ */ jsx(
|
|
68
|
+
LinkButton,
|
|
69
|
+
{
|
|
70
|
+
to: "/catalog?filters[kind]=API",
|
|
71
|
+
variant: "contained",
|
|
72
|
+
color: "primary",
|
|
73
|
+
children: "Browse the full catalog"
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
function NoMatchState({ onClearAll }) {
|
|
80
|
+
return /* @__PURE__ */ jsx(
|
|
81
|
+
EmptyState,
|
|
82
|
+
{
|
|
83
|
+
missing: "content",
|
|
84
|
+
title: "No APIs match your filters",
|
|
85
|
+
description: "Try removing a filter or clearing your search to see more APIs.",
|
|
86
|
+
action: /* @__PURE__ */ jsx(Button, { variant: "contained", color: "primary", onClick: onClearAll, children: "Clear all filters" })
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
function ApiCatalogError({ error }) {
|
|
91
|
+
return /* @__PURE__ */ jsx(ResponseErrorPanel, { title: "Couldn't load Apigee APIs", error });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { ApiCatalogEmptyState, ApiCatalogError, NoMatchState, SkeletonGrid };
|
|
95
|
+
//# sourceMappingURL=states.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"states.esm.js","sources":["../../src/components/states.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n EmptyState,\r\n ItemCardGrid,\r\n LinkButton,\r\n ResponseErrorPanel,\r\n} from '@backstage/core-components';\r\nimport { Box, Button, makeStyles } from '@material-ui/core';\r\n\r\nconst useStyles = makeStyles(theme => ({\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 card: {\r\n minHeight: 172,\r\n padding: theme.spacing(2),\r\n borderRadius: theme.shape.borderRadius,\r\n border: `1px solid ${theme.palette.divider}`,\r\n backgroundColor: theme.palette.background.paper,\r\n },\r\n bar: {\r\n backgroundColor: theme.palette.action.hover,\r\n borderRadius: theme.shape.borderRadius,\r\n animation: '$pulse 1.5s ease-in-out infinite',\r\n '@media (prefers-reduced-motion: reduce)': {\r\n animation: 'none',\r\n },\r\n },\r\n titleBar: { height: 20, width: '70%', marginBottom: theme.spacing(2) },\r\n metaBar: { height: 12, width: '50%', marginBottom: theme.spacing(1) },\r\n badgeBar: { height: 16, width: '30%', marginTop: theme.spacing(2) },\r\n '@keyframes pulse': {\r\n '0%, 100%': { opacity: 1 },\r\n '50%': { opacity: 0.4 },\r\n },\r\n}));\r\n\r\n/**\r\n * Loading placeholder: `count` skeleton tiles inside the same responsive grid, using\r\n * plain Box blocks (no MUI lab dependency — D-07). The region is marked `aria-busy`\r\n * with a visually-hidden polite live region so assistive tech announces the load. (UI-05)\r\n */\r\nexport function SkeletonGrid({ count = 8 }: { count?: number } = {}) {\r\n const classes = useStyles();\r\n return (\r\n <div aria-busy=\"true\">\r\n <span className={classes.srOnly} aria-live=\"polite\">\r\n Loading Apigee APIs…\r\n </span>\r\n <ItemCardGrid>\r\n {Array.from({ length: count }).map((_, index) => (\r\n <Box\r\n key={`apigee-skeleton-${index}`}\r\n className={classes.card}\r\n aria-hidden=\"true\"\r\n >\r\n <Box className={`${classes.bar} ${classes.titleBar}`} />\r\n <Box className={`${classes.bar} ${classes.metaBar}`} />\r\n <Box className={`${classes.bar} ${classes.metaBar}`} />\r\n <Box className={`${classes.bar} ${classes.badgeBar}`} />\r\n </Box>\r\n ))}\r\n </ItemCardGrid>\r\n </div>\r\n );\r\n}\r\n\r\n/** Friendly empty state shown when no Apigee APIs are in the catalog (UI-05). */\r\nexport function ApiCatalogEmptyState() {\r\n return (\r\n <EmptyState\r\n missing=\"content\"\r\n title=\"No Apigee APIs found\"\r\n description=\"No API entities with an Apigee org annotation are in the catalog yet. If you expect to see APIs here, check that the apigee-backend sync has run.\"\r\n action={\r\n <LinkButton\r\n to=\"/catalog?filters[kind]=API\"\r\n variant=\"contained\"\r\n color=\"primary\"\r\n >\r\n Browse the full catalog\r\n </LinkButton>\r\n }\r\n />\r\n );\r\n}\r\n\r\n/**\r\n * Distinct \"filtered to zero\" state (D-10) — shown when Apigee APIs exist but the active\r\n * filters/search exclude them all. Unlike `ApiCatalogEmptyState` (which links out to the full\r\n * catalog), this offers a non-destructive reset CTA (the button below) which is fully reversible,\r\n * so it uses primary/neutral styling (no error color, no confirmation dialog). (UI-08)\r\n */\r\nexport function NoMatchState({ onClearAll }: { onClearAll: () => void }) {\r\n return (\r\n <EmptyState\r\n missing=\"content\"\r\n title=\"No APIs match your filters\"\r\n description=\"Try removing a filter or clearing your search to see more APIs.\"\r\n action={\r\n <Button variant=\"contained\" color=\"primary\" onClick={onClearAll}>\r\n Clear all filters\r\n </Button>\r\n }\r\n />\r\n );\r\n}\r\n\r\n/** Error state shown when the catalog fetch fails (UI-05). */\r\nexport function ApiCatalogError({ error }: { error: Error }) {\r\n return <ResponseErrorPanel title=\"Couldn't load Apigee APIs\" error={error} />;\r\n}\r\n"],"names":[],"mappings":"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"}
|