@igstack/app-catalog-frontend-core 0.13.2 → 0.13.3
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/modules/appCatalog/context/AppCatalogContext.js +1 -0
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +2 -2
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/dist/index.css +10 -0
- package/package.json +4 -4
- package/src/index.css +10 -0
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +2 -2
|
@@ -9,6 +9,7 @@ import "clsx";
|
|
|
9
9
|
import "tailwind-merge";
|
|
10
10
|
import "@tanstack/react-query-devtools";
|
|
11
11
|
import "next-themes";
|
|
12
|
+
import "../../auth/AuthModalContext.js";
|
|
12
13
|
import "@radix-ui/react-dialog";
|
|
13
14
|
import "lucide-react";
|
|
14
15
|
import "../../config/GlobalConfigContext.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: mergeFrontendBuildId(\n data?.versions ?? {},\n uiSettings.frontendBuildId,\n ),\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":"
|
|
1
|
+
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: mergeFrontendBuildId(\n data?.versions ?? {},\n uiSettings.frontendBuildId,\n ),\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAE3C,QAAM,aAAa,cAAA;AAEnB,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,YAAW,6BAAM,cAAa,CAAA;AAAA,MAC9B;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,UAAS,6BAAM,YAAW,CAAA;AAAA,MAC1B,SAAQ,6BAAM,WAAU,CAAA;AAAA,MACxB,UAAU;AAAA,SACR,6BAAM,aAAY,CAAA;AAAA,QAClB,WAAW;AAAA,MAAA;AAAA,IACb;AAAA,IAEF;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,OAAAC,MAAA,6BAAM,aAAN,gBAAAA,IAAgB,YAAhB,gBAAAD,IAAyB,iBAAgB,SAAS;AACpD,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,wCAAM,aAAN,mBAAgB,YAAhB,mBAAyB,WAAW,CAAC;AAEzC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
|
|
@@ -142,7 +142,7 @@ function AccessRequestSection({
|
|
|
142
142
|
}
|
|
143
143
|
)
|
|
144
144
|
] }),
|
|
145
|
-
accessRequest.comments && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground prose prose-sm max-w-none
|
|
145
|
+
accessRequest.comments && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground prose prose-sm max-w-none", children: /* @__PURE__ */ jsx(ReactMarkdown, { components: { a: MarkdownLink }, children: accessRequest.comments }) }),
|
|
146
146
|
accessRequest.roles && accessRequest.roles.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
147
147
|
/* @__PURE__ */ jsx("h4", { className: "mb-2 text-sm font-medium", children: "Available Roles" }),
|
|
148
148
|
/* @__PURE__ */ jsx("div", { className: "rounded-lg border", children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
@@ -190,7 +190,7 @@ function AccessRequestSection({
|
|
|
190
190
|
className: "border rounded-lg px-4",
|
|
191
191
|
children: [
|
|
192
192
|
/* @__PURE__ */ jsx(AccordionTrigger, { className: "text-sm hover:no-underline py-3", children: "Post-Approval Instructions" }),
|
|
193
|
-
/* @__PURE__ */ jsx(AccordionContent, { className: "pb-3", children: /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground prose prose-sm max-w-none
|
|
193
|
+
/* @__PURE__ */ jsx(AccordionContent, { className: "pb-3", children: /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground prose prose-sm max-w-none", children: /* @__PURE__ */ jsx(ReactMarkdown, { components: { a: MarkdownLink }, children: accessRequest.postApprovalInstructions }) }) })
|
|
194
194
|
]
|
|
195
195
|
}
|
|
196
196
|
) })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { MarkdownLink } from './MarkdownText'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: Resource\n approvalMethods: AppApprovalMethod[]\n}\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n // Access UX (#31): some methods carry no clickable target — `noAccessRequired`\n // (open), `unknown` (undocumented), and bare `custom` with no comments. The\n // old UI rendered nothing for `custom`, leaving a blank/dead section. Instead\n // surface an explicit, actionable line so the user is never stranded:\n // - open → \"No request needed — just open it.\"\n // - unknown / undocumented custom → route them to the owner (shown below).\n const isOpen = approvalMethod?.type === 'noAccessRequired'\n const hasWrittenSteps = Boolean(\n accessRequest.requestPrompt || accessRequest.comments,\n )\n const isUndocumented =\n approvalMethod?.type === 'unknown' ||\n (approvalMethod?.type === 'custom' && !hasWrittenSteps)\n\n return (\n <div className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n How to get access\n </h3>\n\n {isOpen && (\n <p className=\"text-sm font-medium text-primary\">\n ✓ Open to everyone — no request needed. Just open it.\n </p>\n )}\n\n {isUndocumented && (\n <p className=\"text-sm text-muted-foreground\">\n Access process not documented yet — contact the owner below to find\n out.\n </p>\n )}\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions - Collapsible (secondary info) */}\n {accessRequest.postApprovalInstructions && (\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { MarkdownLink } from './MarkdownText'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: Resource\n approvalMethods: AppApprovalMethod[]\n}\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n // Access UX (#31): some methods carry no clickable target — `noAccessRequired`\n // (open), `unknown` (undocumented), and bare `custom` with no comments. The\n // old UI rendered nothing for `custom`, leaving a blank/dead section. Instead\n // surface an explicit, actionable line so the user is never stranded:\n // - open → \"No request needed — just open it.\"\n // - unknown / undocumented custom → route them to the owner (shown below).\n const isOpen = approvalMethod?.type === 'noAccessRequired'\n const hasWrittenSteps = Boolean(\n accessRequest.requestPrompt || accessRequest.comments,\n )\n const isUndocumented =\n approvalMethod?.type === 'unknown' ||\n (approvalMethod?.type === 'custom' && !hasWrittenSteps)\n\n return (\n <div className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n How to get access\n </h3>\n\n {isOpen && (\n <p className=\"text-sm font-medium text-primary\">\n ✓ Open to everyone — no request needed. Just open it.\n </p>\n )}\n\n {isUndocumented && (\n <p className=\"text-sm text-muted-foreground\">\n Access process not documented yet — contact the owner below to find\n out.\n </p>\n )}\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions - Collapsible (secondary info) */}\n {accessRequest.postApprovalInstructions && (\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@ import "clsx";
|
|
|
10
10
|
import "tailwind-merge";
|
|
11
11
|
import "@tanstack/react-query-devtools";
|
|
12
12
|
import "next-themes";
|
|
13
|
+
import "../../../auth/AuthModalContext.js";
|
|
13
14
|
import "@radix-ui/react-dialog";
|
|
14
15
|
import "../../../config/GlobalConfigContext.js";
|
|
15
16
|
import "../../../pluginCore/PluginManagerContext.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, Mail, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\n/**\n * Person chip. Clicking it opens a small popover exposing BOTH the full name\n * and the email, each with its own copy button — so the user picks what to copy\n * (previously a single button copied only the email). Email also links to\n * mailto:. Falls back to just the name (no popover) when no email is known.\n */\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n const email = person?.email\n\n // No email → nothing to pick between; render a plain, non-interactive chip.\n if (!email) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex\"\n title={`${displayName} · ${email}`}\n >\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <User className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<User className=\"size-3.5\" />}\n label=\"Name\"\n value={displayName}\n />\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={email}\n href={`mailto:${email}`}\n />\n </PopoverContent>\n </Popover>\n )\n}\n\n/** One copyable field (label + value + copy button), value optionally a link. */\nfunction CopyRow({\n icon,\n label,\n value,\n href,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n href?: string\n}) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(\n () => () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n },\n [],\n )\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n void navigator.clipboard.writeText(value)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [value],\n )\n\n return (\n <div className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted\">\n <span className=\"text-muted-foreground shrink-0\">{icon}</span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n {href ? (\n <a\n href={href}\n className=\"block truncate text-[13px] text-primary hover:underline\"\n >\n {value}\n </a>\n ) : (\n <span className=\"block truncate text-[13px]\">{value}</span>\n )}\n </span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors\"\n title={copied ? 'Copied!' : `Copy ${label.toLowerCase()}`}\n aria-label={`Copy ${label.toLowerCase()}`}\n >\n {copied ? (\n <Check className=\"size-3.5 text-green-600\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n </button>\n </div>\n )\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, Mail, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\n/**\n * Person chip. Clicking it opens a small popover exposing BOTH the full name\n * and the email, each with its own copy button — so the user picks what to copy\n * (previously a single button copied only the email). Email also links to\n * mailto:. Falls back to just the name (no popover) when no email is known.\n */\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n const email = person?.email\n\n // No email → nothing to pick between; render a plain, non-interactive chip.\n if (!email) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex\"\n title={`${displayName} · ${email}`}\n >\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <User className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<User className=\"size-3.5\" />}\n label=\"Name\"\n value={displayName}\n />\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={email}\n href={`mailto:${email}`}\n />\n </PopoverContent>\n </Popover>\n )\n}\n\n/** One copyable field (label + value + copy button), value optionally a link. */\nfunction CopyRow({\n icon,\n label,\n value,\n href,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n href?: string\n}) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(\n () => () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n },\n [],\n )\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n void navigator.clipboard.writeText(value)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [value],\n )\n\n return (\n <div className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted\">\n <span className=\"text-muted-foreground shrink-0\">{icon}</span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n {href ? (\n <a\n href={href}\n className=\"block truncate text-[13px] text-primary hover:underline\"\n >\n {value}\n </a>\n ) : (\n <span className=\"block truncate text-[13px]\">{value}</span>\n )}\n </span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors\"\n title={copied ? 'Copied!' : `Copy ${label.toLowerCase()}`}\n aria-label={`Copy ${label.toLowerCase()}`}\n >\n {copied ? (\n <Check className=\"size-3.5 text-green-600\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n </button>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,7 @@ import "clsx";
|
|
|
13
13
|
import "tailwind-merge";
|
|
14
14
|
import "@tanstack/react-query-devtools";
|
|
15
15
|
import "next-themes";
|
|
16
|
+
import "../../../auth/AuthModalContext.js";
|
|
16
17
|
import "@radix-ui/react-dialog";
|
|
17
18
|
import "../../../config/GlobalConfigContext.js";
|
|
18
19
|
import "../../../pluginCore/PluginManagerContext.js";
|
|
@@ -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 \"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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"}
|
|
@@ -11,6 +11,7 @@ import "clsx";
|
|
|
11
11
|
import "tailwind-merge";
|
|
12
12
|
import "@tanstack/react-query-devtools";
|
|
13
13
|
import "next-themes";
|
|
14
|
+
import "../../../auth/AuthModalContext.js";
|
|
14
15
|
import "@radix-ui/react-dialog";
|
|
15
16
|
import "../../../config/GlobalConfigContext.js";
|
|
16
17
|
import "../../../pluginCore/PluginManagerContext.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TierVariantsSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/TierVariantsSection.tsx"],"sourcesContent":["import type {\n AppAccessRequest,\n AppApprovalMethod,\n TierVariant,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, ExternalLinkIcon, Settings, Users } from 'lucide-react'\nimport { useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Badge } from '~/ui/badge'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { PersonBadge } from './PersonBadge'\n\ninterface TierVariantsSectionProps {\n tiers: TierVariant[]\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 getAccessIcon(type: string): React.ReactNode {\n switch (type) {\n case 'service':\n return <Bot className=\"size-4 text-primary shrink-0\" />\n case 'personTeam':\n return <Users className=\"size-4 text-primary shrink-0\" />\n default:\n return <Settings className=\"size-4 text-primary shrink-0\" />\n }\n}\n\n/** Compact inline access detail for a tier row */\nfunction TierAccessDetail({\n accessRequest,\n methods,\n}: {\n accessRequest: AppAccessRequest\n methods: AppApprovalMethod[]\n}) {\n const [expanded, setExpanded] = useState(false)\n const method = methods.find(\n (m) => m.slug === accessRequest.approvalMethodSlug,\n )\n\n const hasExtra =\n accessRequest.comments ||\n accessRequest.urls?.length ||\n accessRequest.approverPersonSlugs?.length ||\n accessRequest.postApprovalInstructions\n\n return (\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-1.5\">\n {method && getAccessIcon(method.type)}\n {method?.type === 'service' && method.config.url ? (\n <a\n href={method.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {method.displayName}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-sm\">\n {method?.displayName ?? accessRequest.approvalMethodSlug}\n </span>\n )}\n {hasExtra && (\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"text-xs text-muted-foreground hover:text-primary ml-1\"\n >\n {expanded ? 'less' : 'more...'}\n </button>\n )}\n </div>\n {expanded && (\n <div className=\"pl-5 space-y-1.5 text-xs\">\n {accessRequest.comments && (\n <div className=\"text-muted-foreground prose prose-xs max-w-none\">\n <ReactMarkdown>{accessRequest.comments}</ReactMarkdown>\n </div>\n )}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div className=\"flex flex-col gap-0.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline inline-flex items-center gap-1\"\n >\n {urlObj.label || urlObj.url.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ))}\n </div>\n )}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nexport function TierVariantsSection({ tiers }: TierVariantsSectionProps) {\n const { approvalMethods } = useAppCatalogContext()\n\n if (tiers.length === 0) return null\n\n return (\n <div className=\"space-y-2\">\n <div className=\"text-sm font-medium\">Environment Tiers</div>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-[100px]\">Tier</TableHead>\n <TableHead>Name</TableHead>\n <TableHead>URL</TableHead>\n <TableHead>Access</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tiers.map((tier) => (\n <TableRow key={tier.tierSlug}>\n <TableCell>\n <Badge\n variant={getTierBadgeVariant(tier.tierSlug)}\n className={getTierBadgeClassName(tier.tierSlug)}\n >\n {getTierDisplayLabel(tier.tierSlug)}\n </Badge>\n </TableCell>\n <TableCell className=\"font-medium\">\n {tier.displayName ?? tier.tierSlug}\n </TableCell>\n <TableCell>\n {tier.appUrl ? (\n <a\n href={tier.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {tier.appUrl.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {tier.accessRequest ? (\n <TierAccessDetail\n accessRequest={tier.accessRequest}\n methods={approvalMethods}\n />\n ) : (\n <span className=\"text-muted-foreground\">Same as app</span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TierVariantsSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/TierVariantsSection.tsx"],"sourcesContent":["import type {\n AppAccessRequest,\n AppApprovalMethod,\n TierVariant,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, ExternalLinkIcon, Settings, Users } from 'lucide-react'\nimport { useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Badge } from '~/ui/badge'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { PersonBadge } from './PersonBadge'\n\ninterface TierVariantsSectionProps {\n tiers: TierVariant[]\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 getAccessIcon(type: string): React.ReactNode {\n switch (type) {\n case 'service':\n return <Bot className=\"size-4 text-primary shrink-0\" />\n case 'personTeam':\n return <Users className=\"size-4 text-primary shrink-0\" />\n default:\n return <Settings className=\"size-4 text-primary shrink-0\" />\n }\n}\n\n/** Compact inline access detail for a tier row */\nfunction TierAccessDetail({\n accessRequest,\n methods,\n}: {\n accessRequest: AppAccessRequest\n methods: AppApprovalMethod[]\n}) {\n const [expanded, setExpanded] = useState(false)\n const method = methods.find(\n (m) => m.slug === accessRequest.approvalMethodSlug,\n )\n\n const hasExtra =\n accessRequest.comments ||\n accessRequest.urls?.length ||\n accessRequest.approverPersonSlugs?.length ||\n accessRequest.postApprovalInstructions\n\n return (\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-1.5\">\n {method && getAccessIcon(method.type)}\n {method?.type === 'service' && method.config.url ? (\n <a\n href={method.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {method.displayName}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-sm\">\n {method?.displayName ?? accessRequest.approvalMethodSlug}\n </span>\n )}\n {hasExtra && (\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"text-xs text-muted-foreground hover:text-primary ml-1\"\n >\n {expanded ? 'less' : 'more...'}\n </button>\n )}\n </div>\n {expanded && (\n <div className=\"pl-5 space-y-1.5 text-xs\">\n {accessRequest.comments && (\n <div className=\"text-muted-foreground prose prose-xs max-w-none\">\n <ReactMarkdown>{accessRequest.comments}</ReactMarkdown>\n </div>\n )}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div className=\"flex flex-col gap-0.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline inline-flex items-center gap-1\"\n >\n {urlObj.label || urlObj.url.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ))}\n </div>\n )}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nexport function TierVariantsSection({ tiers }: TierVariantsSectionProps) {\n const { approvalMethods } = useAppCatalogContext()\n\n if (tiers.length === 0) return null\n\n return (\n <div className=\"space-y-2\">\n <div className=\"text-sm font-medium\">Environment Tiers</div>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-[100px]\">Tier</TableHead>\n <TableHead>Name</TableHead>\n <TableHead>URL</TableHead>\n <TableHead>Access</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tiers.map((tier) => (\n <TableRow key={tier.tierSlug}>\n <TableCell>\n <Badge\n variant={getTierBadgeVariant(tier.tierSlug)}\n className={getTierBadgeClassName(tier.tierSlug)}\n >\n {getTierDisplayLabel(tier.tierSlug)}\n </Badge>\n </TableCell>\n <TableCell className=\"font-medium\">\n {tier.displayName ?? tier.tierSlug}\n </TableCell>\n <TableCell>\n {tier.appUrl ? (\n <a\n href={tier.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {tier.appUrl.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {tier.accessRequest ? (\n <TierAccessDetail\n accessRequest={tier.accessRequest}\n methods={approvalMethods}\n />\n ) : (\n <span className=\"text-muted-foreground\">Same as app</span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index.css
CHANGED
|
@@ -230,3 +230,13 @@
|
|
|
230
230
|
transform: scale(1);
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
+
|
|
234
|
+
/* Tailwind preflight resets list-style to none. Re-enable for markdown prose. */
|
|
235
|
+
.prose ol {
|
|
236
|
+
list-style-type: decimal;
|
|
237
|
+
padding-left: 1.25rem;
|
|
238
|
+
}
|
|
239
|
+
.prose ul {
|
|
240
|
+
list-style-type: disc;
|
|
241
|
+
padding-left: 1.25rem;
|
|
242
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -134,8 +134,8 @@
|
|
|
134
134
|
"vite-plugin-static-copy": "^3.1.4",
|
|
135
135
|
"vite-plugin-svgr": "^4.2.0",
|
|
136
136
|
"vitest": "^4.1.2",
|
|
137
|
-
"@igstack/app-catalog-backend-core": "0.13.
|
|
138
|
-
"@igstack/app-catalog-shared-core": "0.13.
|
|
137
|
+
"@igstack/app-catalog-backend-core": "0.13.3",
|
|
138
|
+
"@igstack/app-catalog-shared-core": "0.13.3"
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
141
141
|
"react": "19.1.2",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"vite": "^6.3.5",
|
|
145
145
|
"vite-plugin-svgr": "^4.2.0"
|
|
146
146
|
},
|
|
147
|
-
"gitHead": "
|
|
147
|
+
"gitHead": "5e900c4f7cda2501c01164155098fc3f46a58b5c",
|
|
148
148
|
"scripts": {
|
|
149
149
|
"build": "vite build",
|
|
150
150
|
"build:lenient": "vite build --mode lenient",
|
package/src/index.css
CHANGED
|
@@ -230,3 +230,13 @@
|
|
|
230
230
|
transform: scale(1);
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
+
|
|
234
|
+
/* Tailwind preflight resets list-style to none. Re-enable for markdown prose. */
|
|
235
|
+
.prose ol {
|
|
236
|
+
list-style-type: decimal;
|
|
237
|
+
padding-left: 1.25rem;
|
|
238
|
+
}
|
|
239
|
+
.prose ul {
|
|
240
|
+
list-style-type: disc;
|
|
241
|
+
padding-left: 1.25rem;
|
|
242
|
+
}
|
|
@@ -240,7 +240,7 @@ export function AccessRequestSection({
|
|
|
240
240
|
|
|
241
241
|
{/* Comments */}
|
|
242
242
|
{accessRequest.comments && (
|
|
243
|
-
<div className="text-sm text-muted-foreground prose prose-sm max-w-none
|
|
243
|
+
<div className="text-sm text-muted-foreground prose prose-sm max-w-none">
|
|
244
244
|
<ReactMarkdown components={{ a: MarkdownLink }}>
|
|
245
245
|
{accessRequest.comments}
|
|
246
246
|
</ReactMarkdown>
|
|
@@ -326,7 +326,7 @@ export function AccessRequestSection({
|
|
|
326
326
|
Post-Approval Instructions
|
|
327
327
|
</AccordionTrigger>
|
|
328
328
|
<AccordionContent className="pb-3">
|
|
329
|
-
<div className="text-sm text-muted-foreground prose prose-sm max-w-none
|
|
329
|
+
<div className="text-sm text-muted-foreground prose prose-sm max-w-none">
|
|
330
330
|
<ReactMarkdown components={{ a: MarkdownLink }}>
|
|
331
331
|
{accessRequest.postApprovalInstructions}
|
|
332
332
|
</ReactMarkdown>
|