@igstack/app-catalog-frontend-core 0.18.3 → 0.18.4-alpha-20260904223123
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__/modules/appCatalog/ui/components/PersonBadge.test.d.ts +0 -0
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +3 -4
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +3 -3
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.d.ts +6 -0
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +73 -3
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js +1 -1
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +7 -54
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +3 -3
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +1 -1
- package/src/__tests__/modules/appCatalog/ui/components/PersonBadge.test.tsx +51 -0
- package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +1 -1
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +5 -5
- package/src/modules/appCatalog/ui/components/PersonBadge.tsx +99 -2
- package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +4 -2
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +12 -54
- package/src/modules/appCatalog/ui/components/TierVariantsSection.tsx +6 -6
|
File without changes
|
|
@@ -30,6 +30,7 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
|
|
|
30
30
|
accessRequest?: {
|
|
31
31
|
approvalMethodSlug: string;
|
|
32
32
|
comments?: string | undefined;
|
|
33
|
+
approverSlugs?: string[] | undefined;
|
|
33
34
|
urls?: {
|
|
34
35
|
url: string;
|
|
35
36
|
label?: string | undefined;
|
|
@@ -41,7 +42,6 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
|
|
|
41
42
|
description?: string | undefined;
|
|
42
43
|
adminNotes?: string | undefined;
|
|
43
44
|
}[] | undefined;
|
|
44
|
-
approverPersonSlugs?: string[] | undefined;
|
|
45
45
|
} | undefined;
|
|
46
46
|
notes?: string | undefined;
|
|
47
47
|
tags?: string[] | undefined;
|
|
@@ -62,6 +62,7 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
|
|
|
62
62
|
accessRequest?: {
|
|
63
63
|
approvalMethodSlug: string;
|
|
64
64
|
comments?: string | undefined;
|
|
65
|
+
approverSlugs?: string[] | undefined;
|
|
65
66
|
urls?: {
|
|
66
67
|
url: string;
|
|
67
68
|
label?: string | undefined;
|
|
@@ -73,16 +74,14 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
|
|
|
73
74
|
description?: string | undefined;
|
|
74
75
|
adminNotes?: string | undefined;
|
|
75
76
|
}[] | undefined;
|
|
76
|
-
approverPersonSlugs?: string[] | undefined;
|
|
77
77
|
} | undefined;
|
|
78
78
|
appUrl?: string | undefined;
|
|
79
79
|
}[] | undefined;
|
|
80
80
|
parentSlug?: string | undefined;
|
|
81
81
|
tier?: string | undefined;
|
|
82
|
-
familySlug?: string | undefined;
|
|
83
82
|
aliases?: string[] | undefined;
|
|
84
83
|
ownerPersonSlug?: string | undefined;
|
|
85
|
-
|
|
84
|
+
approverSlugs?: string[] | undefined;
|
|
86
85
|
accessComments?: string | undefined;
|
|
87
86
|
extra?: Record<string, unknown> | undefined;
|
|
88
87
|
sources?: string[] | {
|
|
@@ -6,7 +6,7 @@ import { Button } from "../../../../ui/button.js";
|
|
|
6
6
|
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "../../../../ui/accordion.js";
|
|
7
7
|
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "../../../../ui/table.js";
|
|
8
8
|
import { MarkdownLink } from "./MarkdownText.js";
|
|
9
|
-
import {
|
|
9
|
+
import { PersonOrGroupBadge } from "./PersonBadge.js";
|
|
10
10
|
const COPY_FEEDBACK_DURATION = 2e3;
|
|
11
11
|
function getApprovalMethodIcon(type) {
|
|
12
12
|
switch (type) {
|
|
@@ -169,9 +169,9 @@ function AccessRequestSection({
|
|
|
169
169
|
] }, `${role.displayName}-${idx}`)) })
|
|
170
170
|
] }) })
|
|
171
171
|
] }),
|
|
172
|
-
accessRequest.
|
|
172
|
+
accessRequest.approverSlugs && accessRequest.approverSlugs.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
173
173
|
/* @__PURE__ */ jsx("h4", { className: "mb-2 text-sm font-medium", children: "Approvers" }),
|
|
174
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: accessRequest.
|
|
174
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: accessRequest.approverSlugs.map((slug) => /* @__PURE__ */ jsx(PersonOrGroupBadge, { slug }, slug)) })
|
|
175
175
|
] }),
|
|
176
176
|
accessRequest.urls && accessRequest.urls.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
177
177
|
/* @__PURE__ */ jsx("h4", { className: "mb-2 text-sm font-medium", children: "Documentation" }),
|
|
@@ -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 // Two-step: has a primary request action AND follow-up post-approval steps\n const isTwoStep = Boolean(\n hasWrittenSteps && accessRequest.postApprovalInstructions,\n )\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 {isTwoStep && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5\">\n Step 1\n </span>\n <span className=\"text-xs text-muted-foreground\">Request access</span>\n </div>\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 */}\n {accessRequest.postApprovalInstructions &&\n (isTwoStep ? (\n // Two-step apps: show Step 2 prominently, always expanded\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5\">\n Step 2\n </span>\n <span className=\"text-xs text-muted-foreground\">\n After approval\n </span>\n </div>\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 </div>\n ) : (\n // Single-step apps: keep collapsible accordion (secondary info)\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 { PersonOrGroupBadge } 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 // Two-step: has a primary request action AND follow-up post-approval steps\n const isTwoStep = Boolean(\n hasWrittenSteps && accessRequest.postApprovalInstructions,\n )\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 {isTwoStep && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5\">\n Step 1\n </span>\n <span className=\"text-xs text-muted-foreground\">Request access</span>\n </div>\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.approverSlugs &&\n accessRequest.approverSlugs.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.approverSlugs.map((slug) => (\n <PersonOrGroupBadge 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 */}\n {accessRequest.postApprovalInstructions &&\n (isTwoStep ? (\n // Two-step apps: show Step 2 prominently, always expanded\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-bold text-primary bg-primary/10 rounded-full px-2.5 py-0.5\">\n Step 2\n </span>\n <span className=\"text-xs text-muted-foreground\">\n After approval\n </span>\n </div>\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 </div>\n ) : (\n // Single-step apps: keep collapsible accordion (secondary info)\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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"}
|
|
@@ -8,4 +8,10 @@ interface PersonBadgeProps {
|
|
|
8
8
|
* mailto:. Falls back to just the name (no popover) when no email is known.
|
|
9
9
|
*/
|
|
10
10
|
export declare function PersonBadge({ slug }: PersonBadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
/**
|
|
12
|
+
* Chip for a slug that may name EITHER a Person or a Group (owner, approvers).
|
|
13
|
+
* Resolves against both collections; falls back to the raw slug so an unknown
|
|
14
|
+
* slug is still visible rather than silently dropped.
|
|
15
|
+
*/
|
|
16
|
+
export declare function PersonOrGroupBadge({ slug }: PersonBadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
11
17
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { User, Copy, Mail, Check } from "lucide-react";
|
|
2
|
+
import { User, Copy, Mail, Check, Users } from "lucide-react";
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
4
|
import { Badge } from "../../../../ui/badge.js";
|
|
5
5
|
import { Popover, PopoverTrigger, PopoverContent } from "../../../../ui/popover.js";
|
|
@@ -16,7 +16,7 @@ import "../../../pluginCore/PluginManagerContext.js";
|
|
|
16
16
|
import "@radix-ui/react-tooltip";
|
|
17
17
|
import "../../../../ui/empty.js";
|
|
18
18
|
import "../../../../ui/button.js";
|
|
19
|
-
import { getPersonBySlug } from "../../utils/resolveHelpers.js";
|
|
19
|
+
import { getPersonBySlug, getGroupBySlug } from "../../utils/resolveHelpers.js";
|
|
20
20
|
function PersonBadge({ slug }) {
|
|
21
21
|
const { persons } = useAppCatalogContext();
|
|
22
22
|
const person = getPersonBySlug(persons, slug);
|
|
@@ -78,6 +78,75 @@ function PersonBadge({ slug }) {
|
|
|
78
78
|
] })
|
|
79
79
|
] });
|
|
80
80
|
}
|
|
81
|
+
function PersonOrGroupBadge({ slug }) {
|
|
82
|
+
const { persons, groups } = useAppCatalogContext();
|
|
83
|
+
if (getPersonBySlug(persons, slug)) return /* @__PURE__ */ jsx(PersonBadge, { slug });
|
|
84
|
+
const group = getGroupBySlug(groups, slug);
|
|
85
|
+
if (!group) {
|
|
86
|
+
return /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "font-normal", title: slug, children: slug });
|
|
87
|
+
}
|
|
88
|
+
return /* @__PURE__ */ jsx(GroupBadge, { group });
|
|
89
|
+
}
|
|
90
|
+
function GroupBadge({ group }) {
|
|
91
|
+
const { persons } = useAppCatalogContext();
|
|
92
|
+
const memberNames = group.memberSlugs.map((s) => {
|
|
93
|
+
const p = getPersonBySlug(persons, s);
|
|
94
|
+
return p ? `${p.firstName} ${p.lastName}`.trim() || s : s;
|
|
95
|
+
});
|
|
96
|
+
const hasRealDisplayName = group.displayName && group.displayName !== group.slug;
|
|
97
|
+
const displayName = hasRealDisplayName ? group.displayName : memberNames.length > 0 ? memberNames.join(", ") : group.slug;
|
|
98
|
+
if (!group.email && memberNames.length === 0) {
|
|
99
|
+
return /* @__PURE__ */ jsxs(
|
|
100
|
+
Badge,
|
|
101
|
+
{
|
|
102
|
+
variant: "outline",
|
|
103
|
+
className: "font-normal inline-flex items-center gap-1",
|
|
104
|
+
title: displayName,
|
|
105
|
+
children: [
|
|
106
|
+
/* @__PURE__ */ jsx(Users, { className: "size-3" }),
|
|
107
|
+
displayName
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return /* @__PURE__ */ jsxs(Popover, { children: [
|
|
113
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", className: "inline-flex", title: displayName, children: /* @__PURE__ */ jsxs(
|
|
114
|
+
Badge,
|
|
115
|
+
{
|
|
116
|
+
variant: "outline",
|
|
117
|
+
className: "font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors",
|
|
118
|
+
children: [
|
|
119
|
+
/* @__PURE__ */ jsx(Users, { className: "size-3" }),
|
|
120
|
+
displayName,
|
|
121
|
+
/* @__PURE__ */ jsx(Copy, { className: "size-3 opacity-50" })
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
) }) }),
|
|
125
|
+
/* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-64 p-2", children: [
|
|
126
|
+
/* @__PURE__ */ jsx(
|
|
127
|
+
CopyRow,
|
|
128
|
+
{
|
|
129
|
+
icon: /* @__PURE__ */ jsx(Users, { className: "size-3.5" }),
|
|
130
|
+
label: "Group",
|
|
131
|
+
value: displayName
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
group.email && /* @__PURE__ */ jsx(
|
|
135
|
+
CopyRow,
|
|
136
|
+
{
|
|
137
|
+
icon: /* @__PURE__ */ jsx(Mail, { className: "size-3.5" }),
|
|
138
|
+
label: "Email",
|
|
139
|
+
value: group.email,
|
|
140
|
+
href: `mailto:${group.email}`
|
|
141
|
+
}
|
|
142
|
+
),
|
|
143
|
+
memberNames.length > 0 && /* @__PURE__ */ jsxs("div", { className: "px-2 py-1.5", children: [
|
|
144
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[10px] uppercase tracking-wide text-muted-foreground", children: "Members" }),
|
|
145
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[13px]", children: memberNames.join(", ") })
|
|
146
|
+
] })
|
|
147
|
+
] })
|
|
148
|
+
] });
|
|
149
|
+
}
|
|
81
150
|
function CopyRow({
|
|
82
151
|
icon,
|
|
83
152
|
label,
|
|
@@ -129,6 +198,7 @@ function CopyRow({
|
|
|
129
198
|
] });
|
|
130
199
|
}
|
|
131
200
|
export {
|
|
132
|
-
PersonBadge
|
|
201
|
+
PersonBadge,
|
|
202
|
+
PersonOrGroupBadge
|
|
133
203
|
};
|
|
134
204
|
//# sourceMappingURL=PersonBadge.js.map
|
|
@@ -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":"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"}
|
|
1
|
+
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import type { Group } from '@igstack/app-catalog-backend-core'\nimport { Check, Copy, Mail, User, Users } 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 {\n getGroupBySlug,\n getPersonBySlug,\n} 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/**\n * Chip for a slug that may name EITHER a Person or a Group (owner, approvers).\n * Resolves against both collections; falls back to the raw slug so an unknown\n * slug is still visible rather than silently dropped.\n */\nexport function PersonOrGroupBadge({ slug }: PersonBadgeProps) {\n const { persons, groups } = useAppCatalogContext()\n\n if (getPersonBySlug(persons, slug)) return <PersonBadge slug={slug} />\n\n const group = getGroupBySlug(groups, slug)\n if (!group) {\n return (\n <Badge variant=\"outline\" className=\"font-normal\" title={slug}>\n {slug}\n </Badge>\n )\n }\n return <GroupBadge group={group} />\n}\n\n/** Group chip: name, optional email, and the member names in a popover. */\nfunction GroupBadge({ group }: { group: Group }) {\n const { persons } = useAppCatalogContext()\n const memberNames = group.memberSlugs.map((s) => {\n const p = getPersonBySlug(persons, s)\n return p ? `${p.firstName} ${p.lastName}`.trim() || s : s\n })\n // Many groups (e.g. aws-maintainers-*) have no real displayName set — it's\n // just their slug. Show the member names directly instead of the opaque\n // slug so approvers stay recognizable at a glance, without a popover click.\n const hasRealDisplayName =\n group.displayName && group.displayName !== group.slug\n const displayName = hasRealDisplayName\n ? group.displayName!\n : memberNames.length > 0\n ? memberNames.join(', ')\n : group.slug\n\n if (!group.email && memberNames.length === 0) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <Users className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button type=\"button\" className=\"inline-flex\" title={displayName}>\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <Users 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={<Users className=\"size-3.5\" />}\n label=\"Group\"\n value={displayName}\n />\n {group.email && (\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={group.email}\n href={`mailto:${group.email}`}\n />\n )}\n {memberNames.length > 0 && (\n <div className=\"px-2 py-1.5\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n Members\n </span>\n <span className=\"block text-[13px]\">{memberNames.join(', ')}</span>\n </div>\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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"}
|
|
@@ -14,7 +14,7 @@ function ScreenshotGallery({
|
|
|
14
14
|
const isFullscreenRef = useRef(false);
|
|
15
15
|
const images = useMemo(
|
|
16
16
|
() => screenshotIds.map((id) => ({
|
|
17
|
-
url: `/api/screenshots/${id}`,
|
|
17
|
+
url: `/api/screenshots/${id}?size=2000`,
|
|
18
18
|
alt: `${app.abbreviation || app.displayName} screenshot`
|
|
19
19
|
})),
|
|
20
20
|
[screenshotIds, app.abbreviation, app.displayName]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScreenshotGallery.js","sources":["../../../../../../src/modules/appCatalog/ui/components/ScreenshotGallery.tsx"],"sourcesContent":["import { useMemo, useRef } from 'react'\n\nimport type { Resource } from '@igstack/app-catalog-backend-core'\n\nimport { Gallery } from '~/modules/gallery/Gallery'\nimport type { GalleryImage } from '~/modules/gallery/Gallery'\nimport { Dialog, DialogContent, DialogTitle } from '~/ui/dialog'\nimport { VisuallyHidden } from '~/ui/visually-hidden'\n\nexport interface ScreenshotGalleryProps {\n app: Resource\n screenshotIds: string[]\n initialIndex?: number\n open: boolean\n onOpenChange: (open: boolean) => void\n title?: string\n}\n\nexport function ScreenshotGallery({\n app,\n screenshotIds,\n initialIndex = 0,\n open,\n onOpenChange,\n title,\n}: ScreenshotGalleryProps) {\n // Track whether Gallery is in fullscreen — if so, block Radix from closing on Escape\n const isFullscreenRef = useRef(false)\n\n // Transform screenshot IDs to
|
|
1
|
+
{"version":3,"file":"ScreenshotGallery.js","sources":["../../../../../../src/modules/appCatalog/ui/components/ScreenshotGallery.tsx"],"sourcesContent":["import { useMemo, useRef } from 'react'\n\nimport type { Resource } from '@igstack/app-catalog-backend-core'\n\nimport { Gallery } from '~/modules/gallery/Gallery'\nimport type { GalleryImage } from '~/modules/gallery/Gallery'\nimport { Dialog, DialogContent, DialogTitle } from '~/ui/dialog'\nimport { VisuallyHidden } from '~/ui/visually-hidden'\n\nexport interface ScreenshotGalleryProps {\n app: Resource\n screenshotIds: string[]\n initialIndex?: number\n open: boolean\n onOpenChange: (open: boolean) => void\n title?: string\n}\n\nexport function ScreenshotGallery({\n app,\n screenshotIds,\n initialIndex = 0,\n open,\n onOpenChange,\n title,\n}: ScreenshotGalleryProps) {\n // Track whether Gallery is in fullscreen — if so, block Radix from closing on Escape\n const isFullscreenRef = useRef(false)\n\n // Transform screenshot IDs to URLs. Capped at 2000px — plenty for on-screen\n // viewing (incl. fullscreen zoom) while avoiding multi-MB original downloads\n // for every slide in the carousel (all slides mount at once, unlazy).\n const images: GalleryImage[] = useMemo(\n () =>\n screenshotIds.map((id) => ({\n url: `/api/screenshots/${id}?size=2000`,\n alt: `${app.abbreviation || app.displayName} screenshot`,\n })),\n [screenshotIds, app.abbreviation, app.displayName],\n )\n\n // Don't render if no screenshots\n if (screenshotIds.length === 0) {\n return null\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n aria-describedby={undefined}\n className=\"h-[85vh] w-full max-w-[calc(100vw-2rem)] sm:max-w-[calc(100vw-3rem)] md:max-w-[calc(100vw-4rem)] p-0 overflow-hidden\"\n showCloseButton={true}\n onEscapeKeyDown={(e) => {\n // Stop propagation so the Esc event doesn't reach outer document listeners\n // (e.g. SearchResultsList's raw keydown handler, which would clear search).\n e.stopPropagation()\n // If Gallery is in fullscreen, its capture listener already handled Escape.\n // Prevent Radix from also closing the dialog on the same event.\n if (isFullscreenRef.current) {\n e.preventDefault()\n }\n }}\n >\n <VisuallyHidden>\n <DialogTitle>\n {title || `${app.abbreviation || app.displayName} screenshots`}\n </DialogTitle>\n </VisuallyHidden>\n <Gallery\n images={images}\n initialIndex={initialIndex}\n title={title}\n onFullscreenChange={(fs) => {\n isFullscreenRef.current = fs\n }}\n />\n </DialogContent>\n </Dialog>\n )\n}\n"],"names":[],"mappings":";;;;;AAkBO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AAEzB,QAAM,kBAAkB,OAAO,KAAK;AAKpC,QAAM,SAAyB;AAAA,IAC7B,MACE,cAAc,IAAI,CAAC,QAAQ;AAAA,MACzB,KAAK,oBAAoB,EAAE;AAAA,MAC3B,KAAK,GAAG,IAAI,gBAAgB,IAAI,WAAW;AAAA,IAAA,EAC3C;AAAA,IACJ,CAAC,eAAe,IAAI,cAAc,IAAI,WAAW;AAAA,EAAA;AAInD,MAAI,cAAc,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AAEA,SACE,oBAAC,QAAA,EAAO,MAAY,cAClB,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,oBAAkB;AAAA,MAClB,WAAU;AAAA,MACV,iBAAiB;AAAA,MACjB,iBAAiB,CAAC,MAAM;AAGtB,UAAE,gBAAA;AAGF,YAAI,gBAAgB,SAAS;AAC3B,YAAE,eAAA;AAAA,QACJ;AAAA,MACF;AAAA,MAEA,UAAA;AAAA,QAAA,oBAAC,gBAAA,EACC,UAAA,oBAAC,aAAA,EACE,UAAA,SAAS,GAAG,IAAI,gBAAgB,IAAI,WAAW,eAAA,CAClD,GACF;AAAA,QACA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA,oBAAoB,CAAC,OAAO;AAC1B,8BAAgB,UAAU;AAAA,YAC5B;AAAA,UAAA;AAAA,QAAA;AAAA,MACF;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;"}
|
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Search
|
|
3
|
-
import { useMemo, useState
|
|
2
|
+
import { Search } from "lucide-react";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
4
|
import { Badge } from "../../../../ui/badge.js";
|
|
5
5
|
import { Input } from "../../../../ui/input.js";
|
|
6
6
|
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "../../../../ui/table.js";
|
|
7
|
-
import { PersonBadge } from "./PersonBadge.js";
|
|
7
|
+
import { PersonBadge, PersonOrGroupBadge } from "./PersonBadge.js";
|
|
8
8
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../../../../ui/select.js";
|
|
9
|
-
import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
10
|
-
import "../context/AppCatalogFiltersContext.js";
|
|
11
|
-
import "@tanstack/react-router";
|
|
12
|
-
import "clsx";
|
|
13
|
-
import "tailwind-merge";
|
|
14
|
-
import "@tanstack/react-query-devtools";
|
|
15
|
-
import "next-themes";
|
|
16
|
-
import "@radix-ui/react-dialog";
|
|
17
|
-
import "../../../config/GlobalConfigContext.js";
|
|
18
|
-
import "../../../pluginCore/PluginManagerContext.js";
|
|
19
|
-
import "@radix-ui/react-tooltip";
|
|
20
|
-
import "../../../../ui/empty.js";
|
|
21
|
-
import "../../../../ui/button.js";
|
|
22
|
-
import { getGroupBySlug } from "../../utils/resolveHelpers.js";
|
|
23
9
|
import { markdownToPlainText } from "../../utils/markdownToPlainText.js";
|
|
24
10
|
function getTierBadgeVariant(tierSlug) {
|
|
25
11
|
if (tierSlug === "prod" || tierSlug === "production") return "destructive";
|
|
@@ -43,39 +29,10 @@ function getTierDisplayLabel(tierSlug) {
|
|
|
43
29
|
if (tierSlug === "staging") return "Staging";
|
|
44
30
|
return tierSlug;
|
|
45
31
|
}
|
|
46
|
-
function CopyAccountIdButton({ accountId }) {
|
|
47
|
-
const [copied, setCopied] = useState(false);
|
|
48
|
-
const timeoutRef = useRef(null);
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
return () => {
|
|
51
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
52
|
-
};
|
|
53
|
-
}, []);
|
|
54
|
-
const handleCopy = useCallback(() => {
|
|
55
|
-
void navigator.clipboard.writeText(accountId).then(() => {
|
|
56
|
-
setCopied(true);
|
|
57
|
-
timeoutRef.current = setTimeout(() => setCopied(false), 1500);
|
|
58
|
-
});
|
|
59
|
-
}, [accountId]);
|
|
60
|
-
return /* @__PURE__ */ jsxs(
|
|
61
|
-
"button",
|
|
62
|
-
{
|
|
63
|
-
type: "button",
|
|
64
|
-
onClick: handleCopy,
|
|
65
|
-
className: "flex items-center gap-1 font-mono text-xs text-muted-foreground hover:text-foreground transition-colors group",
|
|
66
|
-
title: "Copy account ID",
|
|
67
|
-
children: [
|
|
68
|
-
/* @__PURE__ */ jsx("span", { children: accountId }),
|
|
69
|
-
copied ? /* @__PURE__ */ jsx(Check, { className: "size-3 text-green-500 shrink-0" }) : /* @__PURE__ */ jsx(Copy, { className: "size-3 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" })
|
|
70
|
-
]
|
|
71
|
-
}
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
32
|
function SubResourcesSection({
|
|
75
33
|
subResources,
|
|
76
34
|
initialSearch
|
|
77
35
|
}) {
|
|
78
|
-
const { groups } = useAppCatalogContext();
|
|
79
36
|
const seededSearch = useMemo(() => {
|
|
80
37
|
const q = initialSearch == null ? void 0 : initialSearch.trim().toLowerCase();
|
|
81
38
|
if (!q) return "";
|
|
@@ -144,7 +101,7 @@ function SubResourcesSection({
|
|
|
144
101
|
/* @__PURE__ */ jsx(TableHead, { children: "Name" }),
|
|
145
102
|
/* @__PURE__ */ jsx(TableHead, { className: "w-[80px]", children: "Tier" }),
|
|
146
103
|
/* @__PURE__ */ jsx(TableHead, { children: "Owner" }),
|
|
147
|
-
/* @__PURE__ */ jsx(TableHead, { children: "
|
|
104
|
+
/* @__PURE__ */ jsx(TableHead, { children: "Approvers" }),
|
|
148
105
|
/* @__PURE__ */ jsx(TableHead, { className: "w-[140px]", children: "AWS Account" })
|
|
149
106
|
] }) }),
|
|
150
107
|
/* @__PURE__ */ jsx(TableBody, { children: filtered.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
@@ -155,11 +112,7 @@ function SubResourcesSection({
|
|
|
155
112
|
children: "No resources match your filters"
|
|
156
113
|
}
|
|
157
114
|
) }) : filtered.map((sr) => {
|
|
158
|
-
const
|
|
159
|
-
const group = getGroupBySlug(groups, groupSlug);
|
|
160
|
-
return (group == null ? void 0 : group.memberSlugs) ?? [];
|
|
161
|
-
});
|
|
162
|
-
const uniqueMaintainers = [...new Set(maintainerMembers)];
|
|
115
|
+
const approvers = [...new Set(sr.approverSlugs ?? [])];
|
|
163
116
|
return /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
164
117
|
/* @__PURE__ */ jsxs(TableCell, { children: [
|
|
165
118
|
/* @__PURE__ */ jsx("div", { className: "font-medium text-sm", children: sr.displayName }),
|
|
@@ -175,11 +128,11 @@ function SubResourcesSection({
|
|
|
175
128
|
}
|
|
176
129
|
) }),
|
|
177
130
|
/* @__PURE__ */ jsx(TableCell, { children: sr.ownerPersonSlug && /* @__PURE__ */ jsx(PersonBadge, { slug: sr.ownerPersonSlug }) }),
|
|
178
|
-
/* @__PURE__ */ jsx(TableCell, { children:
|
|
131
|
+
/* @__PURE__ */ jsx(TableCell, { children: approvers.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: approvers.map((slug) => /* @__PURE__ */ jsx(PersonOrGroupBadge, { slug }, slug)) }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "-" }) }),
|
|
179
132
|
/* @__PURE__ */ jsx(TableCell, { children: (() => {
|
|
180
133
|
var _a;
|
|
181
134
|
const accountId = (_a = sr.extra) == null ? void 0 : _a.awsAccountId;
|
|
182
|
-
return accountId ? /* @__PURE__ */ jsx(
|
|
135
|
+
return accountId ? /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-muted-foreground select-text", children: accountId }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" });
|
|
183
136
|
})() })
|
|
184
137
|
] }, sr.slug);
|
|
185
138
|
}) })
|
|
@@ -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 \"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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"}
|
|
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 { Search } from 'lucide-react'\nimport { useMemo, 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, PersonOrGroupBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\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\nexport function SubResourcesSection({\n subResources,\n initialSearch,\n}: SubResourcesSectionProps) {\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>Approvers</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 // Approvers are Person OR Group slugs; the badge resolves both.\n const approvers = [...new Set(sr.approverSlugs ?? [])]\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 {approvers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {approvers.map((slug) => (\n <PersonOrGroupBadge key={slug} slug={slug} />\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 <span className=\"font-mono text-xs text-muted-foreground select-text\">\n {accountId}\n </span>\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":";;;;;;;;;AAmCA,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;AAEO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AACF,GAA6B;AAG3B,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,YAAA,CAAS;AAAA,QACpB,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,YAAY,CAAC,GAAG,IAAI,IAAI,GAAG,iBAAiB,CAAA,CAAE,CAAC;AAErD,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,UAAU,SAAS,wBACjB,OAAA,EAAI,WAAU,wBACZ,UAAA,UAAU,IAAI,CAAC,SACd,oBAAC,oBAAA,EAA8B,KAAA,GAAN,IAAkB,CAC5C,EAAA,CACH,IAEA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,IAAA,CAAC,EAAA,CAE7C;AAAA,UACA,oBAAC,aACG,WAAA,MAAM;;AACN,kBAAM,aACJ,QAAG,UAAH,mBACC;AACH,mBAAO,YACL,oBAAC,QAAA,EAAK,WAAU,uDACb,UAAA,UAAA,CACH,IAEA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,KAAC;AAAA,UAE7C,KAAG,CACL;AAAA,QAAA,EAAA,GAvDa,GAAG,IAwDlB;AAAA,MAEJ,CAAC,EAAA,CAEL;AAAA,IAAA,EAAA,CACF,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
|
|
@@ -17,7 +17,7 @@ import "../../../pluginCore/PluginManagerContext.js";
|
|
|
17
17
|
import "@radix-ui/react-tooltip";
|
|
18
18
|
import "../../../../ui/empty.js";
|
|
19
19
|
import "../../../../ui/button.js";
|
|
20
|
-
import {
|
|
20
|
+
import { PersonOrGroupBadge } from "./PersonBadge.js";
|
|
21
21
|
function getTierBadgeVariant(tierSlug) {
|
|
22
22
|
if (tierSlug === "prod" || tierSlug === "production") return "destructive";
|
|
23
23
|
if (tierSlug === "dev" || tierSlug === "staging") return "secondary";
|
|
@@ -59,7 +59,7 @@ function TierAccessDetail({
|
|
|
59
59
|
const method = methods.find(
|
|
60
60
|
(m) => m.slug === accessRequest.approvalMethodSlug
|
|
61
61
|
);
|
|
62
|
-
const hasExtra = accessRequest.comments || ((_a = accessRequest.urls) == null ? void 0 : _a.length) || ((_b = accessRequest.
|
|
62
|
+
const hasExtra = accessRequest.comments || ((_a = accessRequest.urls) == null ? void 0 : _a.length) || ((_b = accessRequest.approverSlugs) == null ? void 0 : _b.length) || accessRequest.postApprovalInstructions;
|
|
63
63
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
64
64
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
65
65
|
method && getAccessIcon(method.type),
|
|
@@ -102,7 +102,7 @@ function TierAccessDetail({
|
|
|
102
102
|
},
|
|
103
103
|
`${urlObj.url}-${idx}`
|
|
104
104
|
)) }),
|
|
105
|
-
accessRequest.
|
|
105
|
+
accessRequest.approverSlugs && accessRequest.approverSlugs.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: accessRequest.approverSlugs.map((slug) => /* @__PURE__ */ jsx(PersonOrGroupBadge, { slug }, slug)) })
|
|
106
106
|
] })
|
|
107
107
|
] });
|
|
108
108
|
}
|
|
@@ -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 { PersonOrGroupBadge } 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.approverSlugs?.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.approverSlugs &&\n accessRequest.approverSlugs.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {accessRequest.approverSlugs.map((slug) => (\n <PersonOrGroupBadge 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4-alpha-20260904223123",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -132,8 +132,8 @@
|
|
|
132
132
|
"vite-plugin-static-copy": "^3.1.4",
|
|
133
133
|
"vite-plugin-svgr": "^4.2.0",
|
|
134
134
|
"vitest": "^4.1.2",
|
|
135
|
-
"@igstack/app-catalog-backend-core": "0.18.
|
|
136
|
-
"@igstack/app-catalog-shared-core": "0.18.
|
|
135
|
+
"@igstack/app-catalog-backend-core": "0.18.4-alpha-20260904223123",
|
|
136
|
+
"@igstack/app-catalog-shared-core": "0.18.4-alpha-20260904223123"
|
|
137
137
|
},
|
|
138
138
|
"peerDependencies": {
|
|
139
139
|
"react": "19.1.2",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"vite": "^6.3.5",
|
|
143
143
|
"vite-plugin-svgr": "^4.2.0"
|
|
144
144
|
},
|
|
145
|
-
"gitHead": "
|
|
145
|
+
"gitHead": "bc574ef865191058505a76cb803a3844da912b80",
|
|
146
146
|
"scripts": {
|
|
147
147
|
"build": "vite build",
|
|
148
148
|
"build:lenient": "vite build --mode lenient",
|
|
@@ -75,7 +75,7 @@ export class MockBackendConfigurer {
|
|
|
75
75
|
slug: overrides.slug ?? `sr-${id}`,
|
|
76
76
|
displayName: overrides.displayName ?? `Sub Resource ${counter}`,
|
|
77
77
|
aliases: overrides.aliases ?? [],
|
|
78
|
-
|
|
78
|
+
approverSlugs: overrides.approverSlugs ?? [],
|
|
79
79
|
parentSlug: appSlug,
|
|
80
80
|
tier: overrides.tier,
|
|
81
81
|
ownerPersonSlug: overrides.ownerPersonSlug,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Group, Person } from '@igstack/app-catalog-backend-core'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import '@testing-library/jest-dom/vitest'
|
|
5
|
+
|
|
6
|
+
import { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'
|
|
7
|
+
import type { AppCatalogContextIface } from '~/modules/appCatalog/context/AppCatalogContext'
|
|
8
|
+
import { PersonOrGroupBadge } from '~/modules/appCatalog/ui/components/PersonBadge'
|
|
9
|
+
|
|
10
|
+
const persons: Person[] = [
|
|
11
|
+
{ slug: 'jdoe@example.com', firstName: 'Jane', lastName: 'Doe' },
|
|
12
|
+
]
|
|
13
|
+
const groups: Group[] = [
|
|
14
|
+
{ slug: 'cloud-team', displayName: 'Cloud Team', memberSlugs: [] },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function renderBadge(slug: string) {
|
|
18
|
+
const ctx = {
|
|
19
|
+
resources: [],
|
|
20
|
+
isLoadingApps: false,
|
|
21
|
+
tagsDefinitions: [],
|
|
22
|
+
approvalMethods: [],
|
|
23
|
+
persons,
|
|
24
|
+
groups,
|
|
25
|
+
} satisfies AppCatalogContextIface
|
|
26
|
+
|
|
27
|
+
render(
|
|
28
|
+
<AppCatalogContext value={ctx}>
|
|
29
|
+
<PersonOrGroupBadge slug={slug} />
|
|
30
|
+
</AppCatalogContext>,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// An approver slug names EITHER a person or a group, and an unresolvable one
|
|
35
|
+
// must stay visible rather than being silently dropped.
|
|
36
|
+
describe('PersonOrGroupBadge — resolving an approver slug', () => {
|
|
37
|
+
it('renders a person by name', () => {
|
|
38
|
+
renderBadge('jdoe@example.com')
|
|
39
|
+
expect(screen.getByText('Jane Doe')).toBeInTheDocument()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders a group by display name', () => {
|
|
43
|
+
renderBadge('cloud-team')
|
|
44
|
+
expect(screen.getByText('Cloud Team')).toBeInTheDocument()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('renders the raw slug when the slug matches neither', () => {
|
|
48
|
+
renderBadge('nobody-knows-me')
|
|
49
|
+
expect(screen.getByText('nobody-knows-me')).toBeInTheDocument()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
TableRow,
|
|
22
22
|
} from '~/ui/table'
|
|
23
23
|
import { MarkdownLink } from './MarkdownText'
|
|
24
|
-
import {
|
|
24
|
+
import { PersonOrGroupBadge } from './PersonBadge'
|
|
25
25
|
|
|
26
26
|
// Constants
|
|
27
27
|
const COPY_FEEDBACK_DURATION = 2000
|
|
@@ -295,13 +295,13 @@ export function AccessRequestSection({
|
|
|
295
295
|
)}
|
|
296
296
|
|
|
297
297
|
{/* Approvers */}
|
|
298
|
-
{accessRequest.
|
|
299
|
-
accessRequest.
|
|
298
|
+
{accessRequest.approverSlugs &&
|
|
299
|
+
accessRequest.approverSlugs.length > 0 && (
|
|
300
300
|
<div>
|
|
301
301
|
<h4 className="mb-2 text-sm font-medium">Approvers</h4>
|
|
302
302
|
<div className="flex flex-wrap gap-2">
|
|
303
|
-
{accessRequest.
|
|
304
|
-
<
|
|
303
|
+
{accessRequest.approverSlugs.map((slug) => (
|
|
304
|
+
<PersonOrGroupBadge key={slug} slug={slug} />
|
|
305
305
|
))}
|
|
306
306
|
</div>
|
|
307
307
|
</div>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Group } from '@igstack/app-catalog-backend-core'
|
|
2
|
+
import { Check, Copy, Mail, User, Users } from 'lucide-react'
|
|
2
3
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
4
|
import { Badge } from '~/ui/badge'
|
|
4
5
|
import { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'
|
|
5
6
|
import { useAppCatalogContext } from '~/modules/appCatalog'
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getGroupBySlug,
|
|
9
|
+
getPersonBySlug,
|
|
10
|
+
} from '~/modules/appCatalog/utils/resolveHelpers'
|
|
7
11
|
|
|
8
12
|
interface PersonBadgeProps {
|
|
9
13
|
slug: string
|
|
@@ -73,6 +77,99 @@ export function PersonBadge({ slug }: PersonBadgeProps) {
|
|
|
73
77
|
)
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Chip for a slug that may name EITHER a Person or a Group (owner, approvers).
|
|
82
|
+
* Resolves against both collections; falls back to the raw slug so an unknown
|
|
83
|
+
* slug is still visible rather than silently dropped.
|
|
84
|
+
*/
|
|
85
|
+
export function PersonOrGroupBadge({ slug }: PersonBadgeProps) {
|
|
86
|
+
const { persons, groups } = useAppCatalogContext()
|
|
87
|
+
|
|
88
|
+
if (getPersonBySlug(persons, slug)) return <PersonBadge slug={slug} />
|
|
89
|
+
|
|
90
|
+
const group = getGroupBySlug(groups, slug)
|
|
91
|
+
if (!group) {
|
|
92
|
+
return (
|
|
93
|
+
<Badge variant="outline" className="font-normal" title={slug}>
|
|
94
|
+
{slug}
|
|
95
|
+
</Badge>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
return <GroupBadge group={group} />
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Group chip: name, optional email, and the member names in a popover. */
|
|
102
|
+
function GroupBadge({ group }: { group: Group }) {
|
|
103
|
+
const { persons } = useAppCatalogContext()
|
|
104
|
+
const memberNames = group.memberSlugs.map((s) => {
|
|
105
|
+
const p = getPersonBySlug(persons, s)
|
|
106
|
+
return p ? `${p.firstName} ${p.lastName}`.trim() || s : s
|
|
107
|
+
})
|
|
108
|
+
// Many groups (e.g. aws-maintainers-*) have no real displayName set — it's
|
|
109
|
+
// just their slug. Show the member names directly instead of the opaque
|
|
110
|
+
// slug so approvers stay recognizable at a glance, without a popover click.
|
|
111
|
+
const hasRealDisplayName =
|
|
112
|
+
group.displayName && group.displayName !== group.slug
|
|
113
|
+
const displayName = hasRealDisplayName
|
|
114
|
+
? group.displayName!
|
|
115
|
+
: memberNames.length > 0
|
|
116
|
+
? memberNames.join(', ')
|
|
117
|
+
: group.slug
|
|
118
|
+
|
|
119
|
+
if (!group.email && memberNames.length === 0) {
|
|
120
|
+
return (
|
|
121
|
+
<Badge
|
|
122
|
+
variant="outline"
|
|
123
|
+
className="font-normal inline-flex items-center gap-1"
|
|
124
|
+
title={displayName}
|
|
125
|
+
>
|
|
126
|
+
<Users className="size-3" />
|
|
127
|
+
{displayName}
|
|
128
|
+
</Badge>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Popover>
|
|
134
|
+
<PopoverTrigger asChild>
|
|
135
|
+
<button type="button" className="inline-flex" title={displayName}>
|
|
136
|
+
<Badge
|
|
137
|
+
variant="outline"
|
|
138
|
+
className="font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors"
|
|
139
|
+
>
|
|
140
|
+
<Users className="size-3" />
|
|
141
|
+
{displayName}
|
|
142
|
+
<Copy className="size-3 opacity-50" />
|
|
143
|
+
</Badge>
|
|
144
|
+
</button>
|
|
145
|
+
</PopoverTrigger>
|
|
146
|
+
<PopoverContent align="start" className="w-64 p-2">
|
|
147
|
+
<CopyRow
|
|
148
|
+
icon={<Users className="size-3.5" />}
|
|
149
|
+
label="Group"
|
|
150
|
+
value={displayName}
|
|
151
|
+
/>
|
|
152
|
+
{group.email && (
|
|
153
|
+
<CopyRow
|
|
154
|
+
icon={<Mail className="size-3.5" />}
|
|
155
|
+
label="Email"
|
|
156
|
+
value={group.email}
|
|
157
|
+
href={`mailto:${group.email}`}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
{memberNames.length > 0 && (
|
|
161
|
+
<div className="px-2 py-1.5">
|
|
162
|
+
<span className="block text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
163
|
+
Members
|
|
164
|
+
</span>
|
|
165
|
+
<span className="block text-[13px]">{memberNames.join(', ')}</span>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</PopoverContent>
|
|
169
|
+
</Popover>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
76
173
|
/** One copyable field (label + value + copy button), value optionally a link. */
|
|
77
174
|
function CopyRow({
|
|
78
175
|
icon,
|
|
@@ -27,11 +27,13 @@ export function ScreenshotGallery({
|
|
|
27
27
|
// Track whether Gallery is in fullscreen — if so, block Radix from closing on Escape
|
|
28
28
|
const isFullscreenRef = useRef(false)
|
|
29
29
|
|
|
30
|
-
// Transform screenshot IDs to
|
|
30
|
+
// Transform screenshot IDs to URLs. Capped at 2000px — plenty for on-screen
|
|
31
|
+
// viewing (incl. fullscreen zoom) while avoiding multi-MB original downloads
|
|
32
|
+
// for every slide in the carousel (all slides mount at once, unlazy).
|
|
31
33
|
const images: GalleryImage[] = useMemo(
|
|
32
34
|
() =>
|
|
33
35
|
screenshotIds.map((id) => ({
|
|
34
|
-
url: `/api/screenshots/${id}`,
|
|
36
|
+
url: `/api/screenshots/${id}?size=2000`,
|
|
35
37
|
alt: `${app.abbreviation || app.displayName} screenshot`,
|
|
36
38
|
})),
|
|
37
39
|
[screenshotIds, app.abbreviation, app.displayName],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Search } from 'lucide-react'
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
4
|
import { Badge } from '~/ui/badge'
|
|
5
5
|
import { Input } from '~/ui/input'
|
|
6
6
|
import {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
TableHeader,
|
|
12
12
|
TableRow,
|
|
13
13
|
} from '~/ui/table'
|
|
14
|
-
import { PersonBadge } from './PersonBadge'
|
|
14
|
+
import { PersonBadge, PersonOrGroupBadge } from './PersonBadge'
|
|
15
15
|
import {
|
|
16
16
|
Select,
|
|
17
17
|
SelectContent,
|
|
@@ -19,8 +19,6 @@ import {
|
|
|
19
19
|
SelectTrigger,
|
|
20
20
|
SelectValue,
|
|
21
21
|
} from '~/ui/select'
|
|
22
|
-
import { useAppCatalogContext } from '~/modules/appCatalog'
|
|
23
|
-
import { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'
|
|
24
22
|
import { markdownToPlainText } from '~/modules/appCatalog/utils/markdownToPlainText'
|
|
25
23
|
|
|
26
24
|
interface SubResourcesSectionProps {
|
|
@@ -62,45 +60,10 @@ function getTierDisplayLabel(tierSlug: string): string {
|
|
|
62
60
|
return tierSlug
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
function CopyAccountIdButton({ accountId }: { accountId: string }) {
|
|
66
|
-
const [copied, setCopied] = useState(false)
|
|
67
|
-
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
68
|
-
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
return () => {
|
|
71
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
72
|
-
}
|
|
73
|
-
}, [])
|
|
74
|
-
|
|
75
|
-
const handleCopy = useCallback(() => {
|
|
76
|
-
void navigator.clipboard.writeText(accountId).then(() => {
|
|
77
|
-
setCopied(true)
|
|
78
|
-
timeoutRef.current = setTimeout(() => setCopied(false), 1500)
|
|
79
|
-
})
|
|
80
|
-
}, [accountId])
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<button
|
|
84
|
-
type="button"
|
|
85
|
-
onClick={handleCopy}
|
|
86
|
-
className="flex items-center gap-1 font-mono text-xs text-muted-foreground hover:text-foreground transition-colors group"
|
|
87
|
-
title="Copy account ID"
|
|
88
|
-
>
|
|
89
|
-
<span>{accountId}</span>
|
|
90
|
-
{copied ? (
|
|
91
|
-
<Check className="size-3 text-green-500 shrink-0" />
|
|
92
|
-
) : (
|
|
93
|
-
<Copy className="size-3 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
94
|
-
)}
|
|
95
|
-
</button>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
63
|
export function SubResourcesSection({
|
|
100
64
|
subResources,
|
|
101
65
|
initialSearch,
|
|
102
66
|
}: SubResourcesSectionProps) {
|
|
103
|
-
const { groups } = useAppCatalogContext()
|
|
104
67
|
// Seed the filter with the incoming query ONLY if it matches a child, so we
|
|
105
68
|
// reveal the matched sub-resource without hiding everything on a non-match.
|
|
106
69
|
const seededSearch = useMemo(() => {
|
|
@@ -195,7 +158,7 @@ export function SubResourcesSection({
|
|
|
195
158
|
<TableHead>Name</TableHead>
|
|
196
159
|
<TableHead className="w-[80px]">Tier</TableHead>
|
|
197
160
|
<TableHead>Owner</TableHead>
|
|
198
|
-
<TableHead>
|
|
161
|
+
<TableHead>Approvers</TableHead>
|
|
199
162
|
<TableHead className="w-[140px]">AWS Account</TableHead>
|
|
200
163
|
</TableRow>
|
|
201
164
|
</TableHeader>
|
|
@@ -211,15 +174,8 @@ export function SubResourcesSection({
|
|
|
211
174
|
</TableRow>
|
|
212
175
|
) : (
|
|
213
176
|
filtered.map((sr) => {
|
|
214
|
-
//
|
|
215
|
-
const
|
|
216
|
-
sr.accessMaintainerGroupSlugs ?? []
|
|
217
|
-
).flatMap((groupSlug) => {
|
|
218
|
-
const group = getGroupBySlug(groups, groupSlug)
|
|
219
|
-
return group?.memberSlugs ?? []
|
|
220
|
-
})
|
|
221
|
-
// Deduplicate
|
|
222
|
-
const uniqueMaintainers = [...new Set(maintainerMembers)]
|
|
177
|
+
// Approvers are Person OR Group slugs; the badge resolves both.
|
|
178
|
+
const approvers = [...new Set(sr.approverSlugs ?? [])]
|
|
223
179
|
|
|
224
180
|
return (
|
|
225
181
|
<TableRow key={sr.slug}>
|
|
@@ -254,10 +210,10 @@ export function SubResourcesSection({
|
|
|
254
210
|
)}
|
|
255
211
|
</TableCell>
|
|
256
212
|
<TableCell>
|
|
257
|
-
{
|
|
213
|
+
{approvers.length > 0 ? (
|
|
258
214
|
<div className="flex flex-wrap gap-1">
|
|
259
|
-
{
|
|
260
|
-
<
|
|
215
|
+
{approvers.map((slug) => (
|
|
216
|
+
<PersonOrGroupBadge key={slug} slug={slug} />
|
|
261
217
|
))}
|
|
262
218
|
</div>
|
|
263
219
|
) : (
|
|
@@ -270,7 +226,9 @@ export function SubResourcesSection({
|
|
|
270
226
|
sr.extra as Record<string, unknown> | null | undefined
|
|
271
227
|
)?.awsAccountId as string | undefined
|
|
272
228
|
return accountId ? (
|
|
273
|
-
<
|
|
229
|
+
<span className="font-mono text-xs text-muted-foreground select-text">
|
|
230
|
+
{accountId}
|
|
231
|
+
</span>
|
|
274
232
|
) : (
|
|
275
233
|
<span className="text-muted-foreground">—</span>
|
|
276
234
|
)
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
TableRow,
|
|
17
17
|
} from '~/ui/table'
|
|
18
18
|
import { useAppCatalogContext } from '~/modules/appCatalog'
|
|
19
|
-
import {
|
|
19
|
+
import { PersonOrGroupBadge } from './PersonBadge'
|
|
20
20
|
|
|
21
21
|
interface TierVariantsSectionProps {
|
|
22
22
|
tiers: TierVariant[]
|
|
@@ -76,7 +76,7 @@ function TierAccessDetail({
|
|
|
76
76
|
const hasExtra =
|
|
77
77
|
accessRequest.comments ||
|
|
78
78
|
accessRequest.urls?.length ||
|
|
79
|
-
accessRequest.
|
|
79
|
+
accessRequest.approverSlugs?.length ||
|
|
80
80
|
accessRequest.postApprovalInstructions
|
|
81
81
|
|
|
82
82
|
return (
|
|
@@ -131,11 +131,11 @@ function TierAccessDetail({
|
|
|
131
131
|
))}
|
|
132
132
|
</div>
|
|
133
133
|
)}
|
|
134
|
-
{accessRequest.
|
|
135
|
-
accessRequest.
|
|
134
|
+
{accessRequest.approverSlugs &&
|
|
135
|
+
accessRequest.approverSlugs.length > 0 && (
|
|
136
136
|
<div className="flex flex-wrap gap-1">
|
|
137
|
-
{accessRequest.
|
|
138
|
-
<
|
|
137
|
+
{accessRequest.approverSlugs.map((slug) => (
|
|
138
|
+
<PersonOrGroupBadge key={slug} slug={slug} />
|
|
139
139
|
))}
|
|
140
140
|
</div>
|
|
141
141
|
)}
|