@igstack/app-catalog-frontend-core 0.16.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 (56) 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 +5 -2
  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 +114 -34
  15. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js +8 -8
  17. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js.map +1 -1
  18. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +31 -4
  19. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  20. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js +41 -20
  21. package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js.map +1 -1
  22. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
  23. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
  24. package/dist/esm/routeTree.gen.d.ts +2 -0
  25. package/dist/esm/routes/_layout/app.$slug.d.ts +1 -0
  26. package/dist/esm/routes/_layout/app._slug.js +2 -1
  27. package/dist/esm/routes/_layout/app._slug.js.map +1 -1
  28. package/dist/esm/routes/_layout/catalog.apps.index.d.ts +1 -0
  29. package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
  30. package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
  31. package/package.json +5 -7
  32. package/src/__tests__/integration/appCatalog.integration.test.ts +264 -5
  33. package/src/__tests__/integration/appDeepLink.integration.test.ts +46 -19
  34. package/src/__tests__/integration/appFreshness.integration.test.ts +74 -4
  35. package/src/__tests__/integration/crossRefLink.integration.test.ts +8 -8
  36. package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
  37. package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +2 -0
  38. package/src/__tests__/integration/mock-backend/magazines.ts +24 -24
  39. package/src/__tests__/integration/replacementLink.integration.test.ts +2 -2
  40. package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +2 -2
  41. package/src/__tests__/integration/serviceDesks.integration.test.ts +31 -24
  42. package/src/__tests__/integration/tools/AppDetailTools.ts +57 -0
  43. package/src/__tests__/integration/tools/CatalogTools.ts +77 -4
  44. package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +6 -6
  45. package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +1 -1
  46. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +11 -7
  47. package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +1 -1
  48. package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +124 -0
  49. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +1 -1
  50. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +140 -36
  51. package/src/modules/appCatalog/ui/launcher/LauncherHome.tsx +12 -8
  52. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +49 -8
  53. package/src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx +37 -9
  54. package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
  55. package/src/routes/_layout/app.$slug.tsx +1 -0
  56. 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;
@@ -20,6 +20,9 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
20
20
  } | undefined;
21
21
  createdAt?: string | undefined;
22
22
  type?: string | undefined;
23
+ lastCheckedAt?: string | null | undefined;
24
+ nextCheckAfter?: string | null | undefined;
25
+ lastContentChangeAt?: string | null | undefined;
23
26
  abbreviation?: string | undefined;
24
27
  nicknames?: string[] | undefined;
25
28
  description?: string | undefined;
@@ -50,6 +53,7 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
50
53
  iconName?: string | undefined;
51
54
  screenshotIds?: string[] | undefined;
52
55
  aiPrompt?: string | undefined;
56
+ aiMemory?: string | undefined;
53
57
  urlIssues?: string[] | undefined;
54
58
  tiers?: {
55
59
  tierSlug: string;
@@ -81,8 +85,6 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
81
85
  accessMaintainerGroupSlugs?: string[] | undefined;
82
86
  accessComments?: string | undefined;
83
87
  extra?: Record<string, unknown> | undefined;
84
- lastCheckedAt?: string | null | undefined;
85
- nextCheckAfter?: string | null | undefined;
86
88
  sources?: string[] | {
87
89
  sourceSlug: string;
88
90
  url: string;
@@ -90,6 +92,7 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
90
92
  }[] | undefined;
91
93
  freshness?: {
92
94
  lastCheckedAt: string | null;
95
+ lastContentChangeAt: string | null;
93
96
  isStale: boolean;
94
97
  } | undefined;
95
98
  }, Error, UpdateAppVariables, unknown>;
@@ -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}`;
@@ -347,11 +348,14 @@ function AppDetails({
347
348
  "span",
348
349
  {
349
350
  className: "inline-flex items-center gap-1 text-xs text-muted-foreground",
350
- title: app.freshness.lastCheckedAt,
351
+ title: app.freshness.lastContentChangeAt ? `Content last changed ${app.freshness.lastContentChangeAt} · last checked ${app.freshness.lastCheckedAt}` : `Last checked ${app.freshness.lastCheckedAt}`,
351
352
  children: [
352
353
  /* @__PURE__ */ jsx(Clock, { className: "size-3" }),
353
- "Updated ",
354
- formatRelativeTime(app.freshness.lastCheckedAt),
354
+ "Updated",
355
+ " ",
356
+ formatRelativeTime(
357
+ app.freshness.lastContentChangeAt ?? app.freshness.lastCheckedAt
358
+ ),
355
359
  app.freshness.isStale && /* @__PURE__ */ jsxs("span", { className: "italic opacity-75", children: [
356
360
  " ",
357
361
  "· may be out of date"
@@ -546,10 +550,13 @@ function AppCatalogGrid({
546
550
  searchQuery,
547
551
  totalAppsCount,
548
552
  onClearFilters,
549
- onClosePanel
553
+ onClosePanel,
554
+ selectedSubSlug,
555
+ onSubClick,
556
+ onBackToParent
550
557
  }) {
551
- const { resources: allResourcesForDetail } = useAppCatalogContext();
552
- 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;
553
560
  const groupedApps = groupApps(apps, groupingDefinition, hasSearch);
554
561
  const appsInDisplayOrder = React__default.useMemo(
555
562
  () => groupedApps.flatMap((group) => group.apps),
@@ -561,26 +568,31 @@ function AppCatalogGrid({
561
568
  onAppClick
562
569
  });
563
570
  const { recordClick: recordLaunch } = useAppClickHistory();
564
- const { resources: allResources2 } = useAppCatalogContext();
565
- const matchedSubResourceMap = React__default.useMemo(() => {
571
+ const matchedSubResourcesMap = React__default.useMemo(() => {
566
572
  const map = /* @__PURE__ */ new Map();
567
573
  if (!(searchQuery == null ? void 0 : searchQuery.trim()) || allResources2.length === 0) return map;
568
574
  const queryTerms = searchQuery.trim().toLowerCase().split(/\s+/).filter(Boolean);
569
575
  const allTermsMatch = (text) => queryTerms.every((term) => text.includes(term));
570
576
  for (const r of allResources2) {
571
577
  if (!r.parentSlug) continue;
572
- if (map.has(r.parentSlug)) continue;
573
578
  const nameMatch = allTermsMatch(r.displayName.toLowerCase());
574
579
  const aliasMatch = (r.aliases ?? []).some(
575
580
  (a) => allTermsMatch(a.toLowerCase())
576
581
  );
577
582
  const descMatch = r.description ? allTermsMatch(r.description.toLowerCase()) : false;
578
583
  if (nameMatch || aliasMatch || descMatch) {
579
- 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);
580
588
  }
581
589
  }
582
590
  return map;
583
591
  }, [searchQuery, allResources2]);
592
+ const [expandedParentSlugs, setExpandedParentSlugs] = React__default.useState(() => /* @__PURE__ */ new Set());
593
+ React__default.useEffect(() => {
594
+ setExpandedParentSlugs(/* @__PURE__ */ new Set());
595
+ }, [searchQuery]);
584
596
  const columns = React__default.useMemo(
585
597
  () => [
586
598
  {
@@ -622,20 +634,63 @@ function AppCatalogGrid({
622
634
  {
623
635
  id: "description",
624
636
  header: "Description",
625
- cell: ({ row }) => /* @__PURE__ */ jsxs("div", { children: [
626
- /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground line-clamp-2", children: /* @__PURE__ */ jsx(
627
- HighlightedText,
628
- {
629
- text: row.original.description ? markdownToPlainText(row.original.description) : "—",
630
- searchQuery
631
- }
632
- ) }),
633
- matchedSubResourceMap.get(row.original.slug) && /* @__PURE__ */ jsxs("div", { className: "text-xs text-primary mt-0.5", children: [
634
- "Matched sub-resource:",
635
- " ",
636
- matchedSubResourceMap.get(row.original.slug)
637
- ] })
638
- ] })
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
+ }
639
694
  },
640
695
  {
641
696
  // Secondary "launch" action (#31): the primary row click opens the
@@ -665,7 +720,15 @@ function AppCatalogGrid({
665
720
  }
666
721
  }
667
722
  ],
668
- [searchQuery, matchedSubResourceMap, recordLaunch]
723
+ [
724
+ searchQuery,
725
+ matchedSubResourcesMap,
726
+ expandedParentSlugs,
727
+ selectedSubSlug,
728
+ onSubClick,
729
+ setExpandedParentSlugs,
730
+ recordLaunch
731
+ ]
669
732
  );
670
733
  const table = useReactTable({
671
734
  data: apps,
@@ -805,7 +868,7 @@ function AppCatalogGrid({
805
868
  defaultSize: 40,
806
869
  minSize: 25,
807
870
  className: "overflow-hidden",
808
- 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: [
809
872
  /* @__PURE__ */ jsx(
810
873
  Button,
811
874
  {
@@ -817,15 +880,32 @@ function AppCatalogGrid({
817
880
  children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
818
881
  }
819
882
  ),
820
- /* @__PURE__ */ jsx(
821
- AppDetails,
822
- {
823
- app: selectedApp,
824
- onAppClick,
825
- 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
+ );
826
898
  }
827
- )
828
- ] }) : null })
899
+ return /* @__PURE__ */ jsx(
900
+ AppDetails,
901
+ {
902
+ app: selectedApp,
903
+ onAppClick,
904
+ onClosePanel: handleClosePanel
905
+ }
906
+ );
907
+ })()
908
+ ] }) })
829
909
  }
830
910
  )
831
911
  ] })