@nospt/backstage-plugin-apigee 1.1.0-rc2 → 1.1.0-rc4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @nospt/backstage-plugin-apigee might be problematic. Click here for more details.

Files changed (52) hide show
  1. package/README.md +19 -19
  2. package/config.d.ts +25 -25
  3. package/dist/components/ApiCard.esm.js +67 -125
  4. package/dist/components/ApiCard.esm.js.map +1 -1
  5. package/dist/components/ApiCard.module.css.esm.js +8 -0
  6. package/dist/components/ApiCard.module.css.esm.js.map +1 -0
  7. package/dist/components/ApiCardGrid.esm.js +12 -46
  8. package/dist/components/ApiCardGrid.esm.js.map +1 -1
  9. package/dist/components/ApiTable.esm.js +162 -0
  10. package/dist/components/ApiTable.esm.js.map +1 -0
  11. package/dist/components/ApiTable.module.css.esm.js +8 -0
  12. package/dist/components/ApiTable.module.css.esm.js.map +1 -0
  13. package/dist/components/ApigeeApiCatalogPage.esm.js +80 -60
  14. package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -1
  15. package/dist/components/ApigeeNavContent.esm.js +2 -2
  16. package/dist/components/ApigeeNavContent.esm.js.map +1 -1
  17. package/dist/components/CountCards.esm.js +122 -0
  18. package/dist/components/CountCards.esm.js.map +1 -0
  19. package/dist/components/CountCards.module.css.esm.js +8 -0
  20. package/dist/components/CountCards.module.css.esm.js.map +1 -0
  21. package/dist/components/FilterToolbar.esm.js +75 -142
  22. package/dist/components/FilterToolbar.esm.js.map +1 -1
  23. package/dist/components/FilterToolbar.module.css.esm.js +8 -0
  24. package/dist/components/FilterToolbar.module.css.esm.js.map +1 -0
  25. package/dist/components/SearchBar.esm.js +7 -35
  26. package/dist/components/SearchBar.esm.js.map +1 -1
  27. package/dist/components/apiCardViewModel.esm.js +16 -2
  28. package/dist/components/apiCardViewModel.esm.js.map +1 -1
  29. package/dist/components/apigeeFacetFilters.esm.js.map +1 -1
  30. package/dist/components/apigeeOrgFilter.esm.js.map +1 -1
  31. package/dist/components/apigeeSearchParams.esm.js +14 -1
  32. package/dist/components/apigeeSearchParams.esm.js.map +1 -1
  33. package/dist/components/categoryIcons.esm.js +12 -22
  34. package/dist/components/categoryIcons.esm.js.map +1 -1
  35. package/dist/components/facetModel.esm.js +30 -10
  36. package/dist/components/facetModel.esm.js.map +1 -1
  37. package/dist/components/grouping.esm.js +1 -26
  38. package/dist/components/grouping.esm.js.map +1 -1
  39. package/dist/components/states.esm.js +19 -49
  40. package/dist/components/states.esm.js.map +1 -1
  41. package/dist/components/useApigeeFilters.esm.js +18 -2
  42. package/dist/components/useApigeeFilters.esm.js.map +1 -1
  43. package/dist/components/useDebouncedValue.esm.js.map +1 -1
  44. package/dist/constants.esm.js +3 -2
  45. package/dist/constants.esm.js.map +1 -1
  46. package/dist/index.d.ts +2 -2
  47. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
  48. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
  49. package/dist/plugin.esm.js +2 -2
  50. package/dist/plugin.esm.js.map +1 -1
  51. package/dist/routes.esm.js.map +1 -1
  52. package/package.json +17 -15
@@ -1 +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;;;;"}
1
+ {"version":3,"file":"categoryIcons.esm.js","sources":["../../src/components/categoryIcons.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport type { RemixiconComponentType } from '@remixicon/react';\nimport {\n RiAppsLine,\n RiBankCardLine,\n RiBarChartBoxLine,\n RiMapPinLine,\n RiNotification3Line,\n RiRouterLine,\n RiShieldKeyholeLine,\n RiShoppingCartLine,\n RiSmartphoneLine,\n RiTeamLine,\n RiTvLine,\n} from '@remixicon/react';\n\nexport type IconKey =\n | 'Category'\n | 'Security'\n | 'Payment'\n | 'People'\n | 'ShoppingCart'\n | 'Notifications'\n | 'Assessment'\n | 'SettingsEthernet'\n | 'Devices'\n | 'Tv'\n | 'Place';\n\n/**\n * Fixed allow-list registry. A config override may ONLY select one of these keys —\n * a config value is never a module path or an arbitrary component (T-05-01).\n */\nexport const ICONS: Record<IconKey, RemixiconComponentType> = {\n Category: RiAppsLine,\n Security: RiShieldKeyholeLine,\n Payment: RiBankCardLine,\n People: RiTeamLine,\n ShoppingCart: RiShoppingCartLine,\n Notifications: RiNotification3Line,\n Assessment: RiBarChartBoxLine,\n SettingsEthernet: RiRouterLine,\n Devices: RiSmartphoneLine,\n Tv: RiTvLine,\n Place: RiMapPinLine,\n};\n\n/** Default category-slug -> IconKey substring map (D-08, case-insensitive). */\nconst DEFAULT_MAP: ReadonlyArray<{ match: RegExp; key: IconKey }> = [\n { match: /security|auth|identity|oauth/i, key: 'Security' },\n { match: /payment|billing|finance|fino/i, key: 'Payment' },\n { match: /customer|crm|people|partner/i, key: 'People' },\n { match: /order|commerce|catalog|product/i, key: 'ShoppingCart' },\n { match: /communication|messaging|notification/i, key: 'Notifications' },\n { match: /data|analytics|reporting/i, key: 'Assessment' },\n { match: /network|integration|common|infra/i, key: 'SettingsEthernet' },\n { match: /device|iot/i, key: 'Devices' },\n { match: /media|tv|content/i, key: 'Tv' },\n { match: /location|geo|gis/i, key: 'Place' },\n];\n\n/** Type guard: is `v` an OWN key of the static ICONS allow-list? */\nfunction isIconKey(v: string): v is IconKey {\n return Object.prototype.hasOwnProperty.call(ICONS, v);\n}\n\n/**\n * Resolve a category slug to a Material icon component.\n *\n * A config `overrides` value may only select an allow-listed IconKey; any other value\n * (unknown key, module path, `constructor`, etc.) falls back to `Category` and warns\n * once. This never `import()`s a config path or renders an arbitrary component — it is\n * the primary mitigation for the config-driven component-injection threat (T-05-01).\n */\nexport function resolveCategoryIcon(\n slug: string | undefined,\n overrides?: Record<string, string>,\n): RemixiconComponentType {\n if (slug && overrides && Object.prototype.hasOwnProperty.call(overrides, slug)) {\n const value = overrides[slug];\n if (isIconKey(value)) {\n return ICONS[value];\n }\n // eslint-disable-next-line no-console\n console.warn(\n '[apigee] Ignoring unknown categoryIcons value \"%s\" for \"%s\"; using Category',\n value,\n slug,\n );\n }\n\n if (slug) {\n const match = DEFAULT_MAP.find(entry => entry.match.test(slug));\n if (match) {\n return ICONS[match.key];\n }\n }\n\n return ICONS.Category;\n}\n"],"names":[],"mappings":";;AAiCO,MAAM,KAAA,GAAiD;AAAA,EAC5D,QAAA,EAAU,UAAA;AAAA,EACV,QAAA,EAAU,mBAAA;AAAA,EACV,OAAA,EAAS,cAAA;AAAA,EACT,MAAA,EAAQ,UAAA;AAAA,EACR,YAAA,EAAc,kBAAA;AAAA,EACd,aAAA,EAAe,mBAAA;AAAA,EACf,UAAA,EAAY,iBAAA;AAAA,EACZ,gBAAA,EAAkB,YAAA;AAAA,EAClB,OAAA,EAAS,gBAAA;AAAA,EACT,EAAA,EAAI,QAAA;AAAA,EACJ,KAAA,EAAO;AACT;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,EACwB;AACxB,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;;;;"}
@@ -64,15 +64,18 @@ function deriveFacets(entities, overrides, orgLabels) {
64
64
  value,
65
65
  label: labelFor(key, value, orgLabels)
66
66
  }));
67
- const icons = key === "category" ? Object.fromEntries(
68
- options.map((opt) => [
69
- opt.value,
70
- resolveCategoryIcon(
71
- opt.value === "other" ? void 0 : opt.value,
72
- overrides
73
- )
74
- ])
75
- ) : void 0;
67
+ const icons = (
68
+ // Guard on options.length: Object.fromEntries([]) is {}, not undefined.
69
+ key === "category" && options.length > 0 ? Object.fromEntries(
70
+ options.map((opt) => [
71
+ opt.value,
72
+ resolveCategoryIcon(
73
+ opt.value === "other" ? void 0 : opt.value,
74
+ overrides
75
+ )
76
+ ])
77
+ ) : void 0
78
+ );
76
79
  return {
77
80
  key,
78
81
  label: FACET_TRIGGER_LABEL[key],
@@ -82,6 +85,23 @@ function deriveFacets(entities, overrides, orgLabels) {
82
85
  };
83
86
  });
84
87
  }
88
+ function deriveCounts(entities, keys, orgLabels) {
89
+ return keys.map((key) => {
90
+ const counts = /* @__PURE__ */ new Map();
91
+ for (const entity of entities) {
92
+ const value = EXTRACT[key](entity);
93
+ if (value) {
94
+ counts.set(value, (counts.get(value) ?? 0) + 1);
95
+ }
96
+ }
97
+ const buckets = sortValues(key, Array.from(counts.keys())).map((value) => ({
98
+ value,
99
+ label: labelFor(key, value, orgLabels),
100
+ count: counts.get(value) ?? 0
101
+ }));
102
+ return { key, label: FACET_TRIGGER_LABEL[key], buckets };
103
+ });
104
+ }
85
105
 
86
- export { FACET_ORDER, deriveFacets };
106
+ export { FACET_ORDER, deriveCounts, deriveFacets };
87
107
  //# sourceMappingURL=facetModel.esm.js.map
@@ -1 +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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"}
1
+ {"version":3,"file":"facetModel.esm.js","sources":["../../src/components/facetModel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport type { Entity } from '@backstage/catalog-model';\nimport type { RemixiconComponentType } from '@remixicon/react';\nimport { resolveCategoryIcon } from './categoryIcons';\nimport { humanise } from './grouping';\nimport {\n entityCategoryFacet,\n entityDepartment,\n entityLifecycle,\n entityOrg,\n entityOwnerLabel,\n} from './apigeeFacetFilters';\n\nexport type FacetKey = 'lifecycle' | 'org' | 'owner' | 'category' | 'department';\n\nexport interface FacetOption {\n value: string;\n label: string;\n}\n\nexport interface DerivedFacet {\n key: FacetKey;\n label: string;\n options: FacetOption[];\n /**\n * True when the facet has at least one selectable value. The toolbar ALWAYS renders every\n * facet (revised D-05); an `enabled: false` facet (no values in the loaded data) renders as a\n * disabled dropdown so all four filters stay visible.\n */\n enabled: boolean;\n /**\n * Per-option icon map — present ONLY on the `category` facet (UI-10).\n * Keys are option values (e.g. `'payments'`, `'other'`); values are allow-listed\n * RemixIcon components resolved via `resolveCategoryIcon`.\n * Not present on lifecycle / org / owner facets.\n */\n icons?: Record<string, RemixiconComponentType>;\n}\n\n/** Facet order (D-02): Lifecycle → Org → Owner → Category → Department. */\nexport const FACET_ORDER: FacetKey[] = [\n 'lifecycle',\n 'org',\n 'owner',\n 'category',\n 'department',\n];\n\n/**\n * Lifecycle options are ordered by severity (most common / least risky first), NOT\n * alphabetically (UI-SPEC Assumption 5). Unknown values fall to the end via `indexOf` = -1\n * being handled below.\n */\nconst LIFECYCLE_SEVERITY = ['production', 'experimental', 'deprecated', 'unknown'];\n\n/** Fixed facet trigger labels (UI-SPEC §Component Inventory — order-locked). */\nconst FACET_TRIGGER_LABEL: Record<FacetKey, string> = {\n lifecycle: 'Lifecycle',\n org: 'Org',\n owner: 'Owner',\n category: 'Category',\n department: 'Department',\n};\n\n/**\n * The single value extractor per facet slot. Category uses `entityCategoryFacet`\n * (undefined → 'other') so uncategorised APIs are selectable; the others reuse the\n * 06-01 extractors verbatim.\n */\nconst EXTRACT: Record<FacetKey, (e: Entity) => string | undefined> = {\n lifecycle: entityLifecycle,\n org: entityOrg,\n owner: entityOwnerLabel,\n category: entityCategoryFacet,\n department: entityDepartment,\n};\n\n/**\n * Per-facet option label (LOCKED — UI-SPEC §Facet Data + §Assumption 5):\n * - lifecycle → capitalise first char ('production' → 'Production')\n * - owner → value AS-IS (already humanised by `entityOwnerLabel`; do NOT humanise again)\n * - category → humanise(value), except 'other' → 'Other'\n * - org → explicit override for known org IDs from config, else humanise(value)\n */\nfunction labelFor(\n key: FacetKey,\n value: string,\n orgLabels?: Record<string, string>,\n): string {\n switch (key) {\n case 'lifecycle':\n return value.charAt(0).toUpperCase() + value.slice(1);\n case 'owner':\n return value;\n case 'category':\n return value === 'other' ? 'Other' : humanise(value);\n case 'org':\n return orgLabels?.[value] ?? humanise(value);\n default:\n return humanise(value);\n }\n}\n\n/**\n * Sort distinct facet values:\n * - lifecycle by fixed severity order\n * - category alphabetically with 'other' forced LAST (mirrors the grouped view)\n * - org / owner alphabetically (localeCompare)\n */\nfunction sortValues(key: FacetKey, values: string[]): string[] {\n if (key === 'lifecycle') {\n // Unrecognized lifecycles sort to the END, not the front (indexOf === -1 would\n // otherwise rank them before 'production'). Matches the documented intent.\n const rank = (v: string) => {\n const i = LIFECYCLE_SEVERITY.indexOf(v);\n return i === -1 ? LIFECYCLE_SEVERITY.length : i;\n };\n return [...values].sort((a, b) => rank(a) - rank(b));\n }\n if (key === 'category') {\n return [...values].sort((a, b) => {\n if (a === 'other') return 1;\n if (b === 'other') return -1;\n return a.localeCompare(b);\n });\n }\n return [...values].sort((a, b) => a.localeCompare(b));\n}\n\n/**\n * Derive stable, labelled, sorted facets from the FULL loaded entity set. Every facet in\n * FACET_ORDER is ALWAYS returned (revised D-05): `enabled` is true when the facet has >= 1\n * distinct value and false when the data has none — the toolbar renders a disabled dropdown for\n * the latter so all four filters stay visible even with uniform data. The caller passes the\n * pre-facet universe (`backendEntities.filter(isApigeeEntity)`) so options do not vanish as the\n * grid narrows.\n */\nexport function deriveFacets(\n entities: Entity[],\n overrides?: Record<string, string>,\n orgLabels?: Record<string, string>,\n): DerivedFacet[] {\n return FACET_ORDER.map(key => {\n const distinct = Array.from(\n new Set(entities.map(EXTRACT[key]).filter((v): v is string => !!v)),\n );\n const options = sortValues(key, distinct).map(value => ({\n value,\n label: labelFor(key, value, orgLabels),\n }));\n const icons: Record<string, RemixiconComponentType> | undefined =\n // Guard on options.length: Object.fromEntries([]) is {}, not undefined.\n key === 'category' && options.length > 0\n ? Object.fromEntries(\n options.map(opt => [\n opt.value,\n resolveCategoryIcon(\n opt.value === 'other' ? undefined : opt.value,\n overrides,\n ),\n ]),\n )\n : undefined;\n return {\n key,\n label: FACET_TRIGGER_LABEL[key],\n options,\n enabled: options.length > 0,\n ...(icons !== undefined ? { icons } : {}),\n };\n });\n}\n\n/** A single value bucket in a count card: the value, its display label, and the API count. */\nexport interface CountBucket {\n value: string;\n label: string;\n count: number;\n}\n\n/** A facet's per-value API counts, ready to render as a summary count card. */\nexport interface DerivedCount {\n key: FacetKey;\n label: string;\n buckets: CountBucket[];\n}\n\n/**\n * Aggregate per-value API counts for the requested facet keys from an entity set — the data\n * behind the summary count cards. Reuses the facet extractors, labels, and sort order so the\n * cards agree with the filter dropdowns. Entities whose value is absent for a key are omitted,\n * exactly like the facet options.\n */\nexport function deriveCounts(\n entities: Entity[],\n keys: FacetKey[],\n orgLabels?: Record<string, string>,\n): DerivedCount[] {\n return keys.map(key => {\n const counts = new Map<string, number>();\n for (const entity of entities) {\n const value = EXTRACT[key](entity);\n if (value) {\n counts.set(value, (counts.get(value) ?? 0) + 1);\n }\n }\n const buckets = sortValues(key, Array.from(counts.keys())).map(value => ({\n value,\n label: labelFor(key, value, orgLabels),\n count: counts.get(value) ?? 0,\n }));\n return { key, label: FACET_TRIGGER_LABEL[key], buckets };\n });\n}\n"],"names":[],"mappings":"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"}
@@ -1,31 +1,6 @@
1
1
  function humanise(slug) {
2
2
  return slug.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3
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
4
 
30
- export { groupByCategory, humanise };
5
+ export { humanise };
31
6
  //# sourceMappingURL=grouping.esm.js.map
@@ -1 +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;;;;"}
1
+ {"version":3,"file":"grouping.esm.js","sources":["../../src/components/grouping.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/** Humanise a slug: split on `-`/`_`, drop empties, capitalise each word. */\nexport function humanise(slug: string): string {\n return slug\n .split(/[-_]/)\n .filter(Boolean)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n"],"names":[],"mappings":"AAGO,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;;;;"}
@@ -1,57 +1,27 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import { ResponseErrorPanel, ItemCardGrid, EmptyState, LinkButton } from '@backstage/core-components';
3
- import { Box, Button, makeStyles } from '@material-ui/core';
3
+ import { Skeleton, Button } from '@backstage/ui';
4
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
- }));
5
+ const SR_ONLY = {
6
+ position: "absolute",
7
+ width: 1,
8
+ height: 1,
9
+ padding: 0,
10
+ margin: -1,
11
+ overflow: "hidden",
12
+ clip: "rect(0 0 0 0)",
13
+ whiteSpace: "nowrap",
14
+ border: 0
15
+ };
40
16
  function SkeletonGrid({ count = 8 } = {}) {
41
- const classes = useStyles();
42
17
  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,
18
+ /* @__PURE__ */ jsx("span", { style: SR_ONLY, "aria-live": "polite", children: "Loading Apigee APIs\u2026" }),
19
+ /* @__PURE__ */ jsx(ItemCardGrid, { children: Array.from({ length: count }).map((_, index) => /* @__PURE__ */ jsx(
20
+ Skeleton,
46
21
  {
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
- ]
22
+ width: "100%",
23
+ height: 172,
24
+ "aria-hidden": "true"
55
25
  },
56
26
  `apigee-skeleton-${index}`
57
27
  )) })
@@ -83,7 +53,7 @@ function NoMatchState({ onClearAll }) {
83
53
  missing: "content",
84
54
  title: "No APIs match your filters",
85
55
  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" })
56
+ action: /* @__PURE__ */ jsx(Button, { variant: "primary", onPress: onClearAll, children: "Clear all filters" })
87
57
  }
88
58
  );
89
59
  }
@@ -1 +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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"}
1
+ {"version":3,"file":"states.esm.js","sources":["../../src/components/states.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n EmptyState,\n ItemCardGrid,\n LinkButton,\n ResponseErrorPanel,\n} from '@backstage/core-components';\nimport { Button, Skeleton } from '@backstage/ui';\nimport type { CSSProperties } from 'react';\n\nconst SR_ONLY: CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Loading placeholder: `count` skeleton tiles inside the same responsive grid, using\n * @backstage/ui `Skeleton` blocks (D-07). The region is marked `aria-busy` with a\n * visually-hidden polite live region so assistive tech announces the load. (UI-05)\n */\nexport function SkeletonGrid({ count = 8 }: { count?: number } = {}) {\n return (\n <div aria-busy=\"true\">\n <span style={SR_ONLY} aria-live=\"polite\">\n Loading Apigee APIs…\n </span>\n <ItemCardGrid>\n {Array.from({ length: count }).map((_, index) => (\n <Skeleton\n key={`apigee-skeleton-${index}`}\n width=\"100%\"\n height={172}\n aria-hidden=\"true\"\n />\n ))}\n </ItemCardGrid>\n </div>\n );\n}\n\n/** Friendly empty state shown when no Apigee APIs are in the catalog (UI-05). */\nexport function ApiCatalogEmptyState() {\n return (\n <EmptyState\n missing=\"content\"\n title=\"No Apigee APIs found\"\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.\"\n action={\n <LinkButton\n to=\"/catalog?filters[kind]=API\"\n variant=\"contained\"\n color=\"primary\"\n >\n Browse the full catalog\n </LinkButton>\n }\n />\n );\n}\n\n/**\n * Distinct \"filtered to zero\" state (D-10) — shown when Apigee APIs exist but the active\n * filters/search exclude them all. Unlike `ApiCatalogEmptyState` (which links out to the full\n * catalog), this offers a non-destructive reset CTA (the button below) which is fully reversible,\n * so it uses primary/neutral styling (no error color, no confirmation dialog). (UI-08)\n */\nexport function NoMatchState({ onClearAll }: { onClearAll: () => void }) {\n return (\n <EmptyState\n missing=\"content\"\n title=\"No APIs match your filters\"\n description=\"Try removing a filter or clearing your search to see more APIs.\"\n action={\n <Button variant=\"primary\" onPress={onClearAll}>\n Clear all filters\n </Button>\n }\n />\n );\n}\n\n/** Error state shown when the catalog fetch fails (UI-05). */\nexport function ApiCatalogError({ error }: { error: Error }) {\n return <ResponseErrorPanel title=\"Couldn't load Apigee APIs\" error={error} />;\n}\n"],"names":[],"mappings":";;;;AAUA,MAAM,OAAA,GAAyB;AAAA,EAC7B,QAAA,EAAU,UAAA;AAAA,EACV,KAAA,EAAO,CAAA;AAAA,EACP,MAAA,EAAQ,CAAA;AAAA,EACR,OAAA,EAAS,CAAA;AAAA,EACT,MAAA,EAAQ,EAAA;AAAA,EACR,QAAA,EAAU,QAAA;AAAA,EACV,IAAA,EAAM,eAAA;AAAA,EACN,UAAA,EAAY,QAAA;AAAA,EACZ,MAAA,EAAQ;AACV,CAAA;AAOO,SAAS,aAAa,EAAE,KAAA,GAAQ,CAAA,EAAE,GAAwB,EAAC,EAAG;AACnE,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,WAAA,EAAU,MAAA,EACb,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,KAAA,EAAO,OAAA,EAAS,WAAA,EAAU,UAAS,QAAA,EAAA,2BAAA,EAEzC,CAAA;AAAA,oBACA,GAAA,CAAC,YAAA,EAAA,EACE,QAAA,EAAA,KAAA,CAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,KAAA,EAAO,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,EAAG,KAAA,qBACrC,GAAA;AAAA,MAAC,QAAA;AAAA,MAAA;AAAA,QAEC,KAAA,EAAM,MAAA;AAAA,QACN,MAAA,EAAQ,GAAA;AAAA,QACR,aAAA,EAAY;AAAA,OAAA;AAAA,MAHP,mBAAmB,KAAK,CAAA;AAAA,KAKhC,CAAA,EACH;AAAA,GAAA,EACF,CAAA;AAEJ;AAGO,SAAS,oBAAA,GAAuB;AACrC,EAAA,uBACE,GAAA;AAAA,IAAC,UAAA;AAAA,IAAA;AAAA,MACC,OAAA,EAAQ,SAAA;AAAA,MACR,KAAA,EAAM,sBAAA;AAAA,MACN,WAAA,EAAY,mJAAA;AAAA,MACZ,MAAA,kBACE,GAAA;AAAA,QAAC,UAAA;AAAA,QAAA;AAAA,UACC,EAAA,EAAG,4BAAA;AAAA,UACH,OAAA,EAAQ,WAAA;AAAA,UACR,KAAA,EAAM,SAAA;AAAA,UACP,QAAA,EAAA;AAAA;AAAA;AAED;AAAA,GAEJ;AAEJ;AAQO,SAAS,YAAA,CAAa,EAAE,UAAA,EAAW,EAA+B;AACvE,EAAA,uBACE,GAAA;AAAA,IAAC,UAAA;AAAA,IAAA;AAAA,MACC,OAAA,EAAQ,SAAA;AAAA,MACR,KAAA,EAAM,4BAAA;AAAA,MACN,WAAA,EAAY,iEAAA;AAAA,MACZ,wBACE,GAAA,CAAC,MAAA,EAAA,EAAO,SAAQ,SAAA,EAAU,OAAA,EAAS,YAAY,QAAA,EAAA,mBAAA,EAE/C;AAAA;AAAA,GAEJ;AAEJ;AAGO,SAAS,eAAA,CAAgB,EAAE,KAAA,EAAM,EAAqB;AAC3D,EAAA,uBAAO,GAAA,CAAC,kBAAA,EAAA,EAAmB,KAAA,EAAM,2BAAA,EAA4B,KAAA,EAAc,CAAA;AAC7E;;;;"}
@@ -2,7 +2,7 @@ import { useMemo, useState, useEffect, useCallback } from 'react';
2
2
  import { useSearchParams } from 'react-router-dom';
3
3
  import { useEntityList } from '@backstage/plugin-catalog-react';
4
4
  import { FieldFacetFilter, entityDepartment, entityCategoryFacet, entityOwnerLabel, entityOrg, entityLifecycle, ApiSearchFilter } from './apigeeFacetFilters.esm.js';
5
- import { parseSearchParams } from './apigeeSearchParams.esm.js';
5
+ import { parseSearchParams, selectOnlyFacetParams } from './apigeeSearchParams.esm.js';
6
6
  import { useDebouncedValue } from './useDebouncedValue.esm.js';
7
7
 
8
8
  const FACET_EXTRACT = {
@@ -64,6 +64,14 @@ function useApigeeFilters() {
64
64
  },
65
65
  [setSearchParams]
66
66
  );
67
+ const selectOnlyFacet = useCallback(
68
+ (key, value) => {
69
+ setSearchParams((prev) => selectOnlyFacetParams(prev, key, value), {
70
+ replace: true
71
+ });
72
+ },
73
+ [setSearchParams]
74
+ );
67
75
  const clearFacet = useCallback(
68
76
  (key) => {
69
77
  setSearchParams(
@@ -81,7 +89,15 @@ function useApigeeFilters() {
81
89
  setInputValue("");
82
90
  setSearchParams(new URLSearchParams(), { replace: true });
83
91
  }, [setSearchParams]);
84
- return { state, inputValue, setInputValue, toggleFacet, clearFacet, clearAll };
92
+ return {
93
+ state,
94
+ inputValue,
95
+ setInputValue,
96
+ toggleFacet,
97
+ selectOnlyFacet,
98
+ clearFacet,
99
+ clearAll
100
+ };
85
101
  }
86
102
 
87
103
  export { useApigeeFilters };
@@ -1 +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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"}
1
+ {"version":3,"file":"useApigeeFilters.esm.js","sources":["../../src/components/useApigeeFilters.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { useEntityList } from '@backstage/plugin-catalog-react';\nimport type { Entity } from '@backstage/catalog-model';\nimport {\n ApiSearchFilter,\n FieldFacetFilter,\n entityCategoryFacet,\n entityDepartment,\n entityLifecycle,\n entityOrg,\n entityOwnerLabel,\n} from './apigeeFacetFilters';\nimport { parseSearchParams, selectOnlyFacetParams } from './apigeeSearchParams';\nimport { useDebouncedValue } from './useDebouncedValue';\nimport type { ApigeeFilters } from './ApigeeApiCatalogPage';\nimport type { FacetKey } from './facetModel';\n\n/**\n * Value extractor per facet slot used to build the FILTER. Category uses the FACET extractor\n * (undefined → 'other'), so selecting the 'other' bucket matches uncategorised APIs — search uses\n * the real slug instead (06-01).\n */\nconst FACET_EXTRACT: Record<FacetKey, (e: Entity) => string | undefined> = {\n lifecycle: entityLifecycle,\n org: entityOrg,\n owner: entityOwnerLabel,\n category: entityCategoryFacet,\n department: entityDepartment,\n};\n\n/** Facet slot → the `ApigeeFilters` key its `FieldFacetFilter` registers under. */\ntype FacetFilterKey =\n | 'apigeeLifecycle'\n | 'apigeeOrgFacet'\n | 'apigeeOwner'\n | 'apigeeCategory'\n | 'apigeeDepartment';\nconst FILTER_KEY: Record<FacetKey, FacetFilterKey> = {\n lifecycle: 'apigeeLifecycle',\n org: 'apigeeOrgFacet',\n owner: 'apigeeOwner',\n category: 'apigeeCategory',\n department: 'apigeeDepartment',\n};\n\n/**\n * The `useSearchParams` ⇄ `updateFilters` bridge: the URL is the single source of truth. It parses\n * the clean facet + `q` keys into state, drives the provider's in-memory filters one-directionally\n * (URL → `updateFilters`, no re-fetch), and writes every change back with `{ replace: true }` (D-08)\n * so the address bar stays clean, shareable and reload-safe (UI-09). The search box keeps one local\n * `inputValue` (instant echo) with a single 200 ms trailing debounce (D-09) → URL; the URL never\n * writes back into the input, so there is no feedback loop (UI-06, UI-07).\n */\nexport function useApigeeFilters() {\n const [searchParams, setSearchParams] = useSearchParams();\n const { updateFilters } = useEntityList<ApigeeFilters>();\n\n // Depend on the stable STRING, not the per-render URLSearchParams object (Pitfall 4).\n const spString = searchParams.toString();\n const state = useMemo(() => parseSearchParams(searchParams), [spString]); // eslint-disable-line\n\n // Search text is the only local state; seeded once from the URL.\n const [inputValue, setInputValue] = useState(() => state.q);\n const debouncedQ = useDebouncedValue(inputValue, 200); // D-09\n\n // E2: debounced q → URL (replace), merged with the current facet params.\n useEffect(() => {\n setSearchParams(\n prev => {\n const next = new URLSearchParams(prev);\n if (debouncedQ.trim()) {\n next.set('q', debouncedQ.trim());\n } else {\n next.delete('q');\n }\n return next;\n },\n { replace: true },\n );\n }, [debouncedQ, setSearchParams]);\n\n // E1: URL → provider filters (one-directional). Runs on mount (seeds shared links) + on change.\n useEffect(() => {\n const patch: Partial<ApigeeFilters> = {};\n (Object.keys(FILTER_KEY) as FacetKey[]).forEach(k => {\n patch[FILTER_KEY[k]] = state.selected[k].length\n ? new FieldFacetFilter(FACET_EXTRACT[k], state.selected[k])\n : undefined;\n });\n patch.apigeeSearch = state.q.trim()\n ? new ApiSearchFilter(state.q)\n : undefined;\n updateFilters(patch);\n }, [spString, updateFilters]); // eslint-disable-line\n\n const toggleFacet = useCallback(\n (key: FacetKey, value: string) => {\n setSearchParams(\n prev => {\n const next = new URLSearchParams(prev);\n const cur = next.getAll(key);\n next.delete(key);\n const after = cur.includes(value)\n ? cur.filter(v => v !== value)\n : [...cur, value];\n after.forEach(v => next.append(key, v));\n return next;\n },\n { replace: true },\n );\n },\n [setSearchParams],\n );\n\n // Count-card single-select: the clicked value replaces any other card selection (see helper).\n const selectOnlyFacet = useCallback(\n (key: FacetKey, value: string) => {\n setSearchParams(prev => selectOnlyFacetParams(prev, key, value), {\n replace: true,\n });\n },\n [setSearchParams],\n );\n\n const clearFacet = useCallback(\n (key: FacetKey) => {\n setSearchParams(\n prev => {\n const next = new URLSearchParams(prev);\n next.delete(key);\n return next;\n },\n { replace: true },\n );\n },\n [setSearchParams],\n );\n\n const clearAll = useCallback(() => {\n setInputValue('');\n setSearchParams(new URLSearchParams(), { replace: true });\n }, [setSearchParams]);\n\n return {\n state,\n inputValue,\n setInputValue,\n toggleFacet,\n selectOnlyFacet,\n clearFacet,\n clearAll,\n };\n}\n"],"names":[],"mappings":"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"}
@@ -1 +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;;;;"}
1
+ {"version":3,"file":"useDebouncedValue.esm.js","sources":["../../src/components/useDebouncedValue.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { useEffect, useState } from 'react';\n\n/**\n * Trailing-edge debounce: returns `value` immediately, then re-emits the latest `value`\n * after `delayMs` of no change. Rapid successive changes cancel the prior timer via\n * `clearTimeout`, so only the trailing value is emitted. No external dependency (D-09).\n */\nexport function useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debounced, setDebounced] = useState(value);\n useEffect(() => {\n const id = setTimeout(() => setDebounced(value), delayMs);\n return () => clearTimeout(id);\n }, [value, delayMs]);\n return debounced;\n}\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;;;;"}
@@ -2,8 +2,9 @@ const ANNOTATION_APIGEE_ORGS = "apigee.com/orgs";
2
2
  const ANNOTATION_APIGEE_ORG = "apigee.com/org";
3
3
  const ANNOTATION_HAS_SPEC = "apigee.com/has-spec";
4
4
  const ANNOTATION_CATEGORY = "apigee.com/category";
5
- const ANNOTATION_API_HUB_VERSION = "apigee.com/api-hub-version";
5
+ const ANNOTATION_VERSION = "apigee.com/version";
6
+ const ANNOTATION_PROXY_NAME = "apigee.com/proxy-name";
6
7
  const LABEL_DEPARTMENT = "department";
7
8
 
8
- export { ANNOTATION_APIGEE_ORG, ANNOTATION_APIGEE_ORGS, ANNOTATION_API_HUB_VERSION, ANNOTATION_CATEGORY, ANNOTATION_HAS_SPEC, LABEL_DEPARTMENT };
9
+ export { ANNOTATION_APIGEE_ORG, ANNOTATION_APIGEE_ORGS, ANNOTATION_CATEGORY, ANNOTATION_HAS_SPEC, ANNOTATION_PROXY_NAME, ANNOTATION_VERSION, LABEL_DEPARTMENT };
9
10
  //# sourceMappingURL=constants.esm.js.map
@@ -1 +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;;;;"}
1
+ {"version":3,"file":"constants.esm.js","sources":["../src/constants.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n// Apigee catalog annotation keys. Authoritative source:\n// plugins/apigee-backend/src/lib/entity-builder.ts (verified 2026-07-09).\n\n/** Org annotation written by proxyToEntities (PLURAL, proxy-derived API entities). */\nexport const ANNOTATION_APIGEE_ORGS = 'apigee.com/orgs';\n/** Org annotation written by hubOnlyToEntity (SINGULAR, hub-only API entities). */\nexport const ANNOTATION_APIGEE_ORG = 'apigee.com/org';\n/** 'true' | 'false' — set by applyHubMetadata when a spec is embedded or linked. */\nexport const ANNOTATION_HAS_SPEC = 'apigee.com/has-spec';\n/** Comma-joined slugified categories (absent in the current seed dump). */\nexport const ANNOTATION_CATEGORY = 'apigee.com/category';\n/** Full API Hub resource name — present only on hub-only entities. */\nexport const ANNOTATION_API_HUB_ID = 'apigee.com/api-hub-id';\n/**\n * Spec `info.version` — emitted by the backend on both Component and API entities\n * (entity-provider.ts / applyHubMetadata). Primary source for the card version line.\n */\nexport const ANNOTATION_VERSION = 'apigee.com/version';\n/**\n * Proxy name — when `apigee.com/version` is absent, the card derives the version\n * from this name's trailing `-v<major>` segment (e.g. `...-v4` -> `4.0.0`).\n */\nexport const ANNOTATION_PROXY_NAME = 'apigee.com/proxy-name';\n\n/**\n * Department label key (`metadata.labels.department`). Written by the backend's\n * applyHubMetadata from the API Hub \"Business unit\" attribute.\n */\nexport const LABEL_DEPARTMENT = 'department';\n"],"names":[],"mappings":"AAKO,MAAM,sBAAA,GAAyB;AAE/B,MAAM,qBAAA,GAAwB;AAE9B,MAAM,mBAAA,GAAsB;AAE5B,MAAM,mBAAA,GAAsB;AAO5B,MAAM,kBAAA,GAAqB;AAK3B,MAAM,qBAAA,GAAwB;AAM9B,MAAM,gBAAA,GAAmB;;;;"}
package/dist/index.d.ts CHANGED
@@ -9,8 +9,8 @@ declare const apigeePage: _backstage_frontend_plugin_api.OverridableExtensionDef
9
9
  title: string | undefined;
10
10
  };
11
11
  configInput: {
12
- title?: string | undefined;
13
12
  path?: string | undefined;
13
+ title?: string | undefined;
14
14
  };
15
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
16
  optional: true;
@@ -64,8 +64,8 @@ declare const apigeePlugin: _backstage_frontend_plugin_api.OverridableFrontendPl
64
64
  title: string | undefined;
65
65
  };
66
66
  configInput: {
67
- title?: string | undefined;
68
67
  path?: string | undefined;
68
+ title?: string | undefined;
69
69
  };
70
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
71
  optional: true;
@@ -0,0 +1,29 @@
1
+ function styleInject(css, ref) {
2
+ if ( ref === void 0 ) ref = {};
3
+ var insertAt = ref.insertAt;
4
+
5
+ if (!css || typeof document === 'undefined') { return; }
6
+
7
+ var head = document.head || document.getElementsByTagName('head')[0];
8
+ var style = document.createElement('style');
9
+ style.type = 'text/css';
10
+
11
+ if (insertAt === 'top') {
12
+ if (head.firstChild) {
13
+ head.insertBefore(style, head.firstChild);
14
+ } else {
15
+ head.appendChild(style);
16
+ }
17
+ } else {
18
+ head.appendChild(style);
19
+ }
20
+
21
+ if (style.styleSheet) {
22
+ style.styleSheet.cssText = css;
23
+ } else {
24
+ style.appendChild(document.createTextNode(css));
25
+ }
26
+ }
27
+
28
+ export { styleInject as default };
29
+ //# sourceMappingURL=style-inject.es.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"style-inject.es.esm.js","sources":["../../../../../../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":"AAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,MAAM,GAAG,GAAG,GAAG,EAAE;AAChC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ;;AAE7B,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,CAAC;;AAEzD,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACtE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC;AAC7C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU;;AAEzB,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC;AAC/C,IAAI,CAAC,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;AAC7B,IAAI;AACJ,EAAE,CAAC,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;AAC3B,EAAE;;AAEF,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG;AAClC,EAAE,CAAC,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;AACnD,EAAE;AACF;;;;","x_google_ignoreList":[0]}
@@ -1,14 +1,14 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { PageBlueprint, createFrontendModule, createFrontendPlugin } from '@backstage/frontend-plugin-api';
3
3
  import { NavContentBlueprint } from '@backstage/plugin-app-react';
4
- import LayersIcon from '@material-ui/icons/Layers';
4
+ import { RiStackLine } from '@remixicon/react';
5
5
  import { rootRouteRef } from './routes.esm.js';
6
6
  import { ApigeeNavContent } from './components/ApigeeNavContent.esm.js';
7
7
 
8
8
  const apigeePage = PageBlueprint.make({
9
9
  params: {
10
10
  title: "API Catalog",
11
- icon: /* @__PURE__ */ jsx(LayersIcon, {}),
11
+ icon: /* @__PURE__ */ jsx(RiStackLine, {}),
12
12
  routeRef: rootRouteRef,
13
13
  path: "/apigee",
14
14
  loader: () => import('./components/ApigeeApiCatalogPage.esm.js').then(
@@ -1 +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;;;;"}
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n createFrontendModule,\n createFrontendPlugin,\n PageBlueprint,\n} from '@backstage/frontend-plugin-api';\nimport { NavContentBlueprint } from '@backstage/plugin-app-react';\nimport { RiStackLine } from '@remixicon/react';\nimport { rootRouteRef } from './routes';\nimport { ApigeeNavContent } from './components/ApigeeNavContent';\n\nexport const apigeePage = PageBlueprint.make({\n params: {\n title: 'API Catalog',\n icon: <RiStackLine />,\n routeRef: rootRouteRef,\n path: '/apigee',\n loader: () =>\n import('./components/ApigeeApiCatalogPage').then(\n ({ ApigeeApiCatalogPage }) => <ApigeeApiCatalogPage />,\n ),\n },\n});\n\n/**\n * Frontend module that overrides the app nav bar to show an expandable\n * \"API Hub\" group containing all apigee plugin pages as sub-items.\n *\n * Add this to your app's `createApp({ features })` to activate the grouped nav.\n *\n * @example\n * ```ts\n * createApp({ features: [apigeePlugin, apigeeNavModule] });\n * ```\n */\nexport const apigeeNavModule = createFrontendModule({\n pluginId: 'app',\n extensions: [\n NavContentBlueprint.make({\n params: {\n component: ApigeeNavContent,\n },\n }),\n ],\n});\n\nexport const apigeePlugin = createFrontendPlugin({\n pluginId: 'apigee',\n title: 'API Hub',\n extensions: [apigeePage],\n routes: {\n root: rootRouteRef,\n },\n});\n"],"names":[],"mappings":";;;;;;;AAWO,MAAM,UAAA,GAAa,cAAc,IAAA,CAAK;AAAA,EAC3C,MAAA,EAAQ;AAAA,IACN,KAAA,EAAO,aAAA;AAAA,IACP,IAAA,sBAAO,WAAA,EAAA,EAAY,CAAA;AAAA,IACnB,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;;;;"}
@@ -1 +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;;;;"}
1
+ {"version":3,"file":"routes.esm.js","sources":["../src/routes.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { createRouteRef } from '@backstage/frontend-plugin-api';\n\nexport const rootRouteRef = createRouteRef();\n"],"names":[],"mappings":";;AAGO,MAAM,eAAe,cAAA;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nospt/backstage-plugin-apigee",
3
- "version": "1.1.0-rc2",
3
+ "version": "1.1.0-rc4",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Backstage frontend plugin bootstrap for Apigee integration",
6
6
  "author": "NOS SGPS, S.A.",
@@ -48,32 +48,34 @@
48
48
  },
49
49
  "dependencies": {
50
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"
51
+ "@backstage/core-components": "^0.18.10",
52
+ "@backstage/core-plugin-api": "^1.12.6",
53
+ "@backstage/frontend-plugin-api": "^0.17.0",
54
+ "@backstage/plugin-catalog-react": "^3.0.0",
55
+ "@backstage/ui": "^0.15.0",
56
+ "@nospt/backstage-plugin-apigee-common": "^1.1.0-rc4",
57
+ "@remixicon/react": "^4.6.0"
58
58
  },
59
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",
60
+ "react": "^18.0.0",
61
+ "react-dom": "^18.0.0",
62
62
  "react-router-dom": "^6.0.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@backstage/cli": "^0.36.0",
66
66
  "@backstage/config": "^1.3.7",
67
67
  "@backstage/frontend-defaults": "^0.5.0",
68
- "@backstage/frontend-test-utils": "^0.5.1",
68
+ "@backstage/frontend-test-utils": "^0.6.0",
69
69
  "@backstage/plugin-app-react": "^0.2.2",
70
70
  "@backstage/plugin-catalog": "^2.0.1",
71
- "@backstage/ui": "^0.13.2",
72
71
  "@playwright/test": "^1.49.0",
72
+ "@testing-library/dom": "^10.0.0",
73
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",
74
+ "@testing-library/react": "^16.0.0",
75
+ "@types/react": "^18",
76
+ "@types/react-dom": "^18",
77
+ "react": "^18.0.0",
78
+ "react-dom": "^18.0.0",
77
79
  "react-router-dom": "^6.0.0"
78
80
  },
79
81
  "files": [