@nospt/backstage-plugin-apigee 1.1.0-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +19 -0
  2. package/config.d.ts +25 -0
  3. package/dist/components/ApiCard.esm.js +146 -0
  4. package/dist/components/ApiCard.esm.js.map +1 -0
  5. package/dist/components/ApiCardGrid.esm.js +56 -0
  6. package/dist/components/ApiCardGrid.esm.js.map +1 -0
  7. package/dist/components/ApigeeApiCatalogPage.esm.js +153 -0
  8. package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -0
  9. package/dist/components/ApigeeNavContent.esm.js +35 -0
  10. package/dist/components/ApigeeNavContent.esm.js.map +1 -0
  11. package/dist/components/FilterToolbar.esm.js +197 -0
  12. package/dist/components/FilterToolbar.esm.js.map +1 -0
  13. package/dist/components/SearchBar.esm.js +48 -0
  14. package/dist/components/SearchBar.esm.js.map +1 -0
  15. package/dist/components/apiCardViewModel.esm.js +41 -0
  16. package/dist/components/apiCardViewModel.esm.js.map +1 -0
  17. package/dist/components/apigeeFacetFilters.esm.js +62 -0
  18. package/dist/components/apigeeFacetFilters.esm.js.map +1 -0
  19. package/dist/components/apigeeOrgFilter.esm.js +11 -0
  20. package/dist/components/apigeeOrgFilter.esm.js.map +1 -0
  21. package/dist/components/apigeeSearchParams.esm.js +21 -0
  22. package/dist/components/apigeeSearchParams.esm.js.map +1 -0
  23. package/dist/components/categoryIcons.esm.js +63 -0
  24. package/dist/components/categoryIcons.esm.js.map +1 -0
  25. package/dist/components/facetModel.esm.js +87 -0
  26. package/dist/components/facetModel.esm.js.map +1 -0
  27. package/dist/components/grouping.esm.js +31 -0
  28. package/dist/components/grouping.esm.js.map +1 -0
  29. package/dist/components/states.esm.js +95 -0
  30. package/dist/components/states.esm.js.map +1 -0
  31. package/dist/components/useApigeeFilters.esm.js +88 -0
  32. package/dist/components/useApigeeFilters.esm.js.map +1 -0
  33. package/dist/components/useDebouncedValue.esm.js +13 -0
  34. package/dist/components/useDebouncedValue.esm.js.map +1 -0
  35. package/dist/constants.esm.js +9 -0
  36. package/dist/constants.esm.js.map +1 -0
  37. package/dist/index.d.ts +101 -0
  38. package/dist/index.esm.js +2 -0
  39. package/dist/index.esm.js.map +1 -0
  40. package/dist/plugin.esm.js +39 -0
  41. package/dist/plugin.esm.js.map +1 -0
  42. package/dist/routes.esm.js +6 -0
  43. package/dist/routes.esm.js.map +1 -0
  44. package/package.json +92 -0
@@ -0,0 +1 @@
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 entityDepartment,\r\n entityLifecycle,\r\n entityOrg,\r\n entityOwnerLabel,\r\n} from './apigeeFacetFilters';\r\n\r\nexport type FacetKey = 'lifecycle' | 'org' | 'owner' | 'category' | 'department';\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/** Facet order (D-02): Lifecycle → Org → Owner → Category → Department. */\r\nexport const FACET_ORDER: FacetKey[] = [\r\n 'lifecycle',\r\n 'org',\r\n 'owner',\r\n 'category',\r\n 'department',\r\n];\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 department: 'Department',\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 department: entityDepartment,\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 → explicit override for known org IDs from config, else humanise(value)\r\n */\r\nfunction labelFor(\r\n key: FacetKey,\r\n value: string,\r\n orgLabels?: Record<string, string>,\r\n): 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 return orgLabels?.[value] ?? humanise(value);\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 orgLabels?: 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, orgLabels),\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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"}
@@ -0,0 +1,88 @@
1
+ import { useMemo, useState, useEffect, useCallback } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
3
+ import { useEntityList } from '@backstage/plugin-catalog-react';
4
+ import { FieldFacetFilter, entityDepartment, entityCategoryFacet, entityOwnerLabel, entityOrg, entityLifecycle, ApiSearchFilter } from './apigeeFacetFilters.esm.js';
5
+ import { parseSearchParams } from './apigeeSearchParams.esm.js';
6
+ import { useDebouncedValue } from './useDebouncedValue.esm.js';
7
+
8
+ const FACET_EXTRACT = {
9
+ lifecycle: entityLifecycle,
10
+ org: entityOrg,
11
+ owner: entityOwnerLabel,
12
+ category: entityCategoryFacet,
13
+ department: entityDepartment
14
+ };
15
+ const FILTER_KEY = {
16
+ lifecycle: "apigeeLifecycle",
17
+ org: "apigeeOrgFacet",
18
+ owner: "apigeeOwner",
19
+ category: "apigeeCategory",
20
+ department: "apigeeDepartment"
21
+ };
22
+ function useApigeeFilters() {
23
+ const [searchParams, setSearchParams] = useSearchParams();
24
+ const { updateFilters } = useEntityList();
25
+ const spString = searchParams.toString();
26
+ const state = useMemo(() => parseSearchParams(searchParams), [spString]);
27
+ const [inputValue, setInputValue] = useState(() => state.q);
28
+ const debouncedQ = useDebouncedValue(inputValue, 200);
29
+ useEffect(() => {
30
+ setSearchParams(
31
+ (prev) => {
32
+ const next = new URLSearchParams(prev);
33
+ if (debouncedQ.trim()) {
34
+ next.set("q", debouncedQ.trim());
35
+ } else {
36
+ next.delete("q");
37
+ }
38
+ return next;
39
+ },
40
+ { replace: true }
41
+ );
42
+ }, [debouncedQ, setSearchParams]);
43
+ useEffect(() => {
44
+ const patch = {};
45
+ Object.keys(FILTER_KEY).forEach((k) => {
46
+ patch[FILTER_KEY[k]] = state.selected[k].length ? new FieldFacetFilter(FACET_EXTRACT[k], state.selected[k]) : void 0;
47
+ });
48
+ patch.apigeeSearch = state.q.trim() ? new ApiSearchFilter(state.q) : void 0;
49
+ updateFilters(patch);
50
+ }, [spString, updateFilters]);
51
+ const toggleFacet = useCallback(
52
+ (key, value) => {
53
+ setSearchParams(
54
+ (prev) => {
55
+ const next = new URLSearchParams(prev);
56
+ const cur = next.getAll(key);
57
+ next.delete(key);
58
+ const after = cur.includes(value) ? cur.filter((v) => v !== value) : [...cur, value];
59
+ after.forEach((v) => next.append(key, v));
60
+ return next;
61
+ },
62
+ { replace: true }
63
+ );
64
+ },
65
+ [setSearchParams]
66
+ );
67
+ const clearFacet = useCallback(
68
+ (key) => {
69
+ setSearchParams(
70
+ (prev) => {
71
+ const next = new URLSearchParams(prev);
72
+ next.delete(key);
73
+ return next;
74
+ },
75
+ { replace: true }
76
+ );
77
+ },
78
+ [setSearchParams]
79
+ );
80
+ const clearAll = useCallback(() => {
81
+ setInputValue("");
82
+ setSearchParams(new URLSearchParams(), { replace: true });
83
+ }, [setSearchParams]);
84
+ return { state, inputValue, setInputValue, toggleFacet, clearFacet, clearAll };
85
+ }
86
+
87
+ export { useApigeeFilters };
88
+ //# sourceMappingURL=useApigeeFilters.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useApigeeFilters.esm.js","sources":["../../src/components/useApigeeFilters.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { useCallback, useEffect, useMemo, useState } from 'react';\r\nimport { useSearchParams } from 'react-router-dom';\r\nimport { useEntityList } from '@backstage/plugin-catalog-react';\r\nimport type { Entity } from '@backstage/catalog-model';\r\nimport {\r\n ApiSearchFilter,\r\n FieldFacetFilter,\r\n entityCategoryFacet,\r\n entityDepartment,\r\n entityLifecycle,\r\n entityOrg,\r\n entityOwnerLabel,\r\n} from './apigeeFacetFilters';\r\nimport { parseSearchParams } from './apigeeSearchParams';\r\nimport { useDebouncedValue } from './useDebouncedValue';\r\nimport type { ApigeeFilters } from './ApigeeApiCatalogPage';\r\nimport type { FacetKey } from './facetModel';\r\n\r\n/**\r\n * Value extractor per facet slot used to build the FILTER. Category uses the FACET extractor\r\n * (undefined → 'other'), so selecting the 'other' bucket matches uncategorised APIs — search uses\r\n * the real slug instead (06-01).\r\n */\r\nconst FACET_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 department: entityDepartment,\r\n};\r\n\r\n/** Facet slot → the `ApigeeFilters` key its `FieldFacetFilter` registers under. */\r\ntype FacetFilterKey =\r\n | 'apigeeLifecycle'\r\n | 'apigeeOrgFacet'\r\n | 'apigeeOwner'\r\n | 'apigeeCategory'\r\n | 'apigeeDepartment';\r\nconst FILTER_KEY: Record<FacetKey, FacetFilterKey> = {\r\n lifecycle: 'apigeeLifecycle',\r\n org: 'apigeeOrgFacet',\r\n owner: 'apigeeOwner',\r\n category: 'apigeeCategory',\r\n department: 'apigeeDepartment',\r\n};\r\n\r\n/**\r\n * The `useSearchParams` ⇄ `updateFilters` bridge: the URL is the single source of truth. It parses\r\n * the clean facet + `q` keys into state, drives the provider's in-memory filters one-directionally\r\n * (URL → `updateFilters`, no re-fetch), and writes every change back with `{ replace: true }` (D-08)\r\n * so the address bar stays clean, shareable and reload-safe (UI-09). The search box keeps one local\r\n * `inputValue` (instant echo) with a single 200 ms trailing debounce (D-09) → URL; the URL never\r\n * writes back into the input, so there is no feedback loop (UI-06, UI-07).\r\n */\r\nexport function useApigeeFilters() {\r\n const [searchParams, setSearchParams] = useSearchParams();\r\n const { updateFilters } = useEntityList<ApigeeFilters>();\r\n\r\n // Depend on the stable STRING, not the per-render URLSearchParams object (Pitfall 4).\r\n const spString = searchParams.toString();\r\n const state = useMemo(() => parseSearchParams(searchParams), [spString]); // eslint-disable-line\r\n\r\n // Search text is the only local state; seeded once from the URL.\r\n const [inputValue, setInputValue] = useState(() => state.q);\r\n const debouncedQ = useDebouncedValue(inputValue, 200); // D-09\r\n\r\n // E2: debounced q → URL (replace), merged with the current facet params.\r\n useEffect(() => {\r\n setSearchParams(\r\n prev => {\r\n const next = new URLSearchParams(prev);\r\n if (debouncedQ.trim()) {\r\n next.set('q', debouncedQ.trim());\r\n } else {\r\n next.delete('q');\r\n }\r\n return next;\r\n },\r\n { replace: true },\r\n );\r\n }, [debouncedQ, setSearchParams]);\r\n\r\n // E1: URL → provider filters (one-directional). Runs on mount (seeds shared links) + on change.\r\n useEffect(() => {\r\n const patch: Partial<ApigeeFilters> = {};\r\n (Object.keys(FILTER_KEY) as FacetKey[]).forEach(k => {\r\n patch[FILTER_KEY[k]] = state.selected[k].length\r\n ? new FieldFacetFilter(FACET_EXTRACT[k], state.selected[k])\r\n : undefined;\r\n });\r\n patch.apigeeSearch = state.q.trim()\r\n ? new ApiSearchFilter(state.q)\r\n : undefined;\r\n updateFilters(patch);\r\n }, [spString, updateFilters]); // eslint-disable-line\r\n\r\n const toggleFacet = useCallback(\r\n (key: FacetKey, value: string) => {\r\n setSearchParams(\r\n prev => {\r\n const next = new URLSearchParams(prev);\r\n const cur = next.getAll(key);\r\n next.delete(key);\r\n const after = cur.includes(value)\r\n ? cur.filter(v => v !== value)\r\n : [...cur, value];\r\n after.forEach(v => next.append(key, v));\r\n return next;\r\n },\r\n { replace: true },\r\n );\r\n },\r\n [setSearchParams],\r\n );\r\n\r\n const clearFacet = useCallback(\r\n (key: FacetKey) => {\r\n setSearchParams(\r\n prev => {\r\n const next = new URLSearchParams(prev);\r\n next.delete(key);\r\n return next;\r\n },\r\n { replace: true },\r\n );\r\n },\r\n [setSearchParams],\r\n );\r\n\r\n const clearAll = useCallback(() => {\r\n setInputValue('');\r\n setSearchParams(new URLSearchParams(), { replace: true });\r\n }, [setSearchParams]);\r\n\r\n return { state, inputValue, setInputValue, toggleFacet, clearFacet, clearAll };\r\n}\r\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ function useDebouncedValue(value, delayMs) {
4
+ const [debounced, setDebounced] = useState(value);
5
+ useEffect(() => {
6
+ const id = setTimeout(() => setDebounced(value), delayMs);
7
+ return () => clearTimeout(id);
8
+ }, [value, delayMs]);
9
+ return debounced;
10
+ }
11
+
12
+ export { useDebouncedValue };
13
+ //# sourceMappingURL=useDebouncedValue.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDebouncedValue.esm.js","sources":["../../src/components/useDebouncedValue.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { useEffect, useState } from 'react';\r\n\r\n/**\r\n * Trailing-edge debounce: returns `value` immediately, then re-emits the latest `value`\r\n * after `delayMs` of no change. Rapid successive changes cancel the prior timer via\r\n * `clearTimeout`, so only the trailing value is emitted. No external dependency (D-09).\r\n */\r\nexport function useDebouncedValue<T>(value: T, delayMs: number): T {\r\n const [debounced, setDebounced] = useState(value);\r\n useEffect(() => {\r\n const id = setTimeout(() => setDebounced(value), delayMs);\r\n return () => clearTimeout(id);\r\n }, [value, delayMs]);\r\n return debounced;\r\n}\r\n"],"names":[],"mappings":";;AAQO,SAAS,iBAAA,CAAqB,OAAU,OAAA,EAAoB;AACjE,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAS,KAAK,CAAA;AAChD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,KAAK,UAAA,CAAW,MAAM,YAAA,CAAa,KAAK,GAAG,OAAO,CAAA;AACxD,IAAA,OAAO,MAAM,aAAa,EAAE,CAAA;AAAA,EAC9B,CAAA,EAAG,CAAC,KAAA,EAAO,OAAO,CAAC,CAAA;AACnB,EAAA,OAAO,SAAA;AACT;;;;"}
@@ -0,0 +1,9 @@
1
+ const ANNOTATION_APIGEE_ORGS = "apigee.com/orgs";
2
+ const ANNOTATION_APIGEE_ORG = "apigee.com/org";
3
+ const ANNOTATION_HAS_SPEC = "apigee.com/has-spec";
4
+ const ANNOTATION_CATEGORY = "apigee.com/category";
5
+ const ANNOTATION_API_HUB_VERSION = "apigee.com/api-hub-version";
6
+ const LABEL_DEPARTMENT = "department";
7
+
8
+ export { ANNOTATION_APIGEE_ORG, ANNOTATION_APIGEE_ORGS, ANNOTATION_API_HUB_VERSION, ANNOTATION_CATEGORY, ANNOTATION_HAS_SPEC, LABEL_DEPARTMENT };
9
+ //# sourceMappingURL=constants.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.esm.js","sources":["../src/constants.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\n// Apigee catalog annotation keys. Authoritative source:\r\n// plugins/apigee-backend/src/lib/entity-builder.ts (verified 2026-07-09).\r\n\r\n/** Org annotation written by proxyToEntities (PLURAL, proxy-derived API entities). */\r\nexport const ANNOTATION_APIGEE_ORGS = 'apigee.com/orgs';\r\n/** Org annotation written by hubOnlyToEntity (SINGULAR, hub-only API entities). */\r\nexport const ANNOTATION_APIGEE_ORG = 'apigee.com/org';\r\n/** 'true' | 'false' — set by applyHubMetadata when a spec is embedded or linked. */\r\nexport const ANNOTATION_HAS_SPEC = 'apigee.com/has-spec';\r\n/** Comma-joined slugified categories (absent in the current seed dump). */\r\nexport const ANNOTATION_CATEGORY = 'apigee.com/category';\r\n/** Full API Hub resource name — present only on hub-only entities. */\r\nexport const ANNOTATION_API_HUB_ID = 'apigee.com/api-hub-id';\r\n/**\r\n * Best-effort API Hub version key (A3 — not yet emitted by the backend, absent in the seed).\r\n * The view-model HIDES the version line when this is absent (D-05).\r\n */\r\nexport const ANNOTATION_API_HUB_VERSION = 'apigee.com/api-hub-version';\r\n\r\n/**\r\n * Department label key (`metadata.labels.department`). Written by the backend's\r\n * applyHubMetadata from the API Hub \"Business unit\" attribute.\r\n */\r\nexport const LABEL_DEPARTMENT = 'department';\r\n"],"names":[],"mappings":"AAKO,MAAM,sBAAA,GAAyB;AAE/B,MAAM,qBAAA,GAAwB;AAE9B,MAAM,mBAAA,GAAsB;AAE5B,MAAM,mBAAA,GAAsB;AAO5B,MAAM,0BAAA,GAA6B;AAMnC,MAAM,gBAAA,GAAmB;;;;"}
@@ -0,0 +1,101 @@
1
+ import * as react from 'react';
2
+ import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
3
+
4
+ declare const apigeePage: _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
5
+ kind: "page";
6
+ name: undefined;
7
+ config: {
8
+ path: string | undefined;
9
+ title: string | undefined;
10
+ };
11
+ configInput: {
12
+ title?: string | undefined;
13
+ path?: string | undefined;
14
+ };
15
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
16
+ optional: true;
17
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.title", {
18
+ optional: true;
19
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
20
+ optional: true;
21
+ }>;
22
+ inputs: {
23
+ pages: _backstage_frontend_plugin_api.ExtensionInput<_backstage_frontend_plugin_api.ConfigurableExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
24
+ optional: true;
25
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.title", {
26
+ optional: true;
27
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
28
+ optional: true;
29
+ }>, {
30
+ singleton: false;
31
+ optional: false;
32
+ internal: false;
33
+ }>;
34
+ };
35
+ params: {
36
+ path: string;
37
+ title?: string;
38
+ icon?: _backstage_frontend_plugin_api.IconElement;
39
+ loader?: () => Promise<react.JSX.Element>;
40
+ routeRef?: _backstage_frontend_plugin_api.RouteRef;
41
+ noHeader?: boolean;
42
+ };
43
+ }>;
44
+ /**
45
+ * Frontend module that overrides the app nav bar to show an expandable
46
+ * "API Hub" group containing all apigee plugin pages as sub-items.
47
+ *
48
+ * Add this to your app's `createApp({ features })` to activate the grouped nav.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * createApp({ features: [apigeePlugin, apigeeNavModule] });
53
+ * ```
54
+ */
55
+ declare const apigeeNavModule: _backstage_frontend_plugin_api.FrontendModule;
56
+ declare const apigeePlugin: _backstage_frontend_plugin_api.OverridableFrontendPlugin<{
57
+ root: _backstage_frontend_plugin_api.RouteRef<undefined>;
58
+ }, {}, {
59
+ "page:apigee": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
60
+ kind: "page";
61
+ name: undefined;
62
+ config: {
63
+ path: string | undefined;
64
+ title: string | undefined;
65
+ };
66
+ configInput: {
67
+ title?: string | undefined;
68
+ path?: string | undefined;
69
+ };
70
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
71
+ optional: true;
72
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.title", {
73
+ optional: true;
74
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
75
+ optional: true;
76
+ }>;
77
+ inputs: {
78
+ pages: _backstage_frontend_plugin_api.ExtensionInput<_backstage_frontend_plugin_api.ConfigurableExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
79
+ optional: true;
80
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.title", {
81
+ optional: true;
82
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
83
+ optional: true;
84
+ }>, {
85
+ singleton: false;
86
+ optional: false;
87
+ internal: false;
88
+ }>;
89
+ };
90
+ params: {
91
+ path: string;
92
+ title?: string;
93
+ icon?: _backstage_frontend_plugin_api.IconElement;
94
+ loader?: () => Promise<react.JSX.Element>;
95
+ routeRef?: _backstage_frontend_plugin_api.RouteRef;
96
+ noHeader?: boolean;
97
+ };
98
+ }>;
99
+ }>;
100
+
101
+ export { apigeeNavModule, apigeePage, apigeePlugin, apigeePlugin as default };
@@ -0,0 +1,2 @@
1
+ export { apigeeNavModule, apigeePage, apigeePlugin, apigeePlugin as default } from './plugin.esm.js';
2
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { PageBlueprint, createFrontendModule, createFrontendPlugin } from '@backstage/frontend-plugin-api';
3
+ import { NavContentBlueprint } from '@backstage/plugin-app-react';
4
+ import LayersIcon from '@material-ui/icons/Layers';
5
+ import { rootRouteRef } from './routes.esm.js';
6
+ import { ApigeeNavContent } from './components/ApigeeNavContent.esm.js';
7
+
8
+ const apigeePage = PageBlueprint.make({
9
+ params: {
10
+ title: "API Catalog",
11
+ icon: /* @__PURE__ */ jsx(LayersIcon, {}),
12
+ routeRef: rootRouteRef,
13
+ path: "/apigee",
14
+ loader: () => import('./components/ApigeeApiCatalogPage.esm.js').then(
15
+ ({ ApigeeApiCatalogPage }) => /* @__PURE__ */ jsx(ApigeeApiCatalogPage, {})
16
+ )
17
+ }
18
+ });
19
+ const apigeeNavModule = createFrontendModule({
20
+ pluginId: "app",
21
+ extensions: [
22
+ NavContentBlueprint.make({
23
+ params: {
24
+ component: ApigeeNavContent
25
+ }
26
+ })
27
+ ]
28
+ });
29
+ const apigeePlugin = createFrontendPlugin({
30
+ pluginId: "apigee",
31
+ title: "API Hub",
32
+ extensions: [apigeePage],
33
+ routes: {
34
+ root: rootRouteRef
35
+ }
36
+ });
37
+
38
+ export { apigeeNavModule, apigeePage, apigeePlugin };
39
+ //# sourceMappingURL=plugin.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n createFrontendModule,\r\n createFrontendPlugin,\r\n PageBlueprint,\r\n} from '@backstage/frontend-plugin-api';\r\nimport { NavContentBlueprint } from '@backstage/plugin-app-react';\r\nimport LayersIcon from '@material-ui/icons/Layers';\r\nimport { rootRouteRef } from './routes';\r\nimport { ApigeeNavContent } from './components/ApigeeNavContent';\r\n\r\nexport const apigeePage = PageBlueprint.make({\r\n params: {\r\n title: 'API Catalog',\r\n icon: <LayersIcon />,\r\n routeRef: rootRouteRef,\r\n path: '/apigee',\r\n loader: () =>\r\n import('./components/ApigeeApiCatalogPage').then(\r\n ({ ApigeeApiCatalogPage }) => <ApigeeApiCatalogPage />,\r\n ),\r\n },\r\n});\r\n\r\n/**\r\n * Frontend module that overrides the app nav bar to show an expandable\r\n * \"API Hub\" group containing all apigee plugin pages as sub-items.\r\n *\r\n * Add this to your app's `createApp({ features })` to activate the grouped nav.\r\n *\r\n * @example\r\n * ```ts\r\n * createApp({ features: [apigeePlugin, apigeeNavModule] });\r\n * ```\r\n */\r\nexport const apigeeNavModule = createFrontendModule({\r\n pluginId: 'app',\r\n extensions: [\r\n NavContentBlueprint.make({\r\n params: {\r\n component: ApigeeNavContent,\r\n },\r\n }),\r\n ],\r\n});\r\n\r\nexport const apigeePlugin = createFrontendPlugin({\r\n pluginId: 'apigee',\r\n title: 'API Hub',\r\n extensions: [apigeePage],\r\n routes: {\r\n root: rootRouteRef,\r\n },\r\n});\r\n"],"names":[],"mappings":";;;;;;;AAWO,MAAM,UAAA,GAAa,cAAc,IAAA,CAAK;AAAA,EAC3C,MAAA,EAAQ;AAAA,IACN,KAAA,EAAO,aAAA;AAAA,IACP,IAAA,sBAAO,UAAA,EAAA,EAAW,CAAA;AAAA,IAClB,QAAA,EAAU,YAAA;AAAA,IACV,IAAA,EAAM,SAAA;AAAA,IACN,MAAA,EAAQ,MACN,OAAO,0CAAmC,CAAA,CAAE,IAAA;AAAA,MAC1C,CAAC,EAAE,oBAAA,EAAqB,yBAAO,oBAAA,EAAA,EAAqB;AAAA;AACtD;AAEN,CAAC;AAaM,MAAM,kBAAkB,oBAAA,CAAqB;AAAA,EAClD,QAAA,EAAU,KAAA;AAAA,EACV,UAAA,EAAY;AAAA,IACV,oBAAoB,IAAA,CAAK;AAAA,MACvB,MAAA,EAAQ;AAAA,QACN,SAAA,EAAW;AAAA;AACb,KACD;AAAA;AAEL,CAAC;AAEM,MAAM,eAAe,oBAAA,CAAqB;AAAA,EAC/C,QAAA,EAAU,QAAA;AAAA,EACV,KAAA,EAAO,SAAA;AAAA,EACP,UAAA,EAAY,CAAC,UAAU,CAAA;AAAA,EACvB,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA;AAEV,CAAC;;;;"}
@@ -0,0 +1,6 @@
1
+ import { createRouteRef } from '@backstage/frontend-plugin-api';
2
+
3
+ const rootRouteRef = createRouteRef();
4
+
5
+ export { rootRouteRef };
6
+ //# sourceMappingURL=routes.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.esm.js","sources":["../src/routes.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { createRouteRef } from '@backstage/frontend-plugin-api';\r\n\r\nexport const rootRouteRef = createRouteRef();\r\n"],"names":[],"mappings":";;AAGO,MAAM,eAAe,cAAA;;;;"}
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@nospt/backstage-plugin-apigee",
3
+ "version": "1.1.0-rc2",
4
+ "license": "Apache-2.0",
5
+ "description": "Backstage frontend plugin bootstrap for Apigee integration",
6
+ "author": "NOS SGPS, S.A.",
7
+ "homepage": "https://github.com/nosportugal/backstage-plugin-apigee#readme",
8
+ "keywords": [
9
+ "backstage",
10
+ "backstage-plugin",
11
+ "apigee"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/nosportugal/backstage-plugin-apigee",
16
+ "directory": "plugins/apigee"
17
+ },
18
+ "main": "dist/index.esm.js",
19
+ "types": "dist/index.d.ts",
20
+ "configSchema": "config.d.ts",
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "main": "dist/index.esm.js",
24
+ "types": "dist/index.d.ts"
25
+ },
26
+ "backstage": {
27
+ "role": "frontend-plugin",
28
+ "pluginId": "apigee",
29
+ "pluginPackages": [
30
+ "@nospt/backstage-plugin-apigee",
31
+ "@nospt/backstage-plugin-apigee-backend",
32
+ "@nospt/backstage-plugin-apigee-common"
33
+ ],
34
+ "features": {
35
+ ".": "@backstage/FrontendPlugin"
36
+ }
37
+ },
38
+ "sideEffects": false,
39
+ "scripts": {
40
+ "start": "backstage-cli package start",
41
+ "build": "backstage-cli package build",
42
+ "lint": "backstage-cli package lint",
43
+ "test": "backstage-cli package test",
44
+ "test:e2e": "playwright test",
45
+ "clean": "backstage-cli package clean",
46
+ "prepack": "backstage-cli package prepack",
47
+ "postpack": "backstage-cli package postpack"
48
+ },
49
+ "dependencies": {
50
+ "@backstage/catalog-model": "^1.9.0",
51
+ "@backstage/core-components": "^0.18.8",
52
+ "@backstage/core-plugin-api": "^1.10.3",
53
+ "@backstage/frontend-plugin-api": "^0.15.1",
54
+ "@backstage/plugin-catalog-react": "^2.1.1",
55
+ "@material-ui/core": "^4.9.13",
56
+ "@material-ui/icons": "^4.9.1",
57
+ "@nospt/backstage-plugin-apigee-common": "^1.1.0-rc2"
58
+ },
59
+ "peerDependencies": {
60
+ "react": "^16.13.1 || ^17.0.0 || ^18.0.0",
61
+ "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
62
+ "react-router-dom": "^6.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@backstage/cli": "^0.36.0",
66
+ "@backstage/config": "^1.3.7",
67
+ "@backstage/frontend-defaults": "^0.5.0",
68
+ "@backstage/frontend-test-utils": "^0.5.1",
69
+ "@backstage/plugin-app-react": "^0.2.2",
70
+ "@backstage/plugin-catalog": "^2.0.1",
71
+ "@backstage/ui": "^0.13.2",
72
+ "@playwright/test": "^1.49.0",
73
+ "@testing-library/jest-dom": "^6.0.0",
74
+ "@testing-library/react": "^14.0.0",
75
+ "react": "^16.13.1 || ^17.0.0 || ^18.0.0",
76
+ "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
77
+ "react-router-dom": "^6.0.0"
78
+ },
79
+ "files": [
80
+ "dist",
81
+ "config.d.ts",
82
+ "README.md"
83
+ ],
84
+ "typesVersions": {
85
+ "*": {
86
+ "package.json": [
87
+ "package.json"
88
+ ]
89
+ }
90
+ },
91
+ "module": "./dist/index.esm.js"
92
+ }