@igstack/app-catalog-frontend-core 0.17.0 → 0.17.1-alpha-20260821043201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/esm/__tests__/integration/mock-backend/magazines.d.ts +1 -1
  2. package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +16 -0
  3. package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +12 -0
  4. package/dist/esm/api/infra/trpc.d.ts +24 -0
  5. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -0
  6. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +1 -1
  7. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -1
  8. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.d.ts +9 -0
  9. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +90 -0
  10. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -0
  11. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +1 -1
  12. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  13. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +7 -1
  14. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +108 -31
  15. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +31 -4
  17. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  18. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js +41 -20
  19. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js.map +1 -1
  20. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
  21. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
  22. package/dist/esm/routeTree.gen.d.ts +2 -0
  23. package/dist/esm/routes/_layout/app.$slug.d.ts +1 -0
  24. package/dist/esm/routes/_layout/app._slug.js +2 -1
  25. package/dist/esm/routes/_layout/app._slug.js.map +1 -1
  26. package/dist/esm/routes/_layout/catalog.apps.index.d.ts +1 -0
  27. package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
  28. package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
  29. package/package.json +5 -7
  30. package/src/__tests__/integration/appCatalog.integration.test.ts +264 -5
  31. package/src/__tests__/integration/appDeepLink.integration.test.ts +46 -19
  32. package/src/__tests__/integration/crossRefLink.integration.test.ts +8 -8
  33. package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
  34. package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +2 -0
  35. package/src/__tests__/integration/mock-backend/magazines.ts +24 -24
  36. package/src/__tests__/integration/replacementLink.integration.test.ts +2 -2
  37. package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +2 -2
  38. package/src/__tests__/integration/serviceDesks.integration.test.ts +31 -24
  39. package/src/__tests__/integration/tools/AppDetailTools.ts +57 -0
  40. package/src/__tests__/integration/tools/CatalogTools.ts +77 -4
  41. package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +6 -6
  42. package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +1 -1
  43. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +11 -7
  44. package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +1 -1
  45. package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +124 -0
  46. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +1 -1
  47. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +128 -34
  48. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +49 -8
  49. package/src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx +37 -9
  50. package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
  51. package/src/routes/_layout/app.$slug.tsx +1 -0
  52. package/src/routes/_layout/catalog.apps.index.tsx +1 -0
@@ -11,7 +11,7 @@ export type Magazine = (ctx: ConfigurerContext) => void;
11
11
  export declare const magazine: {
12
12
  /** No apps, no tags, no approval methods */
13
13
  empty: () => void;
14
- /** 1 app (Jira), 1 tag, 1 approval method — accepts optional post-configurer */
14
+ /** 1 app (TaskFlow), 1 tag, 1 approval method — accepts optional post-configurer */
15
15
  single: (postConfigure?: Magazine) => Magazine;
16
16
  /** Full catalog, first-time user — accepts optional post-configurer */
17
17
  full: (postConfigure?: Magazine) => Magazine;
@@ -41,6 +41,22 @@ export declare class AppDetailTools {
41
41
  clickViewReplacement(): Promise<void>;
42
42
  /** The title shown in the currently open detail panel (empty if none). */
43
43
  getOpenTitle(): string;
44
+ /**
45
+ * The prominent primary "Open" button in the app detail card.
46
+ * Returns the anchor element when the app has an appUrl, null otherwise.
47
+ */
48
+ getOpenButton(): HTMLAnchorElement | null;
49
+ /**
50
+ * Get sub-resource detail panel data when a sub-resource is selected.
51
+ * Returns null if the sub-resource detail panel is not shown.
52
+ */
53
+ getSubResourceDetail(): {
54
+ subResourceName: string;
55
+ hasStep1: boolean;
56
+ hasStep2: boolean;
57
+ backButtonLabel: string | null;
58
+ } | null;
59
+ clickBackToParent(): Promise<void>;
44
60
  private getPanel;
45
61
  private findHeading;
46
62
  private getNextSiblingText;
@@ -48,6 +48,18 @@ export declare class CatalogTools {
48
48
  * "Your apps" and "Browse all").
49
49
  */
50
50
  private getLauncherRows;
51
+ /**
52
+ * Get expandable subresource rows shown under parents in search results.
53
+ * Returns null if no sub-rows are visible.
54
+ */
55
+ getSubResourceRows(): {
56
+ visible: number;
57
+ total: number;
58
+ hasExpandRow: boolean;
59
+ names: string[];
60
+ } | null;
61
+ clickSubResource(displayName: string): Promise<void>;
62
+ expandSubResources(): Promise<void>;
51
63
  /**
52
64
  * Get the main catalog table (first table on page, skipping sub-resource tables in detail panel).
53
65
  */
@@ -34,6 +34,14 @@ declare const TRPCProvider: import('react').FunctionComponent<{
34
34
  output: import('@igstack/app-catalog-backend-core').Resource;
35
35
  meta: object;
36
36
  }>;
37
+ updateAiMemory: import('@trpc/server').TRPCMutationProcedure<{
38
+ input: {
39
+ id: string;
40
+ aiMemory: string | null;
41
+ };
42
+ output: import('@igstack/app-catalog-backend-core').Resource;
43
+ meta: object;
44
+ }>;
37
45
  }>>;
38
46
  auth: import('@trpc/server').TRPCBuiltRouter<{
39
47
  ctx: import('@igstack/app-catalog-backend-core').AcTrpcContext;
@@ -103,6 +111,14 @@ declare const TRPCProvider: import('react').FunctionComponent<{
103
111
  output: import('@igstack/app-catalog-backend-core').Resource;
104
112
  meta: object;
105
113
  }>;
114
+ updateAiMemory: import('@trpc/server').TRPCMutationProcedure<{
115
+ input: {
116
+ id: string;
117
+ aiMemory: string | null;
118
+ };
119
+ output: import('@igstack/app-catalog-backend-core').Resource;
120
+ meta: object;
121
+ }>;
106
122
  }>>;
107
123
  auth: import('@trpc/server').TRPCBuiltRouter<{
108
124
  ctx: import('@igstack/app-catalog-backend-core').AcTrpcContext;
@@ -171,6 +187,14 @@ declare const TRPCProvider: import('react').FunctionComponent<{
171
187
  output: import('@igstack/app-catalog-backend-core').Resource;
172
188
  meta: object;
173
189
  }>;
190
+ updateAiMemory: import('@trpc/server').TRPCMutationProcedure<{
191
+ input: {
192
+ id: string;
193
+ aiMemory: string | null;
194
+ };
195
+ output: import('@igstack/app-catalog-backend-core').Resource;
196
+ meta: object;
197
+ }>;
174
198
  }>>;
175
199
  auth: import('@trpc/server').TRPCBuiltRouter<{
176
200
  ctx: import('@igstack/app-catalog-backend-core').AcTrpcContext;
@@ -53,6 +53,7 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
53
53
  iconName?: string | undefined;
54
54
  screenshotIds?: string[] | undefined;
55
55
  aiPrompt?: string | undefined;
56
+ aiMemory?: string | undefined;
56
57
  urlIssues?: string[] | undefined;
57
58
  tiers?: {
58
59
  tierSlug: string;
@@ -5,7 +5,7 @@ import { Resource } from '@igstack/app-catalog-backend-core';
5
5
  * the user must obtain the parent's access FIRST. That chain is invisible
6
6
  * otherwise, so surface it explicitly as ordered steps:
7
7
  *
8
- * Step 1 — get access to <parent> (e.g. AWS Console via IT Helpdesk)
8
+ * Step 1 — get access to <parent> (e.g. AWS Console via Support Portal)
9
9
  * Step 2 — get access to THIS resource (contact its owner / access contacts)
10
10
  *
11
11
  * Renders nothing for root resources or children with no access-bearing parent.
@@ -1 +1 @@
1
- {"version":3,"file":"AccessPrerequisiteChain.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { ArrowRight, KeyRound } from 'lucide-react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { getAccessPrerequisiteChain } from '../../utils/resolveHelpers'\n\n/**\n * Two-step (prerequisite) access banner (#38 item D). When the viewed resource\n * is a child whose ancestor(s) carry an access policy, the domain model says\n * the user must obtain the parent's access FIRST. That chain is invisible\n * otherwise, so surface it explicitly as ordered steps:\n *\n * Step 1 — get access to <parent> (e.g. AWS Console via IT Helpdesk)\n * Step 2 — get access to THIS resource (contact its owner / access contacts)\n *\n * Renders nothing for root resources or children with no access-bearing parent.\n * `onOpenParent` lets the user jump to the parent's full access instructions.\n */\nexport function AccessPrerequisiteChain({\n resource,\n onOpenParent,\n}: {\n resource: Resource\n onOpenParent?: (parent: Resource) => void\n}) {\n const { resources } = useAppCatalogContext()\n const chain = getAccessPrerequisiteChain(resources, resource)\n if (chain.length === 0) return null\n\n // Steps = each prerequisite ancestor (root→nearest), then this resource last.\n const steps = [...chain, resource]\n\n return (\n <div\n className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4\"\n role=\"note\"\n aria-label=\"Access requires prerequisite steps\"\n >\n <h3 className=\"mb-1 flex items-center gap-1.5 text-sm font-semibold text-foreground\">\n <KeyRound className=\"size-4 text-primary\" />\n Two-step access\n </h3>\n <p className=\"mb-3 text-xs text-muted-foreground\">\n This resource is nested — you need access to its parent first, then to\n the resource itself.\n </p>\n <ol className=\"space-y-2\">\n {steps.map((r, i) => {\n const isLast = i === steps.length - 1\n return (\n <li key={r.slug} className=\"flex items-start gap-2.5 text-sm\">\n <span className=\"mt-0.5 grid size-5 shrink-0 place-items-center rounded-full bg-primary text-[11px] font-bold text-primary-foreground\">\n {i + 1}\n </span>\n <span className=\"min-w-0\">\n <span className=\"text-muted-foreground\">Get access to </span>\n {isLast ? (\n <span className=\"font-semibold\">this resource</span>\n ) : onOpenParent ? (\n <button\n type=\"button\"\n onClick={() => onOpenParent(r)}\n className=\"inline-flex items-center gap-1 font-semibold text-primary hover:underline\"\n >\n {r.displayName}\n <ArrowRight className=\"size-3\" />\n </button>\n ) : (\n <span className=\"font-semibold\">{r.displayName}</span>\n )}\n {!isLast && (\n <span className=\"text-muted-foreground\">\n {' '}\n (see its access instructions)\n </span>\n )}\n {isLast && (\n <span className=\"text-muted-foreground\">\n {' '}\n — details below.\n </span>\n )}\n </span>\n </li>\n )\n })}\n </ol>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;AAiBO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAGG;AACD,QAAM,EAAE,UAAA,IAAc,qBAAA;AACtB,QAAM,QAAQ,2BAA2B,WAAW,QAAQ;AAC5D,MAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,QAAM,QAAQ,CAAC,GAAG,OAAO,QAAQ;AAEjC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAW;AAAA,MAEX,UAAA;AAAA,QAAA,qBAAC,MAAA,EAAG,WAAU,wEACZ,UAAA;AAAA,UAAA,oBAAC,UAAA,EAAS,WAAU,sBAAA,CAAsB;AAAA,UAAE;AAAA,QAAA,GAE9C;AAAA,QACA,oBAAC,KAAA,EAAE,WAAU,sCAAqC,UAAA,+FAGlD;AAAA,QACA,oBAAC,QAAG,WAAU,aACX,gBAAM,IAAI,CAAC,GAAG,MAAM;AACnB,gBAAM,SAAS,MAAM,MAAM,SAAS;AACpC,iBACE,qBAAC,MAAA,EAAgB,WAAU,oCACzB,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,wHACb,UAAA,IAAI,GACP;AAAA,YACA,qBAAC,QAAA,EAAK,WAAU,WACd,UAAA;AAAA,cAAA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,kBAAc;AAAA,cACrD,SACC,oBAAC,QAAA,EAAK,WAAU,iBAAgB,UAAA,gBAAA,CAAa,IAC3C,eACF;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS,MAAM,aAAa,CAAC;AAAA,kBAC7B,WAAU;AAAA,kBAET,UAAA;AAAA,oBAAA,EAAE;AAAA,oBACH,oBAAC,YAAA,EAAW,WAAU,SAAA,CAAS;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA,IAGjC,oBAAC,QAAA,EAAK,WAAU,iBAAiB,YAAE,aAAY;AAAA,cAEhD,CAAC,UACA,qBAAC,QAAA,EAAK,WAAU,yBACb,UAAA;AAAA,gBAAA;AAAA,gBAAI;AAAA,cAAA,GAEP;AAAA,cAED,UACC,qBAAC,QAAA,EAAK,WAAU,yBACb,UAAA;AAAA,gBAAA;AAAA,gBAAI;AAAA,cAAA,EAAA,CAEP;AAAA,YAAA,EAAA,CAEJ;AAAA,UAAA,EAAA,GAhCO,EAAE,IAiCX;AAAA,QAEJ,CAAC,EAAA,CACH;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
1
+ {"version":3,"file":"AccessPrerequisiteChain.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { ArrowRight, KeyRound } from 'lucide-react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { getAccessPrerequisiteChain } from '../../utils/resolveHelpers'\n\n/**\n * Two-step (prerequisite) access banner (#38 item D). When the viewed resource\n * is a child whose ancestor(s) carry an access policy, the domain model says\n * the user must obtain the parent's access FIRST. That chain is invisible\n * otherwise, so surface it explicitly as ordered steps:\n *\n * Step 1 — get access to <parent> (e.g. AWS Console via Support Portal)\n * Step 2 — get access to THIS resource (contact its owner / access contacts)\n *\n * Renders nothing for root resources or children with no access-bearing parent.\n * `onOpenParent` lets the user jump to the parent's full access instructions.\n */\nexport function AccessPrerequisiteChain({\n resource,\n onOpenParent,\n}: {\n resource: Resource\n onOpenParent?: (parent: Resource) => void\n}) {\n const { resources } = useAppCatalogContext()\n const chain = getAccessPrerequisiteChain(resources, resource)\n if (chain.length === 0) return null\n\n // Steps = each prerequisite ancestor (root→nearest), then this resource last.\n const steps = [...chain, resource]\n\n return (\n <div\n className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4\"\n role=\"note\"\n aria-label=\"Access requires prerequisite steps\"\n >\n <h3 className=\"mb-1 flex items-center gap-1.5 text-sm font-semibold text-foreground\">\n <KeyRound className=\"size-4 text-primary\" />\n Two-step access\n </h3>\n <p className=\"mb-3 text-xs text-muted-foreground\">\n This resource is nested — you need access to its parent first, then to\n the resource itself.\n </p>\n <ol className=\"space-y-2\">\n {steps.map((r, i) => {\n const isLast = i === steps.length - 1\n return (\n <li key={r.slug} className=\"flex items-start gap-2.5 text-sm\">\n <span className=\"mt-0.5 grid size-5 shrink-0 place-items-center rounded-full bg-primary text-[11px] font-bold text-primary-foreground\">\n {i + 1}\n </span>\n <span className=\"min-w-0\">\n <span className=\"text-muted-foreground\">Get access to </span>\n {isLast ? (\n <span className=\"font-semibold\">this resource</span>\n ) : onOpenParent ? (\n <button\n type=\"button\"\n onClick={() => onOpenParent(r)}\n className=\"inline-flex items-center gap-1 font-semibold text-primary hover:underline\"\n >\n {r.displayName}\n <ArrowRight className=\"size-3\" />\n </button>\n ) : (\n <span className=\"font-semibold\">{r.displayName}</span>\n )}\n {!isLast && (\n <span className=\"text-muted-foreground\">\n {' '}\n (see its access instructions)\n </span>\n )}\n {isLast && (\n <span className=\"text-muted-foreground\">\n {' '}\n — details below.\n </span>\n )}\n </span>\n </li>\n )\n })}\n </ol>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;AAiBO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAGG;AACD,QAAM,EAAE,UAAA,IAAc,qBAAA;AACtB,QAAM,QAAQ,2BAA2B,WAAW,QAAQ;AAC5D,MAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,QAAM,QAAQ,CAAC,GAAG,OAAO,QAAQ;AAEjC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAW;AAAA,MAEX,UAAA;AAAA,QAAA,qBAAC,MAAA,EAAG,WAAU,wEACZ,UAAA;AAAA,UAAA,oBAAC,UAAA,EAAS,WAAU,sBAAA,CAAsB;AAAA,UAAE;AAAA,QAAA,GAE9C;AAAA,QACA,oBAAC,KAAA,EAAE,WAAU,sCAAqC,UAAA,+FAGlD;AAAA,QACA,oBAAC,QAAG,WAAU,aACX,gBAAM,IAAI,CAAC,GAAG,MAAM;AACnB,gBAAM,SAAS,MAAM,MAAM,SAAS;AACpC,iBACE,qBAAC,MAAA,EAAgB,WAAU,oCACzB,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,wHACb,UAAA,IAAI,GACP;AAAA,YACA,qBAAC,QAAA,EAAK,WAAU,WACd,UAAA;AAAA,cAAA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,kBAAc;AAAA,cACrD,SACC,oBAAC,QAAA,EAAK,WAAU,iBAAgB,UAAA,gBAAA,CAAa,IAC3C,eACF;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS,MAAM,aAAa,CAAC;AAAA,kBAC7B,WAAU;AAAA,kBAET,UAAA;AAAA,oBAAA,EAAE;AAAA,oBACH,oBAAC,YAAA,EAAW,WAAU,SAAA,CAAS;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA,IAGjC,oBAAC,QAAA,EAAK,WAAU,iBAAiB,YAAE,aAAY;AAAA,cAEhD,CAAC,UACA,qBAAC,QAAA,EAAK,WAAU,yBACb,UAAA;AAAA,gBAAA;AAAA,gBAAI;AAAA,cAAA,GAEP;AAAA,cAED,UACC,qBAAC,QAAA,EAAK,WAAU,yBACb,UAAA;AAAA,gBAAA;AAAA,gBAAI;AAAA,cAAA,EAAA,CAEP;AAAA,YAAA,EAAA,CAEJ;AAAA,UAAA,EAAA,GAhCO,EAAE,IAiCX;AAAA,QAEJ,CAAC,EAAA,CACH;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -0,0 +1,9 @@
1
+ import { AppApprovalMethod, Resource } from '@igstack/app-catalog-backend-core';
2
+ interface SubResourceDetailPanelProps {
3
+ subResource: Resource;
4
+ parent: Resource;
5
+ approvalMethods: AppApprovalMethod[];
6
+ onBack: () => void;
7
+ }
8
+ export declare function SubResourceDetailPanel({ subResource, parent, approvalMethods, onBack, }: SubResourceDetailPanelProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,90 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { ArrowLeft } from "lucide-react";
3
+ import { Button } from "../../../../ui/button.js";
4
+ import { Badge } from "../../../../ui/badge.js";
5
+ import { AccessRequestSection } from "./AccessRequestSection.js";
6
+ function StepBadge({ step }) {
7
+ return /* @__PURE__ */ jsx("span", { className: "inline-flex items-center justify-center size-6 rounded-full bg-primary text-primary-foreground text-xs font-semibold shrink-0", children: step });
8
+ }
9
+ function SubResourceDetailPanel({
10
+ subResource,
11
+ parent,
12
+ approvalMethods,
13
+ onBack
14
+ }) {
15
+ const hasParentAccess = !!parent.accessRequest;
16
+ const hasSubAccess = !!subResource.accessRequest;
17
+ const hasTwoStepAccess = hasParentAccess && hasSubAccess;
18
+ const hasAnyAccess = hasParentAccess || hasSubAccess;
19
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col p-6", children: [
20
+ /* @__PURE__ */ jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsxs(
21
+ Button,
22
+ {
23
+ variant: "ghost",
24
+ size: "sm",
25
+ onClick: onBack,
26
+ className: "gap-1.5 text-muted-foreground hover:text-foreground -ml-2",
27
+ "aria-label": `Back to ${parent.displayName}`,
28
+ children: [
29
+ /* @__PURE__ */ jsx(ArrowLeft, { className: "size-4" }),
30
+ /* @__PURE__ */ jsx("span", { children: parent.displayName })
31
+ ]
32
+ }
33
+ ) }),
34
+ /* @__PURE__ */ jsxs("div", { className: "border-b pb-4", children: [
35
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
36
+ /* @__PURE__ */ jsx("h2", { className: "text-xl font-semibold", children: subResource.displayName }),
37
+ subResource.tier && /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "text-xs", children: subResource.tier })
38
+ ] }),
39
+ subResource.description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1", children: subResource.description }),
40
+ (subResource.aliases ?? []).length > 0 && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: [
41
+ "Also known as: ",
42
+ (subResource.aliases ?? []).join(", ")
43
+ ] })
44
+ ] }),
45
+ hasAnyAccess && /* @__PURE__ */ jsxs("div", { className: "mt-6 space-y-6", children: [
46
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "How to get access" }),
47
+ hasTwoStepAccess ? /* @__PURE__ */ jsxs(Fragment, { children: [
48
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
49
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
50
+ /* @__PURE__ */ jsx(StepBadge, { step: 1 }),
51
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-muted-foreground", children: parent.displayName })
52
+ ] }),
53
+ /* @__PURE__ */ jsx("div", { className: "pl-8", children: /* @__PURE__ */ jsx(
54
+ AccessRequestSection,
55
+ {
56
+ app: parent,
57
+ approvalMethods
58
+ }
59
+ ) })
60
+ ] }),
61
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
62
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
63
+ /* @__PURE__ */ jsx(StepBadge, { step: 2 }),
64
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-muted-foreground", children: subResource.displayName })
65
+ ] }),
66
+ /* @__PURE__ */ jsx("div", { className: "pl-8", children: /* @__PURE__ */ jsx(
67
+ AccessRequestSection,
68
+ {
69
+ app: subResource,
70
+ approvalMethods
71
+ }
72
+ ) })
73
+ ] })
74
+ ] }) : (
75
+ /* Single-step: only one of them has access info */
76
+ /* @__PURE__ */ jsx(
77
+ AccessRequestSection,
78
+ {
79
+ app: hasParentAccess ? parent : subResource,
80
+ approvalMethods
81
+ }
82
+ )
83
+ )
84
+ ] })
85
+ ] });
86
+ }
87
+ export {
88
+ SubResourceDetailPanel
89
+ };
90
+ //# sourceMappingURL=SubResourceDetailPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubResourceDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { ArrowLeft } from 'lucide-react'\nimport { Button } from '~/ui/button'\nimport { Badge } from '~/ui/badge'\nimport { AccessRequestSection } from './AccessRequestSection'\n\ninterface SubResourceDetailPanelProps {\n subResource: Resource\n parent: Resource\n approvalMethods: AppApprovalMethod[]\n onBack: () => void\n}\n\nfunction StepBadge({ step }: { step: number }) {\n return (\n <span className=\"inline-flex items-center justify-center size-6 rounded-full bg-primary text-primary-foreground text-xs font-semibold shrink-0\">\n {step}\n </span>\n )\n}\n\nexport function SubResourceDetailPanel({\n subResource,\n parent,\n approvalMethods,\n onBack,\n}: SubResourceDetailPanelProps) {\n const hasParentAccess = !!parent.accessRequest\n const hasSubAccess = !!subResource.accessRequest\n const hasTwoStepAccess = hasParentAccess && hasSubAccess\n const hasAnyAccess = hasParentAccess || hasSubAccess\n\n return (\n <div className=\"flex h-full flex-col p-6\">\n {/* Back navigation */}\n <div className=\"mb-4\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onBack}\n className=\"gap-1.5 text-muted-foreground hover:text-foreground -ml-2\"\n aria-label={`Back to ${parent.displayName}`}\n >\n <ArrowLeft className=\"size-4\" />\n <span>{parent.displayName}</span>\n </Button>\n </div>\n\n {/* Sub-resource title */}\n <div className=\"border-b pb-4\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <h2 className=\"text-xl font-semibold\">{subResource.displayName}</h2>\n {subResource.tier && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {subResource.tier}\n </Badge>\n )}\n </div>\n {subResource.description && (\n <p className=\"text-sm text-muted-foreground mt-1\">\n {subResource.description}\n </p>\n )}\n {(subResource.aliases ?? []).length > 0 && (\n <p className=\"text-xs text-muted-foreground mt-1\">\n Also known as: {(subResource.aliases ?? []).join(', ')}\n </p>\n )}\n </div>\n\n {/* Two-step access section */}\n {hasAnyAccess && (\n <div className=\"mt-6 space-y-6\">\n <h3 className=\"text-sm font-medium\">How to get access</h3>\n\n {hasTwoStepAccess ? (\n <>\n {/* Step 1: Parent access */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <StepBadge step={1} />\n <span className=\"text-sm font-medium text-muted-foreground\">\n {parent.displayName}\n </span>\n </div>\n <div className=\"pl-8\">\n <AccessRequestSection\n app={parent}\n approvalMethods={approvalMethods}\n />\n </div>\n </div>\n\n {/* Step 2: Sub-resource access */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <StepBadge step={2} />\n <span className=\"text-sm font-medium text-muted-foreground\">\n {subResource.displayName}\n </span>\n </div>\n <div className=\"pl-8\">\n <AccessRequestSection\n app={subResource}\n approvalMethods={approvalMethods}\n />\n </div>\n </div>\n </>\n ) : (\n /* Single-step: only one of them has access info */\n <AccessRequestSection\n app={hasParentAccess ? parent : subResource}\n approvalMethods={approvalMethods}\n />\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;AAgBA,SAAS,UAAU,EAAE,QAA0B;AAC7C,SACE,oBAAC,QAAA,EAAK,WAAU,iIACb,UAAA,MACH;AAEJ;AAEO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,kBAAkB,CAAC,CAAC,OAAO;AACjC,QAAM,eAAe,CAAC,CAAC,YAAY;AACnC,QAAM,mBAAmB,mBAAmB;AAC5C,QAAM,eAAe,mBAAmB;AAExC,SACE,qBAAC,OAAA,EAAI,WAAU,4BAEb,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,QACb,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAU;AAAA,QACV,cAAY,WAAW,OAAO,WAAW;AAAA,QAEzC,UAAA;AAAA,UAAA,oBAAC,WAAA,EAAU,WAAU,SAAA,CAAS;AAAA,UAC9B,oBAAC,QAAA,EAAM,UAAA,OAAO,YAAA,CAAY;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAE9B;AAAA,IAGA,qBAAC,OAAA,EAAI,WAAU,iBACb,UAAA;AAAA,MAAA,qBAAC,OAAA,EAAI,WAAU,qCACb,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAG,WAAU,yBAAyB,UAAA,YAAY,aAAY;AAAA,QAC9D,YAAY,QACX,oBAAC,OAAA,EAAM,SAAQ,WAAU,WAAU,WAChC,UAAA,YAAY,KAAA,CACf;AAAA,MAAA,GAEJ;AAAA,MACC,YAAY,eACX,oBAAC,OAAE,WAAU,sCACV,sBAAY,aACf;AAAA,OAEA,YAAY,WAAW,CAAA,GAAI,SAAS,KACpC,qBAAC,KAAA,EAAE,WAAU,sCAAqC,UAAA;AAAA,QAAA;AAAA,SAC/B,YAAY,WAAW,CAAA,GAAI,KAAK,IAAI;AAAA,MAAA,EAAA,CACvD;AAAA,IAAA,GAEJ;AAAA,IAGC,gBACC,qBAAC,OAAA,EAAI,WAAU,kBACb,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,uBAAsB,UAAA,qBAAiB;AAAA,MAEpD,mBACC,qBAAA,UAAA,EAEE,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,aACb,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,oBAAC,WAAA,EAAU,MAAM,EAAA,CAAG;AAAA,YACpB,oBAAC,QAAA,EAAK,WAAU,6CACb,iBAAO,YAAA,CACV;AAAA,UAAA,GACF;AAAA,UACA,oBAAC,OAAA,EAAI,WAAU,QACb,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK;AAAA,cACL;AAAA,YAAA;AAAA,UAAA,EACF,CACF;AAAA,QAAA,GACF;AAAA,QAGA,qBAAC,OAAA,EAAI,WAAU,aACb,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,oBAAC,WAAA,EAAU,MAAM,EAAA,CAAG;AAAA,YACpB,oBAAC,QAAA,EAAK,WAAU,6CACb,sBAAY,YAAA,CACf;AAAA,UAAA,GACF;AAAA,UACA,oBAAC,OAAA,EAAI,WAAU,QACb,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK;AAAA,cACL;AAAA,YAAA;AAAA,UAAA,EACF,CACF;AAAA,QAAA,EAAA,CACF;AAAA,MAAA,GACF;AAAA;AAAA,QAGA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,KAAK,kBAAkB,SAAS;AAAA,YAChC;AAAA,UAAA;AAAA,QAAA;AAAA;AAAA,IACF,EAAA,CAEJ;AAAA,EAAA,GAEJ;AAEJ;"}
@@ -5,7 +5,7 @@ interface SubResourcesSectionProps {
5
5
  * Initial filter text (#38 item C). When the user reached this app by
6
6
  * searching a term that matched a sub-resource, seed the sub-resource filter
7
7
  * with that term so the matched child is revealed instead of buried in a
8
- * long list (e.g. searching "biom" → open AWS Console → the biomarker
8
+ * long list (e.g. searching "data" → open Cloud Console → the matching
9
9
  * account is pre-filtered). Only applied when it actually matches a child.
10
10
  */
11
11
  initialSearch?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Check, Copy, Search } from 'lucide-react'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\nimport { markdownToPlainText } from '~/modules/appCatalog/utils/markdownToPlainText'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n /**\n * Initial filter text (#38 item C). When the user reached this app by\n * searching a term that matched a sub-resource, seed the sub-resource filter\n * with that term so the matched child is revealed instead of buried in a\n * long list (e.g. searching \"biom\" → open AWS Console → the biomarker\n * account is pre-filtered). Only applied when it actually matches a child.\n */\n initialSearch?: string\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nfunction CopyAccountIdButton({ accountId }: { accountId: string }) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n }\n }, [])\n\n const handleCopy = useCallback(() => {\n void navigator.clipboard.writeText(accountId).then(() => {\n setCopied(true)\n timeoutRef.current = setTimeout(() => setCopied(false), 1500)\n })\n }, [accountId])\n\n return (\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"flex items-center gap-1 font-mono text-xs text-muted-foreground hover:text-foreground transition-colors group\"\n title=\"Copy account ID\"\n >\n <span>{accountId}</span>\n {copied ? (\n <Check className=\"size-3 text-green-500 shrink-0\" />\n ) : (\n <Copy className=\"size-3 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity\" />\n )}\n </button>\n )\n}\n\nexport function SubResourcesSection({\n subResources,\n initialSearch,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n // Seed the filter with the incoming query ONLY if it matches a child, so we\n // reveal the matched sub-resource without hiding everything on a non-match.\n const seededSearch = useMemo(() => {\n const q = initialSearch?.trim().toLowerCase()\n if (!q) return ''\n const hit = subResources.some(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)),\n )\n return hit ? initialSearch!.trim() : ''\n }, [initialSearch, subResources])\n const [search, setSearch] = useState(seededSearch)\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tier) tiers.add(sr.tier)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false) ||\n (\n (sr.extra as Record<string, unknown>).awsAccountId as\n | string\n | undefined\n )?.includes(q) === true,\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n <TableHead className=\"w-[140px]\">AWS Account</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={5}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = (\n sr.accessMaintainerGroupSlugs ?? []\n ).flatMap((groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n })\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {(sr.aliases ?? []).length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {(sr.aliases ?? []).join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {markdownToPlainText(sr.description)}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tier && (\n <Badge\n variant={getTierBadgeVariant(sr.tier)}\n className={`text-xs ${getTierBadgeClassName(sr.tier)}`}\n >\n {getTierDisplayLabel(sr.tier)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {(() => {\n const accountId = (\n sr.extra as Record<string, unknown> | null | undefined\n )?.awsAccountId as string | undefined\n return accountId ? (\n <CopyAccountIdButton accountId={accountId} />\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n })()}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Check, Copy, Search } from 'lucide-react'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\nimport { markdownToPlainText } from '~/modules/appCatalog/utils/markdownToPlainText'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n /**\n * Initial filter text (#38 item C). When the user reached this app by\n * searching a term that matched a sub-resource, seed the sub-resource filter\n * with that term so the matched child is revealed instead of buried in a\n * long list (e.g. searching \"data\" → open Cloud Console → the matching\n * account is pre-filtered). Only applied when it actually matches a child.\n */\n initialSearch?: string\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nfunction CopyAccountIdButton({ accountId }: { accountId: string }) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n }\n }, [])\n\n const handleCopy = useCallback(() => {\n void navigator.clipboard.writeText(accountId).then(() => {\n setCopied(true)\n timeoutRef.current = setTimeout(() => setCopied(false), 1500)\n })\n }, [accountId])\n\n return (\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"flex items-center gap-1 font-mono text-xs text-muted-foreground hover:text-foreground transition-colors group\"\n title=\"Copy account ID\"\n >\n <span>{accountId}</span>\n {copied ? (\n <Check className=\"size-3 text-green-500 shrink-0\" />\n ) : (\n <Copy className=\"size-3 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity\" />\n )}\n </button>\n )\n}\n\nexport function SubResourcesSection({\n subResources,\n initialSearch,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n // Seed the filter with the incoming query ONLY if it matches a child, so we\n // reveal the matched sub-resource without hiding everything on a non-match.\n const seededSearch = useMemo(() => {\n const q = initialSearch?.trim().toLowerCase()\n if (!q) return ''\n const hit = subResources.some(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)),\n )\n return hit ? initialSearch!.trim() : ''\n }, [initialSearch, subResources])\n const [search, setSearch] = useState(seededSearch)\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tier) tiers.add(sr.tier)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false) ||\n (\n (sr.extra as Record<string, unknown>).awsAccountId as\n | string\n | undefined\n )?.includes(q) === true,\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n <TableHead className=\"w-[140px]\">AWS Account</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={5}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = (\n sr.accessMaintainerGroupSlugs ?? []\n ).flatMap((groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n })\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {(sr.aliases ?? []).length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {(sr.aliases ?? []).join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {markdownToPlainText(sr.description)}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tier && (\n <Badge\n variant={getTierBadgeVariant(sr.tier)}\n className={`text-xs ${getTierBadgeClassName(sr.tier)}`}\n >\n {getTierDisplayLabel(sr.tier)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {(() => {\n const accountId = (\n sr.extra as Record<string, unknown> | null | undefined\n )?.awsAccountId as string | undefined\n return accountId ? (\n <CopyAccountIdButton accountId={accountId} />\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n })()}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -14,10 +14,16 @@ export interface AppCatalogGridProps {
14
14
  onClearFilters?: () => void;
15
15
  /** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */
16
16
  onClosePanel?: () => void;
17
+ /** Currently selected subresource slug */
18
+ selectedSubSlug?: string;
19
+ /** Called when user clicks a sub-row */
20
+ onSubClick?: (parentSlug: string, subSlug: string) => void;
21
+ /** Called when user clicks Back in sub detail */
22
+ onBackToParent?: () => void;
17
23
  }
18
24
  export declare function AppDetails({ app, onAppClick, onClosePanel, }: {
19
25
  app: Resource;
20
26
  onAppClick?: (app: Resource) => void;
21
27
  onClosePanel: () => void;
22
28
  }): import("react/jsx-runtime").JSX.Element;
23
- export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, searchQuery, totalAppsCount, onClearFilters, onClosePanel, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, searchQuery, totalAppsCount, onClearFilters, onClosePanel, selectedSubSlug, onSubClick, onBackToParent, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
@@ -25,6 +25,7 @@ import { markdownToPlainText } from "../../utils/markdownToPlainText.js";
25
25
  import { formatRelativeTime } from "../../utils/formatRelativeTime.js";
26
26
  import { TierVariantsSection } from "../components/TierVariantsSection.js";
27
27
  import { SubResourcesSection } from "../components/SubResourcesSection.js";
28
+ import { SubResourceDetailPanel } from "../components/SubResourceDetailPanel.js";
28
29
  import { getChildResources } from "../../utils/resolveHelpers.js";
29
30
  function getIconUrl(iconName) {
30
31
  return `/api/icons/${iconName}`;
@@ -549,10 +550,13 @@ function AppCatalogGrid({
549
550
  searchQuery,
550
551
  totalAppsCount,
551
552
  onClearFilters,
552
- onClosePanel
553
+ onClosePanel,
554
+ selectedSubSlug,
555
+ onSubClick,
556
+ onBackToParent
553
557
  }) {
554
- const { resources: allResourcesForDetail } = useAppCatalogContext();
555
- const selectedApp = selectedAppSlug ? apps.find((a) => a.slug === selectedAppSlug) ?? allResourcesForDetail.find((a) => a.slug === selectedAppSlug) : null;
558
+ const { resources: allResources2, approvalMethods } = useAppCatalogContext();
559
+ const selectedApp = selectedAppSlug ? apps.find((a) => a.slug === selectedAppSlug) ?? allResources2.find((a) => a.slug === selectedAppSlug && !a.parentSlug) ?? null : null;
556
560
  const groupedApps = groupApps(apps, groupingDefinition, hasSearch);
557
561
  const appsInDisplayOrder = React__default.useMemo(
558
562
  () => groupedApps.flatMap((group) => group.apps),
@@ -564,26 +568,31 @@ function AppCatalogGrid({
564
568
  onAppClick
565
569
  });
566
570
  const { recordClick: recordLaunch } = useAppClickHistory();
567
- const { resources: allResources2 } = useAppCatalogContext();
568
- const matchedSubResourceMap = React__default.useMemo(() => {
571
+ const matchedSubResourcesMap = React__default.useMemo(() => {
569
572
  const map = /* @__PURE__ */ new Map();
570
573
  if (!(searchQuery == null ? void 0 : searchQuery.trim()) || allResources2.length === 0) return map;
571
574
  const queryTerms = searchQuery.trim().toLowerCase().split(/\s+/).filter(Boolean);
572
575
  const allTermsMatch = (text) => queryTerms.every((term) => text.includes(term));
573
576
  for (const r of allResources2) {
574
577
  if (!r.parentSlug) continue;
575
- if (map.has(r.parentSlug)) continue;
576
578
  const nameMatch = allTermsMatch(r.displayName.toLowerCase());
577
579
  const aliasMatch = (r.aliases ?? []).some(
578
580
  (a) => allTermsMatch(a.toLowerCase())
579
581
  );
580
582
  const descMatch = r.description ? allTermsMatch(r.description.toLowerCase()) : false;
581
583
  if (nameMatch || aliasMatch || descMatch) {
582
- map.set(r.parentSlug, r.displayName);
584
+ if (!map.has(r.parentSlug)) {
585
+ map.set(r.parentSlug, []);
586
+ }
587
+ map.get(r.parentSlug).push(r);
583
588
  }
584
589
  }
585
590
  return map;
586
591
  }, [searchQuery, allResources2]);
592
+ const [expandedParentSlugs, setExpandedParentSlugs] = React__default.useState(() => /* @__PURE__ */ new Set());
593
+ React__default.useEffect(() => {
594
+ setExpandedParentSlugs(/* @__PURE__ */ new Set());
595
+ }, [searchQuery]);
587
596
  const columns = React__default.useMemo(
588
597
  () => [
589
598
  {
@@ -625,20 +634,63 @@ function AppCatalogGrid({
625
634
  {
626
635
  id: "description",
627
636
  header: "Description",
628
- cell: ({ row }) => /* @__PURE__ */ jsxs("div", { children: [
629
- /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground line-clamp-2", children: /* @__PURE__ */ jsx(
630
- HighlightedText,
631
- {
632
- text: row.original.description ? markdownToPlainText(row.original.description) : "—",
633
- searchQuery
634
- }
635
- ) }),
636
- matchedSubResourceMap.get(row.original.slug) && /* @__PURE__ */ jsxs("div", { className: "text-xs text-primary mt-0.5", children: [
637
- "Matched sub-resource:",
638
- " ",
639
- matchedSubResourceMap.get(row.original.slug)
640
- ] })
641
- ] })
637
+ cell: ({ row }) => {
638
+ var _a;
639
+ return /* @__PURE__ */ jsxs("div", { children: [
640
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground line-clamp-2", children: /* @__PURE__ */ jsx(
641
+ HighlightedText,
642
+ {
643
+ text: row.original.description ? markdownToPlainText(row.original.description) : "—",
644
+ searchQuery
645
+ }
646
+ ) }),
647
+ (((_a = matchedSubResourcesMap.get(row.original.slug)) == null ? void 0 : _a.length) ?? 0) > 0 && (() => {
648
+ const matchedSubs = matchedSubResourcesMap.get(
649
+ row.original.slug
650
+ );
651
+ const COLLAPSED_LIMIT = 5;
652
+ const isExpanded = expandedParentSlugs.has(row.original.slug);
653
+ const visibleSubs = isExpanded ? matchedSubs : matchedSubs.slice(0, COLLAPSED_LIMIT);
654
+ const hiddenCount = matchedSubs.length - COLLAPSED_LIMIT;
655
+ return /* @__PURE__ */ jsxs("div", { className: "mt-1 space-y-0.5", children: [
656
+ visibleSubs.map((sub) => /* @__PURE__ */ jsx(
657
+ "button",
658
+ {
659
+ type: "button",
660
+ onClick: (e) => {
661
+ e.stopPropagation();
662
+ onSubClick == null ? void 0 : onSubClick(row.original.slug, sub.slug);
663
+ },
664
+ className: cn(
665
+ "flex items-center gap-1.5 text-xs pl-3 border-l-2 w-full text-left py-0.5 rounded-sm hover:bg-accent/50 transition-colors",
666
+ selectedSubSlug === sub.slug ? "border-primary text-primary font-medium" : "border-muted-foreground/30 text-muted-foreground hover:text-foreground"
667
+ ),
668
+ children: /* @__PURE__ */ jsx("span", { children: sub.displayName })
669
+ },
670
+ sub.slug
671
+ )),
672
+ !isExpanded && hiddenCount > 0 && /* @__PURE__ */ jsxs(
673
+ "button",
674
+ {
675
+ type: "button",
676
+ onClick: (e) => {
677
+ e.stopPropagation();
678
+ setExpandedParentSlugs(
679
+ (prev) => /* @__PURE__ */ new Set([...prev, row.original.slug])
680
+ );
681
+ },
682
+ className: "text-xs pl-3 border-l-2 border-muted-foreground/30 text-muted-foreground hover:text-primary w-full text-left py-0.5",
683
+ children: [
684
+ "... ",
685
+ hiddenCount,
686
+ " more"
687
+ ]
688
+ }
689
+ )
690
+ ] });
691
+ })()
692
+ ] });
693
+ }
642
694
  },
643
695
  {
644
696
  // Secondary "launch" action (#31): the primary row click opens the
@@ -668,7 +720,15 @@ function AppCatalogGrid({
668
720
  }
669
721
  }
670
722
  ],
671
- [searchQuery, matchedSubResourceMap, recordLaunch]
723
+ [
724
+ searchQuery,
725
+ matchedSubResourcesMap,
726
+ expandedParentSlugs,
727
+ selectedSubSlug,
728
+ onSubClick,
729
+ setExpandedParentSlugs,
730
+ recordLaunch
731
+ ]
672
732
  );
673
733
  const table = useReactTable({
674
734
  data: apps,
@@ -808,7 +868,7 @@ function AppCatalogGrid({
808
868
  defaultSize: 40,
809
869
  minSize: 25,
810
870
  className: "overflow-hidden",
811
- children: /* @__PURE__ */ jsx("div", { className: "h-full overflow-y-auto border-l bg-background pl-4", children: selectedApp ? /* @__PURE__ */ jsxs("div", { className: "relative", children: [
871
+ children: /* @__PURE__ */ jsx("div", { className: "h-full overflow-y-auto border-l bg-background pl-4", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
812
872
  /* @__PURE__ */ jsx(
813
873
  Button,
814
874
  {
@@ -820,15 +880,32 @@ function AppCatalogGrid({
820
880
  children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
821
881
  }
822
882
  ),
823
- /* @__PURE__ */ jsx(
824
- AppDetails,
825
- {
826
- app: selectedApp,
827
- onAppClick,
828
- onClosePanel: handleClosePanel
883
+ selectedApp && (() => {
884
+ const app = selectedApp;
885
+ const selectedSub = selectedSubSlug ? allResources2.find(
886
+ (r) => r.slug === selectedSubSlug && r.parentSlug === app.slug
887
+ ) : null;
888
+ if (selectedSub != null) {
889
+ return /* @__PURE__ */ jsx(
890
+ SubResourceDetailPanel,
891
+ {
892
+ subResource: selectedSub,
893
+ parent: app,
894
+ approvalMethods,
895
+ onBack: onBackToParent ?? (() => void 0)
896
+ }
897
+ );
829
898
  }
830
- )
831
- ] }) : null })
899
+ return /* @__PURE__ */ jsx(
900
+ AppDetails,
901
+ {
902
+ app: selectedApp,
903
+ onAppClick,
904
+ onClosePanel: handleClosePanel
905
+ }
906
+ );
907
+ })()
908
+ ] }) })
832
909
  }
833
910
  )
834
911
  ] })