@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.
- package/dist/esm/__tests__/integration/mock-backend/magazines.d.ts +1 -1
- package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +16 -0
- package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +12 -0
- package/dist/esm/api/infra/trpc.d.ts +24 -0
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +5 -2
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +1 -1
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.d.ts +9 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +90 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +7 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +114 -34
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js +8 -8
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +31 -4
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js +41 -20
- package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +2 -0
- package/dist/esm/routes/_layout/app.$slug.d.ts +1 -0
- package/dist/esm/routes/_layout/app._slug.js +2 -1
- package/dist/esm/routes/_layout/app._slug.js.map +1 -1
- package/dist/esm/routes/_layout/catalog.apps.index.d.ts +1 -0
- package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
- package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
- package/package.json +5 -7
- package/src/__tests__/integration/appCatalog.integration.test.ts +264 -5
- package/src/__tests__/integration/appDeepLink.integration.test.ts +46 -19
- package/src/__tests__/integration/appFreshness.integration.test.ts +74 -4
- package/src/__tests__/integration/crossRefLink.integration.test.ts +8 -8
- package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
- package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +2 -0
- package/src/__tests__/integration/mock-backend/magazines.ts +24 -24
- package/src/__tests__/integration/replacementLink.integration.test.ts +2 -2
- package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +2 -2
- package/src/__tests__/integration/serviceDesks.integration.test.ts +31 -24
- package/src/__tests__/integration/tools/AppDetailTools.ts +57 -0
- package/src/__tests__/integration/tools/CatalogTools.ts +77 -4
- package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +6 -6
- package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +1 -1
- package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +11 -7
- package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +1 -1
- package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +124 -0
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +1 -1
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +140 -36
- package/src/modules/appCatalog/ui/launcher/LauncherHome.tsx +12 -8
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +49 -8
- package/src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx +37 -9
- package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
- package/src/routes/_layout/app.$slug.tsx +1 -0
- 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 (
|
|
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
|
|
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
|
|
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 "
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAS,oBACP,UACqD;AACrD,MAAI,aAAa,UAAU,aAAa,aAAc,QAAO;AAC7D,MAAI,aAAa,SAAS,aAAa,UAAW,QAAO;AACzD,MAAI,aAAa,UAAW,QAAO;AACnC,MAAI,aAAa,UAAW,QAAO;AACnC,SAAO;AACT;AAEA,SAAS,sBAAsB,UAA0B;AACvD,MAAI,aAAa;AACf,WAAO;AACT,MAAI,aAAa;AACf,WAAO;AACT,SAAO;AACT;AAEA,SAAS,oBAAoB,UAA0B;AACrD,MAAI,aAAa,UAAW,QAAO;AACnC,MAAI,aAAa,UAAW,QAAO;AACnC,MAAI,aAAa,UAAU,aAAa,aAAc,QAAO;AAC7D,MAAI,aAAa,MAAO,QAAO;AAC/B,MAAI,aAAa,UAAW,QAAO;AACnC,SAAO;AACT;AAEA,SAAS,oBAAoB,EAAE,aAAoC;AACjE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA8B,IAAI;AAErD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AAAA,IACzD;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,aAAa,YAAY,MAAM;AACnC,SAAK,UAAU,UAAU,UAAU,SAAS,EAAE,KAAK,MAAM;AACvD,gBAAU,IAAI;AACd,iBAAW,UAAU,WAAW,MAAM,UAAU,KAAK,GAAG,IAAI;AAAA,IAC9D,CAAC;AAAA,EACH,GAAG,CAAC,SAAS,CAAC;AAEd,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,WAAU;AAAA,MACV,OAAM;AAAA,MAEN,UAAA;AAAA,QAAA,oBAAC,UAAM,UAAA,UAAA,CAAU;AAAA,QAChB,6BACE,OAAA,EAAM,WAAU,kCAAiC,IAElD,oBAAC,MAAA,EAAK,WAAU,uEAAA,CAAuE;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAI/F;AAEO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,EAAE,OAAA,IAAW,qBAAA;AAGnB,QAAM,eAAe,QAAQ,MAAM;AACjC,UAAM,IAAI,+CAAe,OAAO;AAChC,QAAI,CAAC,EAAG,QAAO;AACf,UAAM,MAAM,aAAa;AAAA,MACvB,CAAC,OACC,GAAG,YAAY,cAAc,SAAS,CAAC,MACtC,GAAG,WAAW,CAAA,GAAI,KAAK,CAAC,MAAM,EAAE,YAAA,EAAc,SAAS,CAAC,CAAC;AAAA,IAAA;AAE9D,WAAO,MAAM,cAAe,KAAA,IAAS;AAAA,EACvC,GAAG,CAAC,eAAe,YAAY,CAAC;AAChC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,YAAY;AACjD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAiB,KAAK;AAE1D,QAAM,cAAc,QAAQ,MAAM;AAChC,UAAM,4BAAY,IAAA;AAClB,eAAW,MAAM,cAAc;AAC7B,UAAI,GAAG,KAAM,OAAM,IAAI,GAAG,IAAI;AAAA,IAChC;AACA,WAAO,CAAC,GAAG,KAAK,EAAE,KAAA;AAAA,EACpB,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,WAAW,QAAQ,MAAM;AAC7B,QAAI,SAAS;AAEb,QAAI,eAAe,OAAO;AACxB,eAAS,OAAO,OAAO,CAAC,OAAO,GAAG,SAAS,UAAU;AAAA,IACvD;AAEA,QAAI,OAAO,QAAQ;AACjB,YAAM,IAAI,OAAO,KAAA,EAAO,YAAA;AACxB,eAAS,OAAO;AAAA,QACd,CAAC,OAAA;;AACC,oBAAG,YAAY,YAAA,EAAc,SAAS,CAAC,MACtC,GAAG,WAAW,CAAA,GAAI,KAAK,CAAC,MAAM,EAAE,YAAA,EAAc,SAAS,CAAC,CAAC,QACzD,QAAG,gBAAH,mBAAgB,cAAc,SAAS,OAAM,YAE3C,QAAG,MAAkC,iBAArC,mBAGA,SAAS,QAAO;AAAA;AAAA,MAAA;AAAA,IAEzB;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,cAAc,QAAQ,UAAU,CAAC;AAErC,MAAI,aAAa,WAAW,EAAG,QAAO;AAEtC,SACE,qBAAC,OAAA,EAAI,WAAU,aACb,UAAA;AAAA,IAAA,oBAAC,SAAI,WAAU,qCACb,UAAA,qBAAC,OAAA,EAAI,WAAU,uBAAsB,UAAA;AAAA,MAAA;AAAA,MACnB,SAAS;AAAA,MAAO;AAAA,MAAK,aAAa;AAAA,MAAO;AAAA,IAAA,EAAA,CAC3D,EAAA,CACF;AAAA,IAGA,qBAAC,OAAA,EAAI,WAAU,cACb,UAAA;AAAA,MAAA,qBAAC,OAAA,EAAI,WAAU,mBACb,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAO,WAAU,yDAAA,CAAyD;AAAA,QAC3E;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,aAAY;AAAA,YACZ,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,UAAU,EAAE,OAAO,KAAK;AAAA,YACzC,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,GACF;AAAA,MACC,YAAY,SAAS,KACpB,qBAAC,UAAO,OAAO,YAAY,eAAe,eACxC,UAAA;AAAA,QAAA,oBAAC,iBAAc,WAAU,iBACvB,8BAAC,aAAA,EAAY,aAAY,aAAY,EAAA,CACvC;AAAA,6BACC,eAAA,EACC,UAAA;AAAA,UAAA,oBAAC,YAAA,EAAW,OAAM,OAAM,UAAA,aAAS;AAAA,UAChC,YAAY,IAAI,CAAC,SAChB,oBAAC,cAAsB,OAAO,MAC3B,UAAA,KAAA,GADc,IAEjB,CACD;AAAA,QAAA,EAAA,CACH;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,GAEJ;AAAA,IAGA,oBAAC,OAAA,EAAI,WAAU,iDACb,+BAAC,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,aAAA,EACC,+BAAC,UAAA,EACC,UAAA;AAAA,QAAA,oBAAC,aAAU,UAAA,OAAA,CAAI;AAAA,QACf,oBAAC,WAAA,EAAU,WAAU,YAAW,UAAA,QAAI;AAAA,QACpC,oBAAC,aAAU,UAAA,QAAA,CAAK;AAAA,QAChB,oBAAC,aAAU,UAAA,kBAAA,CAAe;AAAA,QAC1B,oBAAC,WAAA,EAAU,WAAU,aAAY,UAAA,cAAA,CAAW;AAAA,MAAA,EAAA,CAC9C,EAAA,CACF;AAAA,0BACC,WAAA,EACE,UAAA,SAAS,WAAW,wBAClB,UAAA,EACC,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,SAAS;AAAA,UACT,WAAU;AAAA,UACX,UAAA;AAAA,QAAA;AAAA,MAAA,EAED,CACF,IAEA,SAAS,IAAI,CAAC,OAAO;AAEnB,cAAM,qBACJ,GAAG,8BAA8B,CAAA,GACjC,QAAQ,CAAC,cAAc;AACvB,gBAAM,QAAQ,eAAe,QAAQ,SAAS;AAC9C,kBAAO,+BAAO,gBAAe,CAAA;AAAA,QAC/B,CAAC;AAED,cAAM,oBAAoB,CAAC,GAAG,IAAI,IAAI,iBAAiB,CAAC;AAExD,oCACG,UAAA,EACC,UAAA;AAAA,UAAA,qBAAC,WAAA,EACC,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,uBACZ,UAAA,GAAG,aACN;AAAA,aACE,GAAG,WAAW,CAAA,GAAI,SAAS,KAC3B,oBAAC,OAAA,EAAI,WAAU,wCACX,cAAG,WAAW,CAAA,GAAI,KAAK,IAAI,GAC/B;AAAA,YAED,GAAG,eACF,oBAAC,OAAA,EAAI,WAAU,wCACZ,UAAA,oBAAoB,GAAG,WAAW,EAAA,CACrC;AAAA,UAAA,GAEJ;AAAA,UACA,oBAAC,WAAA,EACE,UAAA,GAAG,QACF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,SAAS,oBAAoB,GAAG,IAAI;AAAA,cACpC,WAAW,WAAW,sBAAsB,GAAG,IAAI,CAAC;AAAA,cAEnD,UAAA,oBAAoB,GAAG,IAAI;AAAA,YAAA;AAAA,UAAA,GAGlC;AAAA,UACA,oBAAC,aACE,UAAA,GAAG,uCACD,aAAA,EAAY,MAAM,GAAG,gBAAA,CAAiB,EAAA,CAE3C;AAAA,UACA,oBAAC,WAAA,EACE,UAAA,kBAAkB,SAAS,IAC1B,oBAAC,OAAA,EAAI,WAAU,wBACZ,UAAA,kBAAkB,IAAI,CAAC,mCACrB,aAAA,EAA6B,MAAM,WAAA,GAAlB,UAA8B,CACjD,EAAA,CACH,IAEA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,IAAA,CAAC,GAE7C;AAAA,UACA,oBAAC,aACG,WAAA,MAAM;;AACN,kBAAM,aACJ,QAAG,UAAH,mBACC;AACH,mBAAO,gCACJ,qBAAA,EAAoB,UAAA,CAAsB,IAE3C,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,IAAA,CAAC;AAAA,UAE7C,KAAG,CACL;AAAA,QAAA,EAAA,GArDa,GAAG,IAsDlB;AAAA,MAEJ,CAAC,EAAA,CAEL;AAAA,IAAA,EAAA,CACF,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
|
|
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
|
-
|
|
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:
|
|
552
|
-
const selectedApp = selectedAppSlug ? apps.find((a) => a.slug === selectedAppSlug) ??
|
|
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
|
|
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.
|
|
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 }) =>
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
{
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
[
|
|
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:
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
app
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
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
|
] })
|