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

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 (41) hide show
  1. package/config.d.ts +0 -10
  2. package/dist/components/ApiCard.esm.js +24 -15
  3. package/dist/components/ApiCard.esm.js.map +1 -1
  4. package/dist/components/ApiCard.module.css.esm.js +2 -2
  5. package/dist/components/ApiCardGrid.esm.js +3 -11
  6. package/dist/components/ApiCardGrid.esm.js.map +1 -1
  7. package/dist/components/ApiDetailsDrawer.esm.js +335 -0
  8. package/dist/components/ApiDetailsDrawer.esm.js.map +1 -0
  9. package/dist/components/ApiDetailsDrawer.module.css.esm.js +8 -0
  10. package/dist/components/ApiDetailsDrawer.module.css.esm.js.map +1 -0
  11. package/dist/components/ApiTable.esm.js +10 -2
  12. package/dist/components/ApiTable.esm.js.map +1 -1
  13. package/dist/components/ApiTable.module.css.esm.js +2 -2
  14. package/dist/components/ApigeeApiCatalogPage.esm.js +62 -49
  15. package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -1
  16. package/dist/components/FilterToolbar.esm.js +27 -26
  17. package/dist/components/FilterToolbar.esm.js.map +1 -1
  18. package/dist/components/FilterToolbar.module.css.esm.js +2 -2
  19. package/dist/components/apiCardViewModel.esm.js +1 -2
  20. package/dist/components/apiCardViewModel.esm.js.map +1 -1
  21. package/dist/components/apiDetailViewModel.esm.js +60 -0
  22. package/dist/components/apiDetailViewModel.esm.js.map +1 -0
  23. package/dist/components/apigeeFacetFilters.esm.js +3 -6
  24. package/dist/components/apigeeFacetFilters.esm.js.map +1 -1
  25. package/dist/components/apigeeSearchParams.esm.js +0 -1
  26. package/dist/components/apigeeSearchParams.esm.js.map +1 -1
  27. package/dist/components/facetModel.esm.js +3 -29
  28. package/dist/components/facetModel.esm.js.map +1 -1
  29. package/dist/components/useApigeeFilters.esm.js +1 -3
  30. package/dist/components/useApigeeFilters.esm.js.map +1 -1
  31. package/dist/constants.esm.js +1 -2
  32. package/dist/constants.esm.js.map +1 -1
  33. package/dist/index.d.ts +52 -1
  34. package/dist/index.esm.js +1 -1
  35. package/dist/plugin.esm.js +17 -7
  36. package/dist/plugin.esm.js.map +1 -1
  37. package/dist/routes.esm.js +2 -1
  38. package/dist/routes.esm.js.map +1 -1
  39. package/package.json +2 -2
  40. package/dist/components/categoryIcons.esm.js +0 -53
  41. package/dist/components/categoryIcons.esm.js.map +0 -1
@@ -0,0 +1,60 @@
1
+ import { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants.esm.js';
2
+ import { humanise } from './grouping.esm.js';
3
+ import { toApiCardViewModel } from './apiCardViewModel.esm.js';
4
+
5
+ function slugify(name) {
6
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
7
+ }
8
+ const ENV_REVISION_KEY = /^apigee\.com\/([a-z0-9-]+)\.env\.([a-z0-9-]+)\.revision$/;
9
+ function envsByOrgSlug(component) {
10
+ const byOrg = /* @__PURE__ */ new Map();
11
+ for (const [key, revision] of Object.entries(component?.metadata.annotations ?? {})) {
12
+ const match = ENV_REVISION_KEY.exec(key);
13
+ if (!match) {
14
+ continue;
15
+ }
16
+ const [, orgSlug, envSlug] = match;
17
+ const list = byOrg.get(orgSlug) ?? [];
18
+ list.push({ name: envSlug, revision });
19
+ byOrg.set(orgSlug, list);
20
+ }
21
+ return byOrg;
22
+ }
23
+ function toRuntimeOrgs(api, component, orgLabels) {
24
+ const annotations = api.metadata.annotations ?? {};
25
+ const raw = annotations[ANNOTATION_APIGEE_ORGS] ?? annotations[ANNOTATION_APIGEE_ORG] ?? "";
26
+ const orgValues = raw.split(",").map((value) => value.trim()).filter(Boolean);
27
+ const envMap = envsByOrgSlug(component);
28
+ return orgValues.map((value) => ({
29
+ value,
30
+ label: orgLabels?.[value] ?? humanise(value),
31
+ environments: (envMap.get(slugify(value)) ?? []).sort(
32
+ (a, b) => a.name.localeCompare(b.name)
33
+ )
34
+ }));
35
+ }
36
+ function firstSourceRepo(entity) {
37
+ const link = (entity.metadata.links ?? []).find((l) => /^https?:\/\//i.test(l.url));
38
+ return link ? { url: link.url, title: link.title || "Source repository" } : void 0;
39
+ }
40
+ function specFileName(baseName, definition) {
41
+ const ext = definition.trim().startsWith("{") ? "json" : "yaml";
42
+ return `${baseName}.${ext}`;
43
+ }
44
+ function toApiDetailViewModel(api, component, orgLabels) {
45
+ const base = toApiCardViewModel(api);
46
+ const spec = api.spec ?? {};
47
+ return {
48
+ ...base,
49
+ kind: api.kind,
50
+ type: spec.type || void 0,
51
+ ownerRef: spec.owner || void 0,
52
+ tags: api.metadata.tags ?? [],
53
+ definition: typeof spec.definition === "string" ? spec.definition : "",
54
+ sourceRepo: firstSourceRepo(api),
55
+ runtimeOrgs: toRuntimeOrgs(api, component, orgLabels)
56
+ };
57
+ }
58
+
59
+ export { specFileName, toApiDetailViewModel, toRuntimeOrgs };
60
+ //# sourceMappingURL=apiDetailViewModel.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiDetailViewModel.esm.js","sources":["../../src/components/apiDetailViewModel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport type { Entity } from '@backstage/catalog-model';\nimport { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG } from '../constants';\nimport { humanise } from './grouping';\nimport { toApiCardViewModel, type ApiCardViewModel } from './apiCardViewModel';\n\n/** One deployed environment with the revision running there (per org). */\nexport interface ApiRuntimeEnv {\n /** Environment slug (e.g. 'prod-1'), as recorded on the Component annotation. */\n name: string;\n /** Deployed proxy revision in this environment (e.g. '3'). */\n revision: string;\n}\n\n/** An Apigee org this API is published to, with its deployed environments. */\nexport interface ApiRuntimeOrg {\n /** Raw org value — matches the `apigee.com/orgs` entry and the org facet identity. */\n value: string;\n /** Display label (config `orgLabels` override, else humanised). */\n label: string;\n /** Deployed environments with revisions; empty when nothing is deployed. */\n environments: ApiRuntimeEnv[];\n}\n\n/** A source-code repository link, read from `metadata.links`. */\nexport interface ApiSourceRepo {\n url: string;\n title: string;\n}\n\n/**\n * Everything the details drawer renders for one API. Extends the card view-model\n * (the shared header/badge/owner fields) with the richer overview, runtime and spec data.\n */\nexport interface ApiDetailViewModel extends ApiCardViewModel {\n /** entity.kind — always 'API' here; shown as an overview row. */\n kind: string;\n /** spec.type (openapi, other, …) — omitted when absent. */\n type?: string;\n /** Raw spec.owner entity ref (ownerLabel is the display text). */\n ownerRef?: string;\n /** metadata.tags — [] when none. */\n tags: string[];\n /** spec.definition — the embedded OpenAPI/AsyncAPI document; '' when not embedded. */\n definition: string;\n /** First source-repository link from metadata.links. */\n sourceRepo?: ApiSourceRepo;\n /** Per-org runtime (environments + revisions). */\n runtimeOrgs: ApiRuntimeOrg[];\n}\n\n/**\n * Frontend mirror of the backend `slugify` (entity-builder.ts): lowercase, non-alphanumeric\n * runs → single hyphen, trimmed. Used to match an org value to the Component's\n * `apigee.com/<orgSlug>.env.<envSlug>.revision` annotation keys.\n */\nfunction slugify(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/** `apigee.com/<orgSlug>.env.<envSlug>.revision` — slugs never contain '.', so this is unambiguous. */\nconst ENV_REVISION_KEY = /^apigee\\.com\\/([a-z0-9-]+)\\.env\\.([a-z0-9-]+)\\.revision$/;\n\n/** Group a Component's per-environment revision annotations by org slug. */\nfunction envsByOrgSlug(component: Entity | undefined): Map<string, ApiRuntimeEnv[]> {\n const byOrg = new Map<string, ApiRuntimeEnv[]>();\n for (const [key, revision] of Object.entries(component?.metadata.annotations ?? {})) {\n const match = ENV_REVISION_KEY.exec(key);\n if (!match) {\n continue;\n }\n const [, orgSlug, envSlug] = match;\n const list = byOrg.get(orgSlug) ?? [];\n list.push({ name: envSlug, revision });\n byOrg.set(orgSlug, list);\n }\n return byOrg;\n}\n\n/**\n * Resolve the runtime orgs for an API. Org identity + order come from the API's\n * `apigee.com/orgs` (or singular `org`) annotation — the same value the facets use — while the\n * deployed environments/revisions come from the matching Component entity (Component-only\n * annotations). Hub-only APIs have no Component, so their orgs render with empty environment lists.\n */\nexport function toRuntimeOrgs(\n api: Entity,\n component: Entity | undefined,\n orgLabels?: Record<string, string>,\n): ApiRuntimeOrg[] {\n const annotations = api.metadata.annotations ?? {};\n const raw =\n annotations[ANNOTATION_APIGEE_ORGS] ?? annotations[ANNOTATION_APIGEE_ORG] ?? '';\n const orgValues = raw\n .split(',')\n .map(value => value.trim())\n .filter(Boolean);\n const envMap = envsByOrgSlug(component);\n return orgValues.map(value => ({\n value,\n label: orgLabels?.[value] ?? humanise(value),\n environments: (envMap.get(slugify(value)) ?? []).sort((a, b) =>\n a.name.localeCompare(b.name),\n ),\n }));\n}\n\n/** First http(s) link from metadata.links — the backend records the GitHub source repository here. */\nfunction firstSourceRepo(entity: Entity): ApiSourceRepo | undefined {\n const link = (entity.metadata.links ?? []).find(l => /^https?:\\/\\//i.test(l.url));\n return link ? { url: link.url, title: link.title || 'Source repository' } : undefined;\n}\n\n/** Download filename for a spec: `.json` for JSON, else `.yaml`. */\nexport function specFileName(baseName: string, definition: string): string {\n const ext = definition.trim().startsWith('{') ? 'json' : 'yaml';\n return `${baseName}.${ext}`;\n}\n\n/**\n * PURE mapper: an Apigee `kind: API` entity (plus its optional Component sibling for runtime\n * data) → the view-model the details drawer renders. Reuses `toApiCardViewModel` for the shared\n * header/badge/owner fields.\n */\nexport function toApiDetailViewModel(\n api: Entity,\n component?: Entity,\n orgLabels?: Record<string, string>,\n): ApiDetailViewModel {\n const base = toApiCardViewModel(api);\n const spec = (api.spec ?? {}) as Record<string, unknown>;\n return {\n ...base,\n kind: api.kind,\n type: (spec.type as string | undefined) || undefined,\n ownerRef: (spec.owner as string | undefined) || undefined,\n tags: api.metadata.tags ?? [],\n definition: typeof spec.definition === 'string' ? spec.definition : '',\n sourceRepo: firstSourceRepo(api),\n runtimeOrgs: toRuntimeOrgs(api, component, orgLabels),\n };\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { parseEntityRef } from '@backstage/catalog-model';
2
2
  import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
3
- import { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG, LABEL_DEPARTMENT, ANNOTATION_CATEGORY } from '../constants.esm.js';
3
+ import { ANNOTATION_APIGEE_ORGS, ANNOTATION_APIGEE_ORG, LABEL_DEPARTMENT } from '../constants.esm.js';
4
4
 
5
5
  function isApigeeEntity(e) {
6
6
  const a = e.metadata.annotations ?? {};
@@ -18,8 +18,6 @@ const entityOrg = (e) => {
18
18
  const a = e.metadata.annotations ?? {};
19
19
  return (a[ANNOTATION_APIGEE_ORGS] ?? a[ANNOTATION_APIGEE_ORG])?.split(",")[0]?.trim() || void 0;
20
20
  };
21
- const entityCategory = (e) => (e.metadata.annotations ?? {})[ANNOTATION_CATEGORY]?.split(",")[0]?.trim() || void 0;
22
- const entityCategoryFacet = (e) => entityCategory(e) ?? "other";
23
21
  const entityDepartment = (e) => (e.metadata.labels ?? {})[LABEL_DEPARTMENT]?.trim() || void 0;
24
22
  class FieldFacetFilter {
25
23
  constructor(extract, values) {
@@ -38,8 +36,7 @@ function apiSearchHaystack(e) {
38
36
  e.metadata.title ?? e.metadata.name,
39
37
  e.metadata.description ?? "",
40
38
  entityOwnerLabel(e) ?? "",
41
- entityOrg(e) ?? "",
42
- entityCategory(e) ?? ""
39
+ entityOrg(e) ?? ""
43
40
  ].join(" ").toLowerCase();
44
41
  }
45
42
  class ApiSearchFilter {
@@ -58,5 +55,5 @@ class ApiKindFilter {
58
55
  }
59
56
  }
60
57
 
61
- export { ApiKindFilter, ApiSearchFilter, FieldFacetFilter, apiSearchHaystack, entityCategory, entityCategoryFacet, entityDepartment, entityLifecycle, entityOrg, entityOwnerLabel, isApigeeEntity };
58
+ export { ApiKindFilter, ApiSearchFilter, FieldFacetFilter, apiSearchHaystack, entityDepartment, entityLifecycle, entityOrg, entityOwnerLabel, isApigeeEntity };
62
59
  //# sourceMappingURL=apigeeFacetFilters.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"apigeeFacetFilters.esm.js","sources":["../../src/components/apigeeFacetFilters.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\nimport type { EntityFilter } from '@backstage/plugin-catalog-react';\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\nimport {\n ANNOTATION_APIGEE_ORGS,\n ANNOTATION_APIGEE_ORG,\n ANNOTATION_CATEGORY,\n LABEL_DEPARTMENT,\n} from '../constants';\n\n/** Dual-key is-Apigee gate (same rule as the existing ApigeeOrgFilter). */\nexport function isApigeeEntity(e: Entity): boolean {\n const a = e.metadata.annotations ?? {};\n return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;\n}\n\n// --- Pure field extractors: the ONE place each facet's value is defined. Reused by\n// deriveFacets(), the facet predicates, and the search haystack. Unit-testable. ---\nexport const entityLifecycle = (e: Entity): string | undefined =>\n (e.spec?.lifecycle as string | undefined)?.trim() || undefined;\n\nexport const entityOwnerLabel = (e: Entity): string | undefined => {\n const owner = e.spec?.owner as string | undefined;\n return owner\n ? humanizeEntityRef(\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\n { defaultKind: 'group' },\n ) // 'group:default/payments-team' -> 'payments-team'\n : undefined;\n};\n\nexport const entityOrg = (e: Entity): string | undefined => {\n const a = e.metadata.annotations ?? {};\n return (\n (a[ANNOTATION_APIGEE_ORGS] ?? a[ANNOTATION_APIGEE_ORG])?.split(',')[0]?.trim() ||\n undefined\n );\n};\n\nexport const entityCategory = (e: Entity): string | undefined =>\n (e.metadata.annotations ?? {})[ANNOTATION_CATEGORY]?.split(',')[0]?.trim() ||\n undefined;\n\n/**\n * Facet-only category value: maps an uncategorised entity to the 'other' bucket\n * (LOCKED UI-SPEC Facet Data). Search uses `entityCategory` (the REAL slug) instead,\n * so a search for \"other\" does not match uncategorised APIs.\n */\nexport const entityCategoryFacet = (e: Entity): string =>\n entityCategory(e) ?? 'other';\n\n/** Department facet value: the `department` label written from the API Hub \"Business unit\". */\nexport const entityDepartment = (e: Entity): string | undefined =>\n (e.metadata.labels ?? {})[LABEL_DEPARTMENT]?.trim() || undefined;\n\n/** Generic single-field multi-value facet. Within-facet OR. No URL seam / getCatalogFilters. */\nexport class FieldFacetFilter implements EntityFilter {\n constructor(\n private readonly extract: (e: Entity) => string | undefined,\n public readonly values: string[],\n ) {}\n filterEntity(entity: Entity): boolean {\n const v = this.extract(entity);\n return v !== undefined && this.values.includes(v); // OR across selected values\n }\n}\n\n/** Broad \"find anything\" search: name + description + owner + org + category (D-09). */\nexport function apiSearchHaystack(e: Entity): string {\n return [\n e.metadata.title ?? e.metadata.name,\n e.metadata.description ?? '',\n entityOwnerLabel(e) ?? '',\n entityOrg(e) ?? '',\n entityCategory(e) ?? '',\n ]\n .join(' ')\n .toLowerCase();\n}\n\nexport class ApiSearchFilter implements EntityFilter {\n private readonly needle: string;\n constructor(query: string) {\n this.needle = query.trim().toLowerCase();\n }\n filterEntity(entity: Entity): boolean {\n // String.includes ONLY — never build a RegExp from user input (ReDoS guard).\n return this.needle === '' || apiSearchHaystack(entity).includes(this.needle);\n }\n}\n\n/**\n * Replaces EntityKindFilter('api','API'): keeps the backend kind=API filter but exposes no\n * URL seam, so the provider does not serialize `filters[kind]=API` into the URL (D-06).\n */\nexport class ApiKindFilter implements EntityFilter {\n readonly value = 'API';\n getCatalogFilters(): Record<string, string> {\n return { kind: this.value };\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"apigeeFacetFilters.esm.js","sources":["../../src/components/apigeeFacetFilters.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { type Entity, parseEntityRef } from '@backstage/catalog-model';\nimport type { EntityFilter } from '@backstage/plugin-catalog-react';\nimport { humanizeEntityRef } from '@backstage/plugin-catalog-react';\nimport {\n ANNOTATION_APIGEE_ORGS,\n ANNOTATION_APIGEE_ORG,\n LABEL_DEPARTMENT,\n} from '../constants';\n\n/** Dual-key is-Apigee gate (same rule as the existing ApigeeOrgFilter). */\nexport function isApigeeEntity(e: Entity): boolean {\n const a = e.metadata.annotations ?? {};\n return ANNOTATION_APIGEE_ORGS in a || ANNOTATION_APIGEE_ORG in a;\n}\n\n// --- Pure field extractors: the ONE place each facet's value is defined. Reused by\n// deriveFacets(), the facet predicates, and the search haystack. Unit-testable. ---\nexport const entityLifecycle = (e: Entity): string | undefined =>\n (e.spec?.lifecycle as string | undefined)?.trim() || undefined;\n\nexport const entityOwnerLabel = (e: Entity): string | undefined => {\n const owner = e.spec?.owner as string | undefined;\n return owner\n ? humanizeEntityRef(\n parseEntityRef(owner, { defaultKind: 'group', defaultNamespace: 'default' }),\n { defaultKind: 'group' },\n ) // 'group:default/payments-team' -> 'payments-team'\n : undefined;\n};\n\nexport const entityOrg = (e: Entity): string | undefined => {\n const a = e.metadata.annotations ?? {};\n return (\n (a[ANNOTATION_APIGEE_ORGS] ?? a[ANNOTATION_APIGEE_ORG])?.split(',')[0]?.trim() ||\n undefined\n );\n};\n\n/** Department facet value: the `department` label written from the API Hub \"Business unit\". */\nexport const entityDepartment = (e: Entity): string | undefined =>\n (e.metadata.labels ?? {})[LABEL_DEPARTMENT]?.trim() || undefined;\n\n/** Generic single-field multi-value facet. Within-facet OR. No URL seam / getCatalogFilters. */\nexport class FieldFacetFilter implements EntityFilter {\n constructor(\n private readonly extract: (e: Entity) => string | undefined,\n public readonly values: string[],\n ) {}\n filterEntity(entity: Entity): boolean {\n const v = this.extract(entity);\n return v !== undefined && this.values.includes(v); // OR across selected values\n }\n}\n\n/** Broad \"find anything\" search: name + description + owner + org (D-09). */\nexport function apiSearchHaystack(e: Entity): string {\n return [\n e.metadata.title ?? e.metadata.name,\n e.metadata.description ?? '',\n entityOwnerLabel(e) ?? '',\n entityOrg(e) ?? '',\n ]\n .join(' ')\n .toLowerCase();\n}\n\nexport class ApiSearchFilter implements EntityFilter {\n private readonly needle: string;\n constructor(query: string) {\n this.needle = query.trim().toLowerCase();\n }\n filterEntity(entity: Entity): boolean {\n // String.includes ONLY — never build a RegExp from user input (ReDoS guard).\n return this.needle === '' || apiSearchHaystack(entity).includes(this.needle);\n }\n}\n\n/**\n * Replaces EntityKindFilter('api','API'): keeps the backend kind=API filter but exposes no\n * URL seam, so the provider does not serialize `filters[kind]=API` into the URL (D-06).\n */\nexport class ApiKindFilter implements EntityFilter {\n readonly value = 'API';\n getCatalogFilters(): Record<string, string> {\n return { kind: this.value };\n }\n}\n"],"names":[],"mappings":";;;;AAWO,SAAS,eAAe,CAAA,EAAoB;AACjD,EAAA,MAAM,CAAA,GAAI,CAAA,CAAE,QAAA,CAAS,WAAA,IAAe,EAAC;AACrC,EAAA,OAAO,sBAAA,IAA0B,KAAK,qBAAA,IAAyB,CAAA;AACjE;AAIO,MAAM,kBAAkB,CAAC,CAAA,KAC7B,EAAE,IAAA,EAAM,SAAA,EAAkC,MAAK,IAAK;AAEhD,MAAM,gBAAA,GAAmB,CAAC,CAAA,KAAkC;AACjE,EAAA,MAAM,KAAA,GAAQ,EAAE,IAAA,EAAM,KAAA;AACtB,EAAA,OAAO,KAAA,GACH,iBAAA;AAAA,IACE,eAAe,KAAA,EAAO,EAAE,aAAa,OAAA,EAAS,gBAAA,EAAkB,WAAW,CAAA;AAAA,IAC3E,EAAE,aAAa,OAAA;AAAQ,GACzB,GACA,MAAA;AACN;AAEO,MAAM,SAAA,GAAY,CAAC,CAAA,KAAkC;AAC1D,EAAA,MAAM,CAAA,GAAI,CAAA,CAAE,QAAA,CAAS,WAAA,IAAe,EAAC;AACrC,EAAA,OAAA,CACG,CAAA,CAAE,sBAAsB,CAAA,IAAK,CAAA,CAAE,qBAAqB,CAAA,GAAI,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,EAAG,IAAA,EAAK,IAC7E,MAAA;AAEJ;AAGO,MAAM,gBAAA,GAAmB,CAAC,CAAA,KAAA,CAC9B,CAAA,CAAE,QAAA,CAAS,MAAA,IAAU,EAAC,EAAG,gBAAgB,CAAA,EAAG,IAAA,EAAK,IAAK;AAGlD,MAAM,gBAAA,CAAyC;AAAA,EACpD,WAAA,CACmB,SACD,MAAA,EAChB;AAFiB,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AACD,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EACf;AAAA,EAFgB,OAAA;AAAA,EACD,MAAA;AAAA,EAElB,aAAa,MAAA,EAAyB;AACpC,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,OAAA,CAAQ,MAAM,CAAA;AAC7B,IAAA,OAAO,CAAA,KAAM,MAAA,IAAa,IAAA,CAAK,MAAA,CAAO,SAAS,CAAC,CAAA;AAAA,EAClD;AACF;AAGO,SAAS,kBAAkB,CAAA,EAAmB;AACnD,EAAA,OAAO;AAAA,IACL,CAAA,CAAE,QAAA,CAAS,KAAA,IAAS,CAAA,CAAE,QAAA,CAAS,IAAA;AAAA,IAC/B,CAAA,CAAE,SAAS,WAAA,IAAe,EAAA;AAAA,IAC1B,gBAAA,CAAiB,CAAC,CAAA,IAAK,EAAA;AAAA,IACvB,SAAA,CAAU,CAAC,CAAA,IAAK;AAAA,GAClB,CACG,IAAA,CAAK,GAAG,CAAA,CACR,WAAA,EAAY;AACjB;AAEO,MAAM,eAAA,CAAwC;AAAA,EAClC,MAAA;AAAA,EACjB,YAAY,KAAA,EAAe;AACzB,IAAA,IAAA,CAAK,MAAA,GAAS,KAAA,CAAM,IAAA,EAAK,CAAE,WAAA,EAAY;AAAA,EACzC;AAAA,EACA,aAAa,MAAA,EAAyB;AAEpC,IAAA,OAAO,IAAA,CAAK,WAAW,EAAA,IAAM,iBAAA,CAAkB,MAAM,CAAA,CAAE,QAAA,CAAS,KAAK,MAAM,CAAA;AAAA,EAC7E;AACF;AAMO,MAAM,aAAA,CAAsC;AAAA,EACxC,KAAA,GAAQ,KAAA;AAAA,EACjB,iBAAA,GAA4C;AAC1C,IAAA,OAAO,EAAE,IAAA,EAAM,IAAA,CAAK,KAAA,EAAM;AAAA,EAC5B;AACF;;;;"}
@@ -4,7 +4,6 @@ const QKEY = {
4
4
  lifecycle: "lifecycle",
5
5
  org: "org",
6
6
  owner: "owner",
7
- category: "category",
8
7
  department: "department"
9
8
  };
10
9
  function parseSearchParams(sp) {
@@ -1 +1 @@
1
- {"version":3,"file":"apigeeSearchParams.esm.js","sources":["../../src/components/apigeeSearchParams.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { FACET_ORDER, type FacetKey } from './facetModel';\n\nexport interface ApigeeFilterState {\n selected: Record<FacetKey, string[]>;\n q: string;\n}\n\n/** Clean URL keys (D-06): one query key per facet, no native `filters[...]` wrapper. */\nconst QKEY: Record<FacetKey, string> = {\n lifecycle: 'lifecycle',\n org: 'org',\n owner: 'owner',\n category: 'category',\n department: 'department',\n};\n\n/**\n * URL → state. Repeated keys become arrays (D-07) via `getAll`; unknown/extra keys are\n * ignored. Values are treated as opaque strings — no eval, RegExp, or HTML (T-06-02-01);\n * an unknown facet value simply matches nothing downstream (derived allow-list).\n */\nexport function parseSearchParams(sp: URLSearchParams): ApigeeFilterState {\n const selected = Object.fromEntries(\n FACET_ORDER.map(k => [k, sp.getAll(QKEY[k])]),\n ) as Record<FacetKey, string[]>;\n return { selected, q: sp.get('q') ?? '' };\n}\n\n/**\n * state → URL. Deterministic order (FACET_ORDER, then `q` last) reproduces the exact SC #3\n * shape `?lifecycle=deprecated&q=payment`. Only the known clean keys are appended and empties\n * are omitted (T-06-02-02) — no injection surface. Multi-value facets serialize as repeated\n * keys (D-07) via `append`.\n */\nexport function buildSearchParams(state: ApigeeFilterState): URLSearchParams {\n const sp = new URLSearchParams();\n for (const k of FACET_ORDER) {\n for (const v of state.selected[k]) {\n sp.append(QKEY[k], v);\n }\n }\n if (state.q.trim()) {\n sp.set('q', state.q.trim());\n }\n return sp;\n}\n\n/** True when any facet is selected or the search box holds a non-blank query. */\nexport function isActive(state: ApigeeFilterState): boolean {\n return (\n state.q.trim() !== '' || FACET_ORDER.some(k => state.selected[k].length > 0)\n );\n}\n\n/**\n * Single-select URL update for the count cards: the clicked value becomes the ONLY active facet\n * selection (every other facet is cleared), so each card click filters to exactly that value\n * instead of accumulating like the toolbar's multi-select toggle. Clicking the current sole\n * selection clears it. The `q` search query is always preserved.\n */\nexport function selectOnlyFacetParams(\n prev: URLSearchParams,\n key: FacetKey,\n value: string,\n): URLSearchParams {\n const current = prev.getAll(QKEY[key]);\n const isSoleSelection =\n current.length === 1 &&\n current[0] === value &&\n FACET_ORDER.every(k => k === key || prev.getAll(QKEY[k]).length === 0);\n const next = new URLSearchParams();\n const q = prev.get('q');\n if (q) {\n next.set('q', q);\n }\n if (!isSoleSelection) {\n next.append(QKEY[key], value);\n }\n return next;\n}\n"],"names":[],"mappings":";;AASA,MAAM,IAAA,GAAiC;AAAA,EACrC,SAAA,EAAW,WAAA;AAAA,EACX,GAAA,EAAK,KAAA;AAAA,EACL,KAAA,EAAO,OAAA;AAAA,EACP,QAAA,EAAU,UAAA;AAAA,EACV,UAAA,EAAY;AACd,CAAA;AAOO,SAAS,kBAAkB,EAAA,EAAwC;AACxE,EAAA,MAAM,WAAW,MAAA,CAAO,WAAA;AAAA,IACtB,WAAA,CAAY,GAAA,CAAI,CAAA,CAAA,KAAK,CAAC,CAAA,EAAG,EAAA,CAAG,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAC,CAAC;AAAA,GAC9C;AACA,EAAA,OAAO,EAAE,QAAA,EAAU,CAAA,EAAG,GAAG,GAAA,CAAI,GAAG,KAAK,EAAA,EAAG;AAC1C;AAsBO,SAAS,SAAS,KAAA,EAAmC;AAC1D,EAAA,OACE,KAAA,CAAM,CAAA,CAAE,IAAA,EAAK,KAAM,EAAA,IAAM,WAAA,CAAY,IAAA,CAAK,CAAA,CAAA,KAAK,KAAA,CAAM,QAAA,CAAS,CAAC,CAAA,CAAE,SAAS,CAAC,CAAA;AAE/E;AAQO,SAAS,qBAAA,CACd,IAAA,EACA,GAAA,EACA,KAAA,EACiB;AACjB,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAC,CAAA;AACrC,EAAA,MAAM,eAAA,GACJ,QAAQ,MAAA,KAAW,CAAA,IACnB,QAAQ,CAAC,CAAA,KAAM,SACf,WAAA,CAAY,KAAA,CAAM,OAAK,CAAA,KAAM,GAAA,IAAO,KAAK,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAA,CAAE,WAAW,CAAC,CAAA;AACvE,EAAA,MAAM,IAAA,GAAO,IAAI,eAAA,EAAgB;AACjC,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AACtB,EAAA,IAAI,CAAA,EAAG;AACL,IAAA,IAAA,CAAK,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EACjB;AACA,EAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAAG,KAAK,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,IAAA;AACT;;;;"}
1
+ {"version":3,"file":"apigeeSearchParams.esm.js","sources":["../../src/components/apigeeSearchParams.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { FACET_ORDER, type FacetKey } from './facetModel';\n\nexport interface ApigeeFilterState {\n selected: Record<FacetKey, string[]>;\n q: string;\n}\n\n/** Clean URL keys (D-06): one query key per facet, no native `filters[...]` wrapper. */\nconst QKEY: Record<FacetKey, string> = {\n lifecycle: 'lifecycle',\n org: 'org',\n owner: 'owner',\n department: 'department',\n};\n\n/**\n * URL → state. Repeated keys become arrays (D-07) via `getAll`; unknown/extra keys are\n * ignored. Values are treated as opaque strings — no eval, RegExp, or HTML (T-06-02-01);\n * an unknown facet value simply matches nothing downstream (derived allow-list).\n */\nexport function parseSearchParams(sp: URLSearchParams): ApigeeFilterState {\n const selected = Object.fromEntries(\n FACET_ORDER.map(k => [k, sp.getAll(QKEY[k])]),\n ) as Record<FacetKey, string[]>;\n return { selected, q: sp.get('q') ?? '' };\n}\n\n/**\n * state → URL. Deterministic order (FACET_ORDER, then `q` last) reproduces the exact SC #3\n * shape `?lifecycle=deprecated&q=payment`. Only the known clean keys are appended and empties\n * are omitted (T-06-02-02) — no injection surface. Multi-value facets serialize as repeated\n * keys (D-07) via `append`.\n */\nexport function buildSearchParams(state: ApigeeFilterState): URLSearchParams {\n const sp = new URLSearchParams();\n for (const k of FACET_ORDER) {\n for (const v of state.selected[k]) {\n sp.append(QKEY[k], v);\n }\n }\n if (state.q.trim()) {\n sp.set('q', state.q.trim());\n }\n return sp;\n}\n\n/** True when any facet is selected or the search box holds a non-blank query. */\nexport function isActive(state: ApigeeFilterState): boolean {\n return (\n state.q.trim() !== '' || FACET_ORDER.some(k => state.selected[k].length > 0)\n );\n}\n\n/**\n * Single-select URL update for the count cards: the clicked value becomes the ONLY active facet\n * selection (every other facet is cleared), so each card click filters to exactly that value\n * instead of accumulating like the toolbar's multi-select toggle. Clicking the current sole\n * selection clears it. The `q` search query is always preserved.\n */\nexport function selectOnlyFacetParams(\n prev: URLSearchParams,\n key: FacetKey,\n value: string,\n): URLSearchParams {\n const current = prev.getAll(QKEY[key]);\n const isSoleSelection =\n current.length === 1 &&\n current[0] === value &&\n FACET_ORDER.every(k => k === key || prev.getAll(QKEY[k]).length === 0);\n const next = new URLSearchParams();\n const q = prev.get('q');\n if (q) {\n next.set('q', q);\n }\n if (!isSoleSelection) {\n next.append(QKEY[key], value);\n }\n return next;\n}\n"],"names":[],"mappings":";;AASA,MAAM,IAAA,GAAiC;AAAA,EACrC,SAAA,EAAW,WAAA;AAAA,EACX,GAAA,EAAK,KAAA;AAAA,EACL,KAAA,EAAO,OAAA;AAAA,EACP,UAAA,EAAY;AACd,CAAA;AAOO,SAAS,kBAAkB,EAAA,EAAwC;AACxE,EAAA,MAAM,WAAW,MAAA,CAAO,WAAA;AAAA,IACtB,WAAA,CAAY,GAAA,CAAI,CAAA,CAAA,KAAK,CAAC,CAAA,EAAG,EAAA,CAAG,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAC,CAAC;AAAA,GAC9C;AACA,EAAA,OAAO,EAAE,QAAA,EAAU,CAAA,EAAG,GAAG,GAAA,CAAI,GAAG,KAAK,EAAA,EAAG;AAC1C;AAsBO,SAAS,SAAS,KAAA,EAAmC;AAC1D,EAAA,OACE,KAAA,CAAM,CAAA,CAAE,IAAA,EAAK,KAAM,EAAA,IAAM,WAAA,CAAY,IAAA,CAAK,CAAA,CAAA,KAAK,KAAA,CAAM,QAAA,CAAS,CAAC,CAAA,CAAE,SAAS,CAAC,CAAA;AAE/E;AAQO,SAAS,qBAAA,CACd,IAAA,EACA,GAAA,EACA,KAAA,EACiB;AACjB,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAC,CAAA;AACrC,EAAA,MAAM,eAAA,GACJ,QAAQ,MAAA,KAAW,CAAA,IACnB,QAAQ,CAAC,CAAA,KAAM,SACf,WAAA,CAAY,KAAA,CAAM,OAAK,CAAA,KAAM,GAAA,IAAO,KAAK,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAA,CAAE,WAAW,CAAC,CAAA;AACvE,EAAA,MAAM,IAAA,GAAO,IAAI,eAAA,EAAgB;AACjC,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AACtB,EAAA,IAAI,CAAA,EAAG;AACL,IAAA,IAAA,CAAK,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EACjB;AACA,EAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAAG,KAAK,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,IAAA;AACT;;;;"}
@@ -1,12 +1,10 @@
1
- import { resolveCategoryIcon } from './categoryIcons.esm.js';
2
1
  import { humanise } from './grouping.esm.js';
3
- import { entityDepartment, entityCategoryFacet, entityOwnerLabel, entityOrg, entityLifecycle } from './apigeeFacetFilters.esm.js';
2
+ import { entityDepartment, entityOwnerLabel, entityOrg, entityLifecycle } from './apigeeFacetFilters.esm.js';
4
3
 
5
4
  const FACET_ORDER = [
6
5
  "lifecycle",
7
6
  "org",
8
7
  "owner",
9
- "category",
10
8
  "department"
11
9
  ];
12
10
  const LIFECYCLE_SEVERITY = ["production", "experimental", "deprecated", "unknown"];
@@ -14,14 +12,12 @@ const FACET_TRIGGER_LABEL = {
14
12
  lifecycle: "Lifecycle",
15
13
  org: "Org",
16
14
  owner: "Owner",
17
- category: "Category",
18
15
  department: "Department"
19
16
  };
20
17
  const EXTRACT = {
21
18
  lifecycle: entityLifecycle,
22
19
  org: entityOrg,
23
20
  owner: entityOwnerLabel,
24
- category: entityCategoryFacet,
25
21
  department: entityDepartment
26
22
  };
27
23
  function labelFor(key, value, orgLabels) {
@@ -30,8 +26,6 @@ function labelFor(key, value, orgLabels) {
30
26
  return value.charAt(0).toUpperCase() + value.slice(1);
31
27
  case "owner":
32
28
  return value;
33
- case "category":
34
- return value === "other" ? "Other" : humanise(value);
35
29
  case "org":
36
30
  return orgLabels?.[value] ?? humanise(value);
37
31
  default:
@@ -46,16 +40,9 @@ function sortValues(key, values) {
46
40
  };
47
41
  return [...values].sort((a, b) => rank(a) - rank(b));
48
42
  }
49
- if (key === "category") {
50
- return [...values].sort((a, b) => {
51
- if (a === "other") return 1;
52
- if (b === "other") return -1;
53
- return a.localeCompare(b);
54
- });
55
- }
56
43
  return [...values].sort((a, b) => a.localeCompare(b));
57
44
  }
58
- function deriveFacets(entities, overrides, orgLabels) {
45
+ function deriveFacets(entities, orgLabels) {
59
46
  return FACET_ORDER.map((key) => {
60
47
  const distinct = Array.from(
61
48
  new Set(entities.map(EXTRACT[key]).filter((v) => !!v))
@@ -64,24 +51,11 @@ function deriveFacets(entities, overrides, orgLabels) {
64
51
  value,
65
52
  label: labelFor(key, value, orgLabels)
66
53
  }));
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
- );
79
54
  return {
80
55
  key,
81
56
  label: FACET_TRIGGER_LABEL[key],
82
57
  options,
83
- enabled: options.length > 0,
84
- ...icons !== void 0 ? { icons } : {}
58
+ enabled: options.length > 0
85
59
  };
86
60
  });
87
61
  }
@@ -1 +1 @@
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
+ {"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 { humanise } from './grouping';\nimport {\n entityDepartment,\n entityLifecycle,\n entityOrg,\n entityOwnerLabel,\n} from './apigeeFacetFilters';\n\nexport type FacetKey = 'lifecycle' | 'org' | 'owner' | '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\n/** Facet order (D-02): Lifecycle → Org → Owner → Department. */\nexport const FACET_ORDER: FacetKey[] = [\n 'lifecycle',\n 'org',\n 'owner',\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 department: 'Department',\n};\n\n/** The single value extractor per facet slot (06-01 extractors, verbatim). */\nconst EXTRACT: Record<FacetKey, (e: Entity) => string | undefined> = {\n lifecycle: entityLifecycle,\n org: entityOrg,\n owner: entityOwnerLabel,\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 * - 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 '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 * - org / owner / department 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 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 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 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 return {\n key,\n label: FACET_TRIGGER_LABEL[key],\n options,\n enabled: options.length > 0,\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,7 +1,7 @@
1
1
  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
- import { FieldFacetFilter, entityDepartment, entityCategoryFacet, entityOwnerLabel, entityOrg, entityLifecycle, ApiSearchFilter } from './apigeeFacetFilters.esm.js';
4
+ import { FieldFacetFilter, entityDepartment, entityOwnerLabel, entityOrg, entityLifecycle, ApiSearchFilter } from './apigeeFacetFilters.esm.js';
5
5
  import { parseSearchParams, selectOnlyFacetParams } from './apigeeSearchParams.esm.js';
6
6
  import { useDebouncedValue } from './useDebouncedValue.esm.js';
7
7
 
@@ -9,14 +9,12 @@ const FACET_EXTRACT = {
9
9
  lifecycle: entityLifecycle,
10
10
  org: entityOrg,
11
11
  owner: entityOwnerLabel,
12
- category: entityCategoryFacet,
13
12
  department: entityDepartment
14
13
  };
15
14
  const FILTER_KEY = {
16
15
  lifecycle: "apigeeLifecycle",
17
16
  org: "apigeeOrgFacet",
18
17
  owner: "apigeeOwner",
19
- category: "apigeeCategory",
20
18
  department: "apigeeDepartment"
21
19
  };
22
20
  function useApigeeFilters() {
@@ -1 +1 @@
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
+ {"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 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/** Value extractor per facet slot used to build the FILTER (06-01 extractors). */\nconst FACET_EXTRACT: Record<FacetKey, (e: Entity) => string | undefined> = {\n lifecycle: entityLifecycle,\n org: entityOrg,\n owner: entityOwnerLabel,\n department: entityDepartment,\n};\n\n/** Facet slot → the `ApigeeFilters` key its `FieldFacetFilter` registers under. */\ntype FacetFilterKey =\n | 'apigeeLifecycle'\n | 'apigeeOrgFacet'\n | 'apigeeOwner'\n | 'apigeeDepartment';\nconst FILTER_KEY: Record<FacetKey, FacetFilterKey> = {\n lifecycle: 'apigeeLifecycle',\n org: 'apigeeOrgFacet',\n owner: 'apigeeOwner',\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,10 +1,9 @@
1
1
  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
- const ANNOTATION_CATEGORY = "apigee.com/category";
5
4
  const ANNOTATION_VERSION = "apigee.com/version";
6
5
  const ANNOTATION_PROXY_NAME = "apigee.com/proxy-name";
7
6
  const LABEL_DEPARTMENT = "department";
8
7
 
9
- export { ANNOTATION_APIGEE_ORG, ANNOTATION_APIGEE_ORGS, ANNOTATION_CATEGORY, ANNOTATION_HAS_SPEC, ANNOTATION_PROXY_NAME, ANNOTATION_VERSION, LABEL_DEPARTMENT };
8
+ export { ANNOTATION_APIGEE_ORG, ANNOTATION_APIGEE_ORGS, ANNOTATION_HAS_SPEC, ANNOTATION_PROXY_NAME, ANNOTATION_VERSION, LABEL_DEPARTMENT };
10
9
  //# 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\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;;;;"}
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/** 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;AAO5B,MAAM,kBAAA,GAAqB;AAK3B,MAAM,qBAAA,GAAwB;AAM9B,MAAM,gBAAA,GAAmB;;;;"}
package/dist/index.d.ts CHANGED
@@ -41,6 +41,31 @@ declare const apigeePage: _backstage_frontend_plugin_api.OverridableExtensionDef
41
41
  noHeader?: boolean;
42
42
  };
43
43
  }>;
44
+ declare const apigeeCatalogSubPage: _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
45
+ kind: "sub-page";
46
+ name: "catalog";
47
+ config: {
48
+ path: string | undefined;
49
+ title: string | undefined;
50
+ };
51
+ configInput: {
52
+ path?: string | undefined;
53
+ title?: string | undefined;
54
+ };
55
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
56
+ optional: true;
57
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.title", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
58
+ optional: true;
59
+ }>;
60
+ inputs: {};
61
+ params: {
62
+ path: string;
63
+ title: string;
64
+ icon?: _backstage_frontend_plugin_api.IconElement;
65
+ loader: () => Promise<JSX.Element>;
66
+ routeRef?: _backstage_frontend_plugin_api.RouteRef;
67
+ };
68
+ }>;
44
69
  /**
45
70
  * Frontend module that overrides the app nav bar to show an expandable
46
71
  * "API Hub" group containing all apigee plugin pages as sub-items.
@@ -55,6 +80,7 @@ declare const apigeePage: _backstage_frontend_plugin_api.OverridableExtensionDef
55
80
  declare const apigeeNavModule: _backstage_frontend_plugin_api.FrontendModule;
56
81
  declare const apigeePlugin: _backstage_frontend_plugin_api.OverridableFrontendPlugin<{
57
82
  root: _backstage_frontend_plugin_api.RouteRef<undefined>;
83
+ catalog: _backstage_frontend_plugin_api.RouteRef<undefined>;
58
84
  }, {}, {
59
85
  "page:apigee": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
60
86
  kind: "page";
@@ -96,6 +122,31 @@ declare const apigeePlugin: _backstage_frontend_plugin_api.OverridableFrontendPl
96
122
  noHeader?: boolean;
97
123
  };
98
124
  }>;
125
+ "sub-page:apigee/catalog": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
126
+ kind: "sub-page";
127
+ name: "catalog";
128
+ config: {
129
+ path: string | undefined;
130
+ title: string | undefined;
131
+ };
132
+ configInput: {
133
+ path?: string | undefined;
134
+ title?: string | undefined;
135
+ };
136
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
137
+ optional: true;
138
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.title", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
139
+ optional: true;
140
+ }>;
141
+ inputs: {};
142
+ params: {
143
+ path: string;
144
+ title: string;
145
+ icon?: _backstage_frontend_plugin_api.IconElement;
146
+ loader: () => Promise<JSX.Element>;
147
+ routeRef?: _backstage_frontend_plugin_api.RouteRef;
148
+ };
149
+ }>;
99
150
  }>;
100
151
 
101
- export { apigeeNavModule, apigeePage, apigeePlugin, apigeePlugin as default };
152
+ export { apigeeCatalogSubPage, apigeeNavModule, apigeePage, apigeePlugin, apigeePlugin as default };
package/dist/index.esm.js CHANGED
@@ -1,2 +1,2 @@
1
- export { apigeeNavModule, apigeePage, apigeePlugin, apigeePlugin as default } from './plugin.esm.js';
1
+ export { apigeeCatalogSubPage, apigeeNavModule, apigeePage, apigeePlugin, apigeePlugin as default } from './plugin.esm.js';
2
2
  //# sourceMappingURL=index.esm.js.map
@@ -1,16 +1,24 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { PageBlueprint, createFrontendModule, createFrontendPlugin } from '@backstage/frontend-plugin-api';
2
+ import { PageBlueprint, SubPageBlueprint, createFrontendModule, createFrontendPlugin } from '@backstage/frontend-plugin-api';
3
3
  import { NavContentBlueprint } from '@backstage/plugin-app-react';
4
4
  import { RiStackLine } from '@remixicon/react';
5
- import { rootRouteRef } from './routes.esm.js';
5
+ import { rootRouteRef, catalogRouteRef } 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
- title: "API Catalog",
10
+ title: "API Hub",
11
11
  icon: /* @__PURE__ */ jsx(RiStackLine, {}),
12
12
  routeRef: rootRouteRef,
13
- path: "/apigee",
13
+ path: "/apigee"
14
+ }
15
+ });
16
+ const apigeeCatalogSubPage = SubPageBlueprint.make({
17
+ name: "catalog",
18
+ params: {
19
+ path: "",
20
+ title: "API catalog",
21
+ routeRef: catalogRouteRef,
14
22
  loader: () => import('./components/ApigeeApiCatalogPage.esm.js').then(
15
23
  ({ ApigeeApiCatalogPage }) => /* @__PURE__ */ jsx(ApigeeApiCatalogPage, {})
16
24
  )
@@ -29,11 +37,13 @@ const apigeeNavModule = createFrontendModule({
29
37
  const apigeePlugin = createFrontendPlugin({
30
38
  pluginId: "apigee",
31
39
  title: "API Hub",
32
- extensions: [apigeePage],
40
+ icon: /* @__PURE__ */ jsx(RiStackLine, {}),
41
+ extensions: [apigeePage, apigeeCatalogSubPage],
33
42
  routes: {
34
- root: rootRouteRef
43
+ root: rootRouteRef,
44
+ catalog: catalogRouteRef
35
45
  }
36
46
  });
37
47
 
38
- export { apigeeNavModule, apigeePage, apigeePlugin };
48
+ export { apigeeCatalogSubPage, apigeeNavModule, apigeePage, apigeePlugin };
39
49
  //# sourceMappingURL=plugin.esm.js.map
@@ -1 +1 @@
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
+ {"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 SubPageBlueprint,\n} from '@backstage/frontend-plugin-api';\nimport { NavContentBlueprint } from '@backstage/plugin-app-react';\nimport { RiStackLine } from '@remixicon/react';\nimport { catalogRouteRef, rootRouteRef } from './routes';\nimport { ApigeeNavContent } from './components/ApigeeNavContent';\n\n// Container page: owns `/apigee`, the \"API Hub\" header (title + icon), and the tab bar\n// rendered from its sub-pages. Content lives in the sub-pages, so it has no `loader`.\nexport const apigeePage = PageBlueprint.make({\n params: {\n title: 'API Hub',\n icon: <RiStackLine />,\n routeRef: rootRouteRef,\n path: '/apigee',\n },\n});\n\n// The \"API catalog\" tab — the index sub-page of API Hub, mounted at `/apigee`.\nexport const apigeeCatalogSubPage = SubPageBlueprint.make({\n name: 'catalog',\n params: {\n path: '',\n title: 'API catalog',\n routeRef: catalogRouteRef,\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 icon: <RiStackLine />,\n extensions: [apigeePage, apigeeCatalogSubPage],\n routes: {\n root: rootRouteRef,\n catalog: catalogRouteRef,\n },\n});\n"],"names":[],"mappings":";;;;;;;AAcO,MAAM,UAAA,GAAa,cAAc,IAAA,CAAK;AAAA,EAC3C,MAAA,EAAQ;AAAA,IACN,KAAA,EAAO,SAAA;AAAA,IACP,IAAA,sBAAO,WAAA,EAAA,EAAY,CAAA;AAAA,IACnB,QAAA,EAAU,YAAA;AAAA,IACV,IAAA,EAAM;AAAA;AAEV,CAAC;AAGM,MAAM,oBAAA,GAAuB,iBAAiB,IAAA,CAAK;AAAA,EACxD,IAAA,EAAM,SAAA;AAAA,EACN,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM,EAAA;AAAA,IACN,KAAA,EAAO,aAAA;AAAA,IACP,QAAA,EAAU,eAAA;AAAA,IACV,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,IAAA,sBAAO,WAAA,EAAA,EAAY,CAAA;AAAA,EACnB,UAAA,EAAY,CAAC,UAAA,EAAY,oBAAoB,CAAA;AAAA,EAC7C,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM,YAAA;AAAA,IACN,OAAA,EAAS;AAAA;AAEb,CAAC;;;;"}
@@ -1,6 +1,7 @@
1
1
  import { createRouteRef } from '@backstage/frontend-plugin-api';
2
2
 
3
3
  const rootRouteRef = createRouteRef();
4
+ const catalogRouteRef = createRouteRef();
4
5
 
5
- export { rootRouteRef };
6
+ export { catalogRouteRef, rootRouteRef };
6
7
  //# sourceMappingURL=routes.esm.js.map
@@ -1 +1 @@
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;;;;"}
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();\nexport const catalogRouteRef = createRouteRef();\n"],"names":[],"mappings":";;AAGO,MAAM,eAAe,cAAA;AACrB,MAAM,kBAAkB,cAAA;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nospt/backstage-plugin-apigee",
3
- "version": "1.1.0-rc4",
3
+ "version": "1.1.0-rc5",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Backstage frontend plugin bootstrap for Apigee integration",
6
6
  "author": "NOS SGPS, S.A.",
@@ -53,7 +53,7 @@
53
53
  "@backstage/frontend-plugin-api": "^0.17.0",
54
54
  "@backstage/plugin-catalog-react": "^3.0.0",
55
55
  "@backstage/ui": "^0.15.0",
56
- "@nospt/backstage-plugin-apigee-common": "^1.1.0-rc4",
56
+ "@nospt/backstage-plugin-apigee-common": "^1.1.0-rc5",
57
57
  "@remixicon/react": "^4.6.0"
58
58
  },
59
59
  "peerDependencies": {