@igstack/app-catalog-frontend-core 6.8.3 → 6.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/esm/index.d.ts +1 -1
  2. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +0 -1
  3. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
  4. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +22 -12
  5. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  6. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +0 -1
  7. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
  8. package/dist/esm/modules/dismiss/DismissStack.d.ts +9 -0
  9. package/dist/esm/modules/dismiss/DismissStack.js +26 -7
  10. package/dist/esm/modules/dismiss/DismissStack.js.map +1 -1
  11. package/dist/esm/modules/extensions/features/resourceDetail.d.ts +16 -1
  12. package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -1
  13. package/dist/esm/modules/extensions/index.d.ts +19 -3
  14. package/dist/esm/modules/extensions/index.js +10 -4
  15. package/dist/esm/modules/extensions/index.js.map +1 -1
  16. package/dist/esm/modules/extensions/types.d.ts +1 -0
  17. package/package.json +4 -4
  18. package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.tsx +41 -4
  19. package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.test.tsx +34 -0
  20. package/src/__tests__/modules/dismiss/DismissStack.test.tsx +91 -1
  21. package/src/index.tsx +2 -0
  22. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +60 -36
  23. package/src/modules/dismiss/DismissStack.tsx +52 -9
  24. package/src/modules/extensions/features/resourceDetail.ts +17 -1
  25. package/src/modules/extensions/index.ts +37 -2
  26. package/src/modules/extensions/types.ts +1 -0
@@ -7,7 +7,7 @@ export type { AreaIcon, AreasSettings, UiSettings } from './types/uiSettings.js'
7
7
  export { DAY_TO_DAY_AREA_KEY, DAY_TO_DAY_TAG, } from './modules/appCatalog/utils/areaGrouping.js';
8
8
  export { PwaAutoUpdateProvider, usePwaAutoUpdate } from './modules/pwa/index.js';
9
9
  export type { PwaAutoUpdateOptions, PwaUpdateHandle } from './modules/pwa/index.js';
10
- export type { AcPlugin, PluginSlots, PluginWrappers, PluginUser, SlotSpec, WrapperSpec, } from './modules/extensions/index.js';
10
+ export type { AcPlugin, PluginAppliesTo, PluginSlots, PluginWrappers, PluginUser, SlotScopeSpec, SlotSpec, WrapperSpec, } from './modules/extensions/index.js';
11
11
  export type { Resource } from '@igstack/app-catalog-backend-core';
12
12
  export { useCatalogResources } from './modules/appCatalog/context/AppCatalogContext.js';
13
13
  /**
@@ -5,7 +5,6 @@ import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
5
5
  import { useOptionalUser } from "../../../auth/AuthContext.js";
6
6
  import "@tanstack/react-query";
7
7
  import "../../../../ui/button.js";
8
- import "../../../auth/AuthModalContext.js";
9
8
  import { useDismissStack, useDismissable } from "../../../dismiss/DismissStack.js";
10
9
  import { getChildResources } from "../../utils/resolveHelpers.js";
11
10
  import { SubResourceDetailPanel } from "../components/SubResourceDetailPanel.js";
@@ -1 +1 @@
1
- {"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Maximize2, Minimize2, X } from 'lucide-react'\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailProvider,\n} from '~/modules/extensions'\nimport { useOptionalUser } from '~/modules/auth'\nimport { useDismissStack, useDismissable } from '~/modules/dismiss/DismissStack'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * Escape handling: this component does NOT globally bind Escape. Escape-to-close\n * is owned by AppDetails' own key handling (which first closes an open\n * screenshot gallery, then closes the card), so pressing Esc inside the gallery\n * returns to the card — not all the way to the home page (#38 Esc-stacking bug).\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const user = useOptionalUser()\n\n // The sub-resource branch has no band of its own, so its slot payload is\n // assembled here, where the catalog context already is, and handed down.\n const subResourceActionsCtx = useMemo(\n () => ({\n resource: subResource ?? app,\n parent: subResource ? app : undefined,\n subResources: getChildResources(allResources, app.slug),\n user: user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null,\n }),\n [subResource, app, allResources, user],\n )\n const dialogRef = useRef<HTMLDivElement>(null)\n // Not persisted: every open starts at the normal width. The toggle is for the\n // one card being read, and the default stays readable for the next one.\n const [isWide, setIsWide] = useState(false)\n\n /**\n * Centre the card ONCE when it opens, then hold that position.\n *\n * `my-auto` centred it on every render, so each panel height change pushed both\n * edges and half the delta became upward movement of everything above it —\n * including the tab strip, which moved 113px and took the control the user had\n * just clicked with it (CLS 0.19, \"needs improvement\").\n *\n * Top-anchoring fixed the shift but looked wrong: a short card sat high on the\n * page. So pin the centred offset instead. The card still opens centred, and\n * anything that grows it afterwards extends downward only — which is what a\n * reader expects when they open a tab with more in it.\n *\n * Only on open and on resize. Deliberately NOT on content change: reacting to\n * content is precisely the bug.\n */\n useLayoutEffect(() => {\n const card = dialogRef.current\n const scroller = card?.parentElement\n if (!card || !scroller) return\n\n const pinCentre = () => {\n card.style.marginTop = '0px'\n const style = getComputedStyle(scroller)\n const usable =\n scroller.clientHeight -\n parseFloat(style.paddingTop) -\n parseFloat(style.paddingBottom)\n const slack = usable - card.offsetHeight\n // Negative slack means the card is already taller than the viewport; leave it\n // at the top and let the overlay scroll, rather than pushing its head off.\n card.style.marginTop = `${Math.max(0, Math.round(slack / 2))}px`\n }\n\n pinCentre()\n window.addEventListener('resize', pinCentre)\n return () => window.removeEventListener('resize', pinCentre)\n // Keyed on the opened entry: a different card is a new open, and should\n // re-centre for its own height.\n }, [app.slug, subResource?.slug])\n\n // Focus the card on mount so Esc hotkeys fire immediately, even when\n // the card was opened by a mouse click (which leaves focus on the grid row).\n //\n // The door link rather than the dialog itself when the app has one: Enter on\n // a search result opens the card, so a second Enter is the browser's own link\n // activation and takes the reader into the app. No door: the dialog.\n //\n // Unless something inside already took the caret — the Quick Jump field does,\n // and it has the better claim: a child's effect runs before its parent's, so\n // without this guard the card would take focus straight back off it.\n //\n // Runs again whenever the card navigates to another resource, keyed the same\n // way the body below is: that navigation destroys whatever was focused (the\n // \"View replacement\" button, a prerequisite's parent), and without this the\n // caret would land on <body> — where the next Tab walks the catalog grid\n // behind the scrim, since the card has no focus trap.\n const shownSlug = subResource?.slug ?? app.slug\n const stack = useDismissStack()\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog || dialog.contains(document.activeElement)) return\n // Only while the card is on top. A layer opened OVER it — a deployment's\n // dialog on a nested route — owns the caret, and on a cold deep link that\n // layer mounts before the card does, so without this check the card took\n // focus back off the dialog's first field the moment the catalog arrived.\n const top = stack.topElement()\n if (top && top !== dialog) return\n const target = dialog.querySelector<HTMLElement>('.app-door') ?? dialog\n target.focus()\n }, [shownSlug, stack])\n\n // Escape closes the card. Registered here rather than in AppDetails because\n // this is the layer's element, and because the sub-resource view renders a\n // different body in place of AppDetails — which left that view with no\n // Escape at all while the registration lived in the body.\n useDismissable(dialogRef, onClose)\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10\">\n {/* scrim */}\n <button\n type=\"button\"\n aria-label=\"Close details panel\"\n onClick={onClose}\n className=\"fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in\"\n />\n {/* centered card — wide enough for data tables (sub-resources have 5\n columns incl. \"Access Contacts\"/\"AWS Account\" that wrapped at 760px);\n prose inside AppDetails is width-capped separately so it stays\n readable. Caps at 94vw so it never touches the edges. */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${subResource?.displayName ?? app.displayName} details`}\n tabIndex={-1}\n // Centred on open, then held — see the pin effect above. `my-auto` is\n // deliberately absent: it re-centres on EVERY height change, which is the\n // layout-shift bug.\n // `overflow-hidden` belongs on the rounded box: the scrolling child\n // below carries the band's tint to its own square corners and paints\n // them over this radius, so the card's top corners read as cut off.\n // Widening drops the cap only. The pinned top margin is left alone, so\n // the card's top edge stays put and restoring gives back the same layout.\n className={`relative w-full ${isWide ? 'max-w-none' : 'max-w-[min(1120px,94vw)]'} overflow-hidden rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none`}\n >\n <div className=\"absolute top-3 right-3 z-10 flex gap-1\">\n <button\n type=\"button\"\n aria-label={isWide ? 'Restore width' : 'Widen card'}\n title={isWide ? 'Restore width' : 'Widen card'}\n onClick={() => setIsWide((wide) => !wide)}\n className=\"rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n {isWide ? (\n <Minimize2 className=\"size-4\" />\n ) : (\n <Maximize2 className=\"size-4\" />\n )}\n </button>\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={onClose}\n className=\"rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n {/* Keyed by the resource on screen, so the body remounts when the card\n navigates from one app to another (\"View replacement\", an access\n prerequisite's parent) instead of updating in place. Everything\n per-app inside it is component state with no reset of its own — the\n roles table's expansion, the icon/screenshot error flags, a draft\n source edit — and it used to arrive at the next app still carrying\n the previous one's, plus the previous scroll offset.\n\n The key is here and not on the outer dialog on purpose: that one\n would restart the open animation and re-run the mount-focus effect\n on every in-card navigation. */}\n <div\n key={shownSlug}\n className=\"max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7\"\n >\n {/* Inside the key, so anything a plugin provides here is scoped to\n the resource on screen and is discarded when the card navigates.\n Renders its children untouched when no plugin contributes. */}\n <ResourceDetailProvider\n resource={subResource ?? app}\n parent={subResource ? app : undefined}\n >\n {subResource ? (\n <SubResourceDetailPanel\n subResource={subResource}\n parent={app}\n approvalMethods={approvalMethods}\n onBack={onBackToParent ?? onClose}\n actions={\n <ResourceDetailAccessActions {...subResourceActionsCtx} />\n }\n />\n ) : (\n <AppDetails app={app} onAppClick={onAppClick} />\n )}\n </ResourceDetailProvider>\n </div>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Maximize2, Minimize2, X } from 'lucide-react'\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailProvider,\n} from '~/modules/extensions'\nimport { useOptionalUser } from '~/modules/auth'\nimport { useDismissStack, useDismissable } from '~/modules/dismiss/DismissStack'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * Escape handling: this component does NOT globally bind Escape. Escape-to-close\n * is owned by AppDetails' own key handling (which first closes an open\n * screenshot gallery, then closes the card), so pressing Esc inside the gallery\n * returns to the card — not all the way to the home page (#38 Esc-stacking bug).\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const user = useOptionalUser()\n\n // The sub-resource branch has no band of its own, so its slot payload is\n // assembled here, where the catalog context already is, and handed down.\n const subResourceActionsCtx = useMemo(\n () => ({\n resource: subResource ?? app,\n parent: subResource ? app : undefined,\n subResources: getChildResources(allResources, app.slug),\n user: user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null,\n }),\n [subResource, app, allResources, user],\n )\n const dialogRef = useRef<HTMLDivElement>(null)\n // Not persisted: every open starts at the normal width. The toggle is for the\n // one card being read, and the default stays readable for the next one.\n const [isWide, setIsWide] = useState(false)\n\n /**\n * Centre the card ONCE when it opens, then hold that position.\n *\n * `my-auto` centred it on every render, so each panel height change pushed both\n * edges and half the delta became upward movement of everything above it —\n * including the tab strip, which moved 113px and took the control the user had\n * just clicked with it (CLS 0.19, \"needs improvement\").\n *\n * Top-anchoring fixed the shift but looked wrong: a short card sat high on the\n * page. So pin the centred offset instead. The card still opens centred, and\n * anything that grows it afterwards extends downward only — which is what a\n * reader expects when they open a tab with more in it.\n *\n * Only on open and on resize. Deliberately NOT on content change: reacting to\n * content is precisely the bug.\n */\n useLayoutEffect(() => {\n const card = dialogRef.current\n const scroller = card?.parentElement\n if (!card || !scroller) return\n\n const pinCentre = () => {\n card.style.marginTop = '0px'\n const style = getComputedStyle(scroller)\n const usable =\n scroller.clientHeight -\n parseFloat(style.paddingTop) -\n parseFloat(style.paddingBottom)\n const slack = usable - card.offsetHeight\n // Negative slack means the card is already taller than the viewport; leave it\n // at the top and let the overlay scroll, rather than pushing its head off.\n card.style.marginTop = `${Math.max(0, Math.round(slack / 2))}px`\n }\n\n pinCentre()\n window.addEventListener('resize', pinCentre)\n return () => window.removeEventListener('resize', pinCentre)\n // Keyed on the opened entry: a different card is a new open, and should\n // re-centre for its own height.\n }, [app.slug, subResource?.slug])\n\n // Focus the card on mount so Esc hotkeys fire immediately, even when\n // the card was opened by a mouse click (which leaves focus on the grid row).\n //\n // The door link rather than the dialog itself when the app has one: Enter on\n // a search result opens the card, so a second Enter is the browser's own link\n // activation and takes the reader into the app. No door: the dialog.\n //\n // Unless something inside already took the caret — the Quick Jump field does,\n // and it has the better claim: a child's effect runs before its parent's, so\n // without this guard the card would take focus straight back off it.\n //\n // Runs again whenever the card navigates to another resource, keyed the same\n // way the body below is: that navigation destroys whatever was focused (the\n // \"View replacement\" button, a prerequisite's parent), and without this the\n // caret would land on <body> — where the next Tab walks the catalog grid\n // behind the scrim, since the card has no focus trap.\n const shownSlug = subResource?.slug ?? app.slug\n const stack = useDismissStack()\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog || dialog.contains(document.activeElement)) return\n // Only while the card is on top. A layer opened OVER it — a deployment's\n // dialog on a nested route — owns the caret, and on a cold deep link that\n // layer mounts before the card does, so without this check the card took\n // focus back off the dialog's first field the moment the catalog arrived.\n const top = stack.topElement()\n if (top && top !== dialog) return\n const target = dialog.querySelector<HTMLElement>('.app-door') ?? dialog\n target.focus()\n }, [shownSlug, stack])\n\n // Escape closes the card. Registered here rather than in AppDetails because\n // this is the layer's element, and because the sub-resource view renders a\n // different body in place of AppDetails — which left that view with no\n // Escape at all while the registration lived in the body.\n useDismissable(dialogRef, onClose)\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10\">\n {/* scrim */}\n <button\n type=\"button\"\n aria-label=\"Close details panel\"\n onClick={onClose}\n className=\"fixed inset-0 -z-10 bg-black/40 backdrop-blur-[2px] animate-in fade-in\"\n />\n {/* centered card — wide enough for data tables (sub-resources have 5\n columns incl. \"Access Contacts\"/\"AWS Account\" that wrapped at 760px);\n prose inside AppDetails is width-capped separately so it stays\n readable. Caps at 94vw so it never touches the edges. */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${subResource?.displayName ?? app.displayName} details`}\n tabIndex={-1}\n // Centred on open, then held — see the pin effect above. `my-auto` is\n // deliberately absent: it re-centres on EVERY height change, which is the\n // layout-shift bug.\n // `overflow-hidden` belongs on the rounded box: the scrolling child\n // below carries the band's tint to its own square corners and paints\n // them over this radius, so the card's top corners read as cut off.\n // Widening drops the cap only. The pinned top margin is left alone, so\n // the card's top edge stays put and restoring gives back the same layout.\n className={`relative w-full ${isWide ? 'max-w-none' : 'max-w-[min(1120px,94vw)]'} overflow-hidden rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none`}\n >\n <div className=\"absolute top-3 right-3 z-10 flex gap-1\">\n <button\n type=\"button\"\n aria-label={isWide ? 'Restore width' : 'Widen card'}\n title={isWide ? 'Restore width' : 'Widen card'}\n onClick={() => setIsWide((wide) => !wide)}\n className=\"rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n {isWide ? (\n <Minimize2 className=\"size-4\" />\n ) : (\n <Maximize2 className=\"size-4\" />\n )}\n </button>\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={onClose}\n className=\"rounded-full p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n {/* Keyed by the resource on screen, so the body remounts when the card\n navigates from one app to another (\"View replacement\", an access\n prerequisite's parent) instead of updating in place. Everything\n per-app inside it is component state with no reset of its own — the\n roles table's expansion, the icon/screenshot error flags, a draft\n source edit — and it used to arrive at the next app still carrying\n the previous one's, plus the previous scroll offset.\n\n The key is here and not on the outer dialog on purpose: that one\n would restart the open animation and re-run the mount-focus effect\n on every in-card navigation. */}\n <div\n key={shownSlug}\n className=\"max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7\"\n >\n {/* Inside the key, so anything a plugin provides here is scoped to\n the resource on screen and is discarded when the card navigates.\n Renders its children untouched when no plugin contributes. */}\n <ResourceDetailProvider\n resource={subResource ?? app}\n parent={subResource ? app : undefined}\n >\n {subResource ? (\n <SubResourceDetailPanel\n subResource={subResource}\n parent={app}\n approvalMethods={approvalMethods}\n onBack={onBackToParent ?? onClose}\n actions={\n <ResourceDetailAccessActions {...subResourceActionsCtx} />\n }\n />\n ) : (\n <AppDetails app={app} onAppClick={onAppClick} />\n )}\n </ResourceDetailProvider>\n </div>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -11,6 +11,10 @@ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from ".
11
11
  import { markdownToPlainText } from "../../utils/markdownToPlainText.js";
12
12
  import { useSlotFilled, usePluginUser } from "../../../extensions/index.js";
13
13
  import { ResourceSubResourceRowActions } from "../../../extensions/features/resourceDetail.js";
14
+ function awsAccountIdOf(sr) {
15
+ var _a;
16
+ return (_a = sr.extra) == null ? void 0 : _a.awsAccountId;
17
+ }
14
18
  function getTierBadgeVariant(tierSlug) {
15
19
  if (tierSlug === "prod" || tierSlug === "production") return "destructive";
16
20
  if (tierSlug === "dev" || tierSlug === "staging") return "secondary";
@@ -37,14 +41,14 @@ function SubResourceRowBase({
37
41
  resource: sr,
38
42
  parentSlug,
39
43
  adminParent,
44
+ showAccount,
40
45
  user,
41
46
  isHighlighted,
42
47
  measureRef,
43
48
  index
44
49
  }) {
45
- var _a;
46
50
  const approvers = [...new Set(sr.approverSlugs ?? [])];
47
- const accountId = (_a = sr.extra) == null ? void 0 : _a.awsAccountId;
51
+ const accountId = awsAccountIdOf(sr);
48
52
  return /* @__PURE__ */ jsxs(
49
53
  TableRow,
50
54
  {
@@ -76,7 +80,7 @@ function SubResourceRowBase({
76
80
  ) }),
77
81
  /* @__PURE__ */ jsx(TableCell, { children: sr.ownerPersonSlug && /* @__PURE__ */ jsx(PersonBadge, { slug: sr.ownerPersonSlug }) }),
78
82
  /* @__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: "-" }) }),
79
- /* @__PURE__ */ jsx(TableCell, { children: !accountId ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" }) : (
83
+ showAccount && /* @__PURE__ */ jsx(TableCell, { children: !accountId ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" }) : (
80
84
  // The account id is what people copy; when the resource also
81
85
  // carries its own `appUrl` that is the console deep link for THAT
82
86
  // account, so the id doubles as the launch affordance.
@@ -153,7 +157,14 @@ function SubResourcesSection({
153
157
  initialSearch,
154
158
  childrenLabel
155
159
  }) {
156
- const showAdminColumn = useSlotFilled("resourceSubResourceRowActions") && parent !== void 0;
160
+ const showAdminColumn = useSlotFilled(
161
+ "resourceSubResourceRowActions",
162
+ parent ? { parent } : void 0
163
+ );
164
+ const showAccountColumn = useMemo(
165
+ () => subResources.some((sr) => awsAccountIdOf(sr) !== void 0),
166
+ [subResources]
167
+ );
157
168
  const user = usePluginUser();
158
169
  const search_ = useSearch({ strict: false });
159
170
  const selectedSubSlug = search_.sub;
@@ -212,11 +223,8 @@ function SubResourcesSection({
212
223
  const q = deferredSearch.trim().toLowerCase();
213
224
  result = result.filter(
214
225
  (sr) => {
215
- var _a, _b, _c;
216
- return sr.displayName.toLowerCase().includes(q) || (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) || (((_a = sr.description) == null ? void 0 : _a.toLowerCase().includes(q)) ?? false) || // `extra` is nullable on a served resource, and the cast hid that:
217
- // dereferencing it threw a TypeError the moment anything typed into
218
- // this box met a child without it, taking the whole table down.
219
- ((_c = (_b = sr.extra) == null ? void 0 : _b.awsAccountId) == null ? void 0 : _c.includes(q)) === true;
226
+ var _a, _b;
227
+ return sr.displayName.toLowerCase().includes(q) || (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) || (((_a = sr.description) == null ? void 0 : _a.toLowerCase().includes(q)) ?? false) || ((_b = awsAccountIdOf(sr)) == null ? void 0 : _b.includes(q)) === true;
220
228
  }
221
229
  );
222
230
  }
@@ -225,7 +233,7 @@ function SubResourcesSection({
225
233
  const scrollRef = useRef(null);
226
234
  const { windowed, virtualRows, padTop, padBottom, measureElement } = useRowWindow(filtered, scrollRef);
227
235
  const fixedLayout = subResources.length > VIRTUALIZE_ABOVE;
228
- const columnCount = showAdminColumn ? 6 : 5;
236
+ const columnCount = 4 + (showAccountColumn ? 1 : 0) + (showAdminColumn ? 1 : 0);
229
237
  if (subResources.length === 0) return null;
230
238
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
231
239
  /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between", children: /* @__PURE__ */ jsxs(
@@ -300,13 +308,13 @@ function SubResourcesSection({
300
308
  /* @__PURE__ */ jsx(TableHead, { className: "w-[80px]", children: "Tier" }),
301
309
  /* @__PURE__ */ jsx(TableHead, { className: fixedLayout ? "w-[190px]" : void 0, children: "Owner" }),
302
310
  /* @__PURE__ */ jsx(TableHead, { className: fixedLayout ? "w-[230px]" : void 0, children: "Approvers" }),
303
- /* @__PURE__ */ jsx(TableHead, { className: "w-[140px]", children: "AWS Account" }),
311
+ showAccountColumn && /* @__PURE__ */ jsx(TableHead, { className: "w-[140px]", children: "AWS Account" }),
304
312
  showAdminColumn && /* @__PURE__ */ jsx(TableHead, { className: "w-[168px]", children: "Admin" })
305
313
  ] }) }),
306
314
  /* @__PURE__ */ jsx(TableBody, { children: filtered.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
307
315
  TableCell,
308
316
  {
309
- colSpan: showAdminColumn ? 6 : 5,
317
+ colSpan: columnCount,
310
318
  className: "text-center text-muted-foreground py-8",
311
319
  children: "No resources match your filters"
312
320
  }
@@ -321,6 +329,7 @@ function SubResourcesSection({
321
329
  resource: sr,
322
330
  parentSlug,
323
331
  adminParent: showAdminColumn ? parent : void 0,
332
+ showAccount: showAccountColumn,
324
333
  user,
325
334
  isHighlighted: sr.slug === highlightSlug,
326
335
  measureRef: measureElement,
@@ -336,6 +345,7 @@ function SubResourcesSection({
336
345
  resource: sr,
337
346
  parentSlug,
338
347
  adminParent: showAdminColumn ? parent : void 0,
348
+ showAccount: showAccountColumn,
339
349
  user,
340
350
  isHighlighted: sr.slug === highlightSlug
341
351
  },
@@ -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 { Link, useNavigate, useSearch } from '@tanstack/react-router'\nimport { Search, X } from 'lucide-react'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport {\n memo,\n useCallback,\n useDeferredValue,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} 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'\nimport {\n ResourceSubResourceRowActions,\n usePluginUser,\n useSlotFilled,\n} from '~/modules/extensions/index'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n /** Slug of the parent, for linking to a child's own page. */\n parentSlug: string\n /**\n * The parent itself, for the row-actions slot's payload.\n *\n * Optional, and the Admin column is skipped without it: a row action is about\n * a child *of something*, and a plugin deciding whether it applies needs the\n * parent to decide against.\n */\n parent?: 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 * What this app calls its children, from the parent's `childrenLabel`.\n *\n * The tab above already uses it, so without it here the tab and the panel it\n * opens disagree — \"AWS Accounts\" over a heading that says \"Sub-Resources\".\n * Defaults to the same generic wording the tab falls back to.\n */\n childrenLabel?: 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\ninterface SubResourceRowProps {\n resource: Resource\n parentSlug: string\n /**\n * The parent, but ONLY when the Admin column is being rendered — so this one\n * prop carries both \"show the cell\" and \"what to pass it\", and the row needs\n * no separate boolean. `undefined` means no Admin cell, which keeps the cells\n * in lockstep with the header and the empty row's `colSpan`, all three\n * derived from `showAdminColumn` in one place above.\n */\n adminParent?: Resource\n user: ReturnType<typeof usePluginUser>\n isHighlighted: boolean\n /** Set only when the body is virtualized: lets the virtualizer measure the row. */\n measureRef?: (el: HTMLTableRowElement | null) => void\n index?: number\n}\n\n/**\n * One row, memoized — which is load-bearing rather than tidiness.\n *\n * Measured on the 667-account table: a filter keystroke that changed no visible\n * row still cost ~400ms and wrote 9 attributes, because every row's element\n * tree was rebuilt and diffed. The browser can build all 33,861 nodes from\n * scratch in ~195ms, so React was spending twice that to change nothing. The\n * work was reconciliation, not the DOM.\n *\n * `memo` makes a keystroke cost one props comparison per row instead. For that\n * to hold, every prop here must be a primitive or a stable reference:\n * `resource` objects come from the parent's `useMemo` filter (same identities),\n * `user` is memoized in `usePluginUser`, and the highlight arrives as a\n * BOOLEAN. Passing `highlightSlug` and comparing inside would change one prop\n * on all 667 rows whenever the highlight moved, re-rendering the whole table to\n * restyle two rows.\n */\nfunction SubResourceRowBase({\n resource: sr,\n parentSlug,\n adminParent,\n user,\n isHighlighted,\n measureRef,\n index,\n}: SubResourceRowProps) {\n // Approvers are Person OR Group slugs; the badge resolves both.\n const approvers = [...new Set(sr.approverSlugs ?? [])]\n const accountId = (sr.extra as Record<string, unknown> | null | undefined)\n ?.awsAccountId as string | undefined\n\n return (\n <TableRow\n ref={measureRef}\n data-index={index}\n // The row the user asked for, so it reads as \"this one\" even once the\n // filter is cleared and its siblings come back.\n aria-current={isHighlighted ? 'true' : undefined}\n // `group` so a row-slot contribution can react to the row being hovered\n // or focused (`group-hover:`, `group-focus-within:`). A plugin cannot add\n // this itself — it renders inside the cell, not on the row — and the\n // alternative is every plugin hand-rolling a `tr:hover &` arbitrary\n // variant.\n className={`group ${isHighlighted ? 'bg-primary/[0.06]' : ''}`}\n >\n <TableCell>\n {/* A real link, not a click handler on the row: this is a table of\n resources, each of which has its own page. */}\n <Link\n to=\"/app/$slug/sub/$subSlug\"\n params={{ slug: parentSlug, subSlug: sr.slug }}\n className=\"font-medium text-sm hover:text-primary hover:underline\"\n >\n {sr.displayName}\n </Link>\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 && <PersonBadge slug={sr.ownerPersonSlug} />}\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 {!accountId ? (\n <span className=\"text-muted-foreground\">—</span>\n ) : // The account id is what people copy; when the resource also\n // carries its own `appUrl` that is the console deep link for THAT\n // account, so the id doubles as the launch affordance.\n sr.appUrl ? (\n <a\n href={sr.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"font-mono text-xs text-muted-foreground hover:text-primary hover:underline select-text\"\n >\n {accountId}\n </a>\n ) : (\n <span className=\"font-mono text-xs text-muted-foreground select-text\">\n {accountId}\n </span>\n )}\n </TableCell>\n {adminParent !== undefined && (\n // `relative`, so a contribution can position something against the\n // cell — a resting-state glyph under a hover-revealed control, for\n // instance. A plugin cannot add it, because the cell is the core's.\n <TableCell className=\"relative w-[168px]\">\n <ResourceSubResourceRowActions\n resource={sr}\n parent={adminParent}\n user={user}\n />\n </TableCell>\n )}\n </TableRow>\n )\n}\n\nconst SubResourceRow = memo(SubResourceRowBase)\n\n/** Stands in for the rows scrolled out of view, so the scrollbar is honest. */\nfunction SpacerRow({ height, columns }: { height: number; columns: number }) {\n return (\n <tr aria-hidden=\"true\">\n <td colSpan={columns} style={{ height, padding: 0, border: 0 }} />\n </tr>\n )\n}\n\n/**\n * Past this many rows the body is virtualized: only what is on screen, plus\n * a margin, is mounted.\n *\n * Tab panels mount only while active, so every switch to this tab built the\n * whole table from scratch — measured at ~950 ms for 667 rows and 33,861\n * nodes, every time. `memo` cannot help with that: it saves RE-rendering,\n * and this is mounting. Below the threshold nothing changes, which keeps the\n * common short child list rendering exactly as it always has.\n *\n * Spacer rows rather than absolutely positioned ones, so this stays a real\n * `<table>`: column widths still come from table layout, and the sticky\n * header still resolves against the container that scrolls.\n */\nconst VIRTUALIZE_ABOVE = 60\n\nfunction useRowWindow(\n rows: readonly Resource[],\n scrollRef: React.RefObject<HTMLDivElement | null>,\n) {\n const virtualize = rows.length > VIRTUALIZE_ABOVE\n // Stable on purpose. The virtualizer caches row measurements keyed on this\n // function's IDENTITY, so an inline arrow — a new function every render —\n // invalidated the cache on every render, which recomputed the visible range,\n // which re-rendered, which made a new arrow: \"Too many re-renders\", and the\n // card blanked the moment this tab opened.\n const getItemKey = useCallback((i: number) => rows[i]?.slug ?? i, [rows])\n // TanStack Virtual hands back functions the React Compiler cannot memoize\n // safely, so it skips this hook. Nothing to fix short of dropping the\n // library; registered in eslint.config.js alongside TanStack Table.\n // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Virtual\n const rowVirtualizer = useVirtualizer({\n count: virtualize ? rows.length : 0,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => 88,\n overscan: 8,\n getItemKey,\n // The container's height limit, so the first render already mounts the\n // rows that will be visible instead of painting an empty body for a frame.\n initialRect: { width: 0, height: 400 },\n })\n // No layout, no window. Without layout (jsdom, a print preview) the\n // container measures 0 tall, a virtualizer mounts nothing, and every row\n // becomes unreachable — so render them all instead.\n const [hasLayout, setHasLayout] = useState(true)\n useLayoutEffect(() => {\n const el = scrollRef.current\n if (el) setHasLayout(el.clientHeight > 0)\n }, [scrollRef])\n const windowed = virtualize && hasLayout\n const virtualRows = windowed ? rowVirtualizer.getVirtualItems() : []\n const padTop = virtualRows[0]?.start ?? 0\n const lastRow = virtualRows.at(-1)\n const padBottom = lastRow ? rowVirtualizer.getTotalSize() - lastRow.end : 0\n return {\n windowed,\n virtualRows,\n padTop,\n padBottom,\n measureElement: rowVirtualizer.measureElement,\n }\n}\n\nexport function SubResourcesSection({\n subResources,\n parentSlug,\n parent,\n initialSearch,\n childrenLabel,\n}: SubResourcesSectionProps) {\n // `useSlotFilled`, not \"render it and see\": the column is layout, so the\n // header has to be decided before any cell renders.\n //\n // ONE flag for the header, the cells and the empty row's colSpan. Deriving it\n // separately per site is how a header appears over a column with no cells —\n // the parent check only guarded the cells at first, and the header rendered\n // anyway.\n const showAdminColumn =\n useSlotFilled('resourceSubResourceRowActions') && parent !== undefined\n const user = usePluginUser()\n // `?sub=<slug>` — the one sub-resource the user asked for, by clicking its row\n // in the search results. It wins over the query-seeded filter below: a query\n // like \"project\" matches every child, so seeding with it would bury the row\n // that was actually clicked.\n const search_ = useSearch({ strict: false })\n const selectedSubSlug = search_.sub\n const selectedSub = useMemo(\n () => subResources.find((sr) => sr.slug === selectedSubSlug) ?? null,\n [subResources, selectedSubSlug],\n )\n const navigate = useNavigate()\n\n // The row to mark as \"this one\". It outlives `?sub=` so clearing the filter\n // brings the siblings back without losing the user's place.\n const [highlightSlug, setHighlightSlug] = useState(selectedSubSlug)\n useEffect(() => {\n if (selectedSubSlug) setHighlightSlug(selectedSubSlug)\n }, [selectedSubSlug])\n\n // The search input only mounts once the selection is gone, so focus it then.\n const searchInputRef = useRef<HTMLInputElement>(null)\n const focusSearchAfterClearRef = useRef(false)\n useEffect(() => {\n if (!selectedSub && focusSearchAfterClearRef.current) {\n focusSearchAfterClearRef.current = false\n searchInputRef.current?.focus()\n }\n }, [selectedSub])\n\n const clearSelection = () => {\n focusSearchAfterClearRef.current = true\n // A navigation, not a replace, so Back restores the single-row view.\n void navigate({\n to: '.',\n search: (prev) => ({ ...prev, sub: undefined }),\n })\n }\n\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 /**\n * The input stays bound to `search`; the TABLE filters on this.\n *\n * Rebuilding the row list is the expensive half of a keystroke — on a large\n * entry it mounts and unmounts hundreds of rows — and doing it in the same\n * render as the input makes the caret wait for it. `useDeferredValue` splits\n * them: React commits the typed character immediately and re-renders the\n * table at low priority, interruptibly. Keep typing and the superseded\n * low-priority render is abandoned rather than finished and thrown away, so\n * a burst costs roughly one filter pass instead of one per character.\n *\n * This only works because the row is memoized. Deferring tells React it MAY\n * skip the expensive subtree between keystrokes; `memo` is what lets it\n * actually skip anything.\n *\n * Not a debounce: there is no fixed delay to tune, nothing is thrown away on\n * a fast typist, and the final result is never late — React yields to input\n * instead of waiting on a timer.\n */\n const deferredSearch = useDeferredValue(search)\n // True while the table is a render behind the box. Used only to dim it, so\n // stale rows read as stale instead of as the answer.\n const isFilterPending = search !== deferredSearch\n\n // useState only reads its argument on the first render, so without this the\n // filter kept the query from whenever this panel first mounted — searching\n // again with the panel already open left the old term in the box.\n useEffect(() => setSearch(seededSearch), [seededSearch])\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 if (selectedSub) return [selectedSub]\n\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (deferredSearch.trim()) {\n const q = deferredSearch.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 // `extra` is nullable on a served resource, and the cast hid that:\n // dereferencing it threw a TypeError the moment anything typed into\n // this box met a child without it, taking the whole table down.\n (\n (sr.extra as Record<string, unknown> | null | undefined)\n ?.awsAccountId as string | undefined\n )?.includes(q) === true,\n )\n }\n\n return result\n }, [subResources, deferredSearch, tierFilter, selectedSub])\n\n const scrollRef = useRef<HTMLDivElement>(null)\n const { windowed, virtualRows, padTop, padBottom, measureElement } =\n useRowWindow(filtered, scrollRef)\n // Fixed layout for any table big enough to be windowed. With auto layout a\n // column is as wide as its widest MOUNTED cell, so while scrolling a\n // windowed body the columns resized under the cursor — measured at 264px.\n // Keyed on the unfiltered size so a big table keeps one layout while it is\n // filtered below the threshold, rather than jumping as the user types.\n const fixedLayout = subResources.length > VIRTUALIZE_ABOVE\n const columnCount = showAdminColumn ? 6 : 5\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 {/* `data-testid`, because the label is now deployment-defined: the test\n kit used to locate this heading by matching the literal string\n \"Sub-Resources (n of m)\", which silently stopped finding it the\n moment a catalog named its children something else. */}\n <div\n data-testid=\"sub-resources-heading\"\n className=\"text-sm font-medium\"\n >\n {/* Each site keeps the exact wording it had before `childrenLabel`\n existed, so a deployment that names nothing renders identically —\n the heading and the placeholder never agreed, and quietly making\n them agree would change every existing catalog. */}\n {childrenLabel ?? 'Sub-Resources'} ({filtered.length} of{' '}\n {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n {selectedSub ? (\n // `?sub=` wins over the search box, so show it where the box would be\n // and make it dismissible — otherwise the only way out is the URL.\n <div className=\"flex flex-1 items-center\">\n <Badge variant=\"secondary\" className=\"h-9 gap-1 pl-3 pr-1 text-sm\">\n Showing: {selectedSub.displayName}\n <button\n type=\"button\"\n aria-label=\"Clear sub-resource filter\"\n onClick={clearSelection}\n className=\"rounded-sm p-1 hover:bg-muted-foreground/20\"\n >\n <X className=\"size-4\" />\n </button>\n </Badge>\n </div>\n ) : (\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n ref={searchInputRef}\n // Verbatim, not lower-cased: the deployment chose the casing and\n // an acronym does not survive it — \"AWS Accounts\" became\n // \"Search aws accounts…\". The no-label default stays lowercase\n // because that is the wording this placeholder already had.\n placeholder={`Search ${childrenLabel ?? '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 )}\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 The vertical scroll sits on the Table's own wrapper rather than here,\n so the sticky header below pins to the element that actually scrolls —\n see the comment on `stickyHeader`. */}\n {/* Dimmed while the rows are a render behind the filter box, so stale\n rows read as stale rather than as the answer. `aria-busy` says the\n same thing to a screen reader. Opacity only — no spinner and no\n layout change, because on a fast filter this lasts one frame and\n anything heavier would strobe. */}\n <div\n className=\"rounded-lg border overflow-hidden\"\n aria-busy={isFilterPending || undefined}\n >\n <Table\n stickyHeader\n containerRef={scrollRef}\n className={`max-h-[400px] transition-opacity ${\n fixedLayout ? '[&>table]:table-fixed' : ''\n } ${isFilterPending ? 'opacity-60' : ''}`}\n >\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead className={fixedLayout ? 'w-[190px]' : undefined}>\n Owner\n </TableHead>\n <TableHead className={fixedLayout ? 'w-[230px]' : undefined}>\n Approvers\n </TableHead>\n <TableHead className=\"w-[140px]\">AWS Account</TableHead>\n {/* Only when something fills it: an empty column with a header and\n no cells is worse than no column, and the open-source build\n registers no plugins at all. */}\n {showAdminColumn && (\n <TableHead className=\"w-[168px]\">Admin</TableHead>\n )}\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={showAdminColumn ? 6 : 5}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : windowed ? (\n <>\n {padTop > 0 && (\n <SpacerRow height={padTop} columns={columnCount} />\n )}\n {virtualRows.map((v) => {\n const sr = filtered[v.index]\n if (!sr) return null\n return (\n <SubResourceRow\n key={sr.slug}\n resource={sr}\n parentSlug={parentSlug}\n adminParent={showAdminColumn ? parent : undefined}\n user={user}\n isHighlighted={sr.slug === highlightSlug}\n measureRef={measureElement}\n index={v.index}\n />\n )\n })}\n {padBottom > 0 && (\n <SpacerRow height={padBottom} columns={columnCount} />\n )}\n </>\n ) : (\n filtered.map((sr) => (\n <SubResourceRow\n key={sr.slug}\n resource={sr}\n parentSlug={parentSlug}\n // One prop for \"render the Admin cell, with this parent\".\n // `showAdminColumn` already implies `parent !== undefined`,\n // so this cannot disagree with the header or the colSpan.\n adminParent={showAdminColumn ? parent : undefined}\n user={user}\n isHighlighted={sr.slug === highlightSlug}\n />\n ))\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":["_a"],"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 { Link, useNavigate, useSearch } from '@tanstack/react-router'\nimport { Search, X } from 'lucide-react'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport {\n memo,\n useCallback,\n useDeferredValue,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} 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'\nimport {\n ResourceSubResourceRowActions,\n usePluginUser,\n useSlotFilled,\n} from '~/modules/extensions/index'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n /** Slug of the parent, for linking to a child's own page. */\n parentSlug: string\n /**\n * The parent itself, for the row-actions slot's payload.\n *\n * Optional, and the Admin column is skipped without it: a row action is about\n * a child *of something*, and a plugin deciding whether it applies needs the\n * parent to decide against.\n */\n parent?: 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 * What this app calls its children, from the parent's `childrenLabel`.\n *\n * The tab above already uses it, so without it here the tab and the panel it\n * opens disagree — \"AWS Accounts\" over a heading that says \"Sub-Resources\".\n * Defaults to the same generic wording the tab falls back to.\n */\n childrenLabel?: string\n}\n\n/**\n * The AWS account a child stands for, if it is one. `extra` is nullable on a\n * served resource, so the optional chain is load-bearing: dereferencing it threw\n * the moment anything typed into the filter met a child without it.\n */\nfunction awsAccountIdOf(sr: Resource): string | undefined {\n return (sr.extra as Record<string, unknown> | null | undefined)\n ?.awsAccountId as string | undefined\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\ninterface SubResourceRowProps {\n resource: Resource\n parentSlug: string\n /**\n * The parent, but ONLY when the Admin column is being rendered — so this one\n * prop carries both \"show the cell\" and \"what to pass it\", and the row needs\n * no separate boolean. `undefined` means no Admin cell, which keeps the cells\n * in lockstep with the header and the empty row's `colSpan`, all three\n * derived from `showAdminColumn` in one place above.\n */\n adminParent?: Resource\n /** Whether the AWS Account column is rendered — `showAccountColumn` above. */\n showAccount: boolean\n user: ReturnType<typeof usePluginUser>\n isHighlighted: boolean\n /** Set only when the body is virtualized: lets the virtualizer measure the row. */\n measureRef?: (el: HTMLTableRowElement | null) => void\n index?: number\n}\n\n/**\n * One row, memoized — which is load-bearing rather than tidiness.\n *\n * Measured on the 667-account table: a filter keystroke that changed no visible\n * row still cost ~400ms and wrote 9 attributes, because every row's element\n * tree was rebuilt and diffed. The browser can build all 33,861 nodes from\n * scratch in ~195ms, so React was spending twice that to change nothing. The\n * work was reconciliation, not the DOM.\n *\n * `memo` makes a keystroke cost one props comparison per row instead. For that\n * to hold, every prop here must be a primitive or a stable reference:\n * `resource` objects come from the parent's `useMemo` filter (same identities),\n * `user` is memoized in `usePluginUser`, and the highlight arrives as a\n * BOOLEAN. Passing `highlightSlug` and comparing inside would change one prop\n * on all 667 rows whenever the highlight moved, re-rendering the whole table to\n * restyle two rows.\n */\nfunction SubResourceRowBase({\n resource: sr,\n parentSlug,\n adminParent,\n showAccount,\n user,\n isHighlighted,\n measureRef,\n index,\n}: SubResourceRowProps) {\n // Approvers are Person OR Group slugs; the badge resolves both.\n const approvers = [...new Set(sr.approverSlugs ?? [])]\n const accountId = awsAccountIdOf(sr)\n\n return (\n <TableRow\n ref={measureRef}\n data-index={index}\n // The row the user asked for, so it reads as \"this one\" even once the\n // filter is cleared and its siblings come back.\n aria-current={isHighlighted ? 'true' : undefined}\n // `group` so a row-slot contribution can react to the row being hovered\n // or focused (`group-hover:`, `group-focus-within:`). A plugin cannot add\n // this itself — it renders inside the cell, not on the row — and the\n // alternative is every plugin hand-rolling a `tr:hover &` arbitrary\n // variant.\n className={`group ${isHighlighted ? 'bg-primary/[0.06]' : ''}`}\n >\n <TableCell>\n {/* A real link, not a click handler on the row: this is a table of\n resources, each of which has its own page. */}\n <Link\n to=\"/app/$slug/sub/$subSlug\"\n params={{ slug: parentSlug, subSlug: sr.slug }}\n className=\"font-medium text-sm hover:text-primary hover:underline\"\n >\n {sr.displayName}\n </Link>\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 && <PersonBadge slug={sr.ownerPersonSlug} />}\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 {showAccount && (\n <TableCell>\n {!accountId ? (\n <span className=\"text-muted-foreground\">—</span>\n ) : // The account id is what people copy; when the resource also\n // carries its own `appUrl` that is the console deep link for THAT\n // account, so the id doubles as the launch affordance.\n sr.appUrl ? (\n <a\n href={sr.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"font-mono text-xs text-muted-foreground hover:text-primary hover:underline select-text\"\n >\n {accountId}\n </a>\n ) : (\n <span className=\"font-mono text-xs text-muted-foreground select-text\">\n {accountId}\n </span>\n )}\n </TableCell>\n )}\n {adminParent !== undefined && (\n // `relative`, so a contribution can position something against the\n // cell — a resting-state glyph under a hover-revealed control, for\n // instance. A plugin cannot add it, because the cell is the core's.\n <TableCell className=\"relative w-[168px]\">\n <ResourceSubResourceRowActions\n resource={sr}\n parent={adminParent}\n user={user}\n />\n </TableCell>\n )}\n </TableRow>\n )\n}\n\nconst SubResourceRow = memo(SubResourceRowBase)\n\n/** Stands in for the rows scrolled out of view, so the scrollbar is honest. */\nfunction SpacerRow({ height, columns }: { height: number; columns: number }) {\n return (\n <tr aria-hidden=\"true\">\n <td colSpan={columns} style={{ height, padding: 0, border: 0 }} />\n </tr>\n )\n}\n\n/**\n * Past this many rows the body is virtualized: only what is on screen, plus\n * a margin, is mounted.\n *\n * Tab panels mount only while active, so every switch to this tab built the\n * whole table from scratch — measured at ~950 ms for 667 rows and 33,861\n * nodes, every time. `memo` cannot help with that: it saves RE-rendering,\n * and this is mounting. Below the threshold nothing changes, which keeps the\n * common short child list rendering exactly as it always has.\n *\n * Spacer rows rather than absolutely positioned ones, so this stays a real\n * `<table>`: column widths still come from table layout, and the sticky\n * header still resolves against the container that scrolls.\n */\nconst VIRTUALIZE_ABOVE = 60\n\nfunction useRowWindow(\n rows: readonly Resource[],\n scrollRef: React.RefObject<HTMLDivElement | null>,\n) {\n const virtualize = rows.length > VIRTUALIZE_ABOVE\n // Stable on purpose. The virtualizer caches row measurements keyed on this\n // function's IDENTITY, so an inline arrow — a new function every render —\n // invalidated the cache on every render, which recomputed the visible range,\n // which re-rendered, which made a new arrow: \"Too many re-renders\", and the\n // card blanked the moment this tab opened.\n const getItemKey = useCallback((i: number) => rows[i]?.slug ?? i, [rows])\n // TanStack Virtual hands back functions the React Compiler cannot memoize\n // safely, so it skips this hook. Nothing to fix short of dropping the\n // library; registered in eslint.config.js alongside TanStack Table.\n // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Virtual\n const rowVirtualizer = useVirtualizer({\n count: virtualize ? rows.length : 0,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => 88,\n overscan: 8,\n getItemKey,\n // The container's height limit, so the first render already mounts the\n // rows that will be visible instead of painting an empty body for a frame.\n initialRect: { width: 0, height: 400 },\n })\n // No layout, no window. Without layout (jsdom, a print preview) the\n // container measures 0 tall, a virtualizer mounts nothing, and every row\n // becomes unreachable — so render them all instead.\n const [hasLayout, setHasLayout] = useState(true)\n useLayoutEffect(() => {\n const el = scrollRef.current\n if (el) setHasLayout(el.clientHeight > 0)\n }, [scrollRef])\n const windowed = virtualize && hasLayout\n const virtualRows = windowed ? rowVirtualizer.getVirtualItems() : []\n const padTop = virtualRows[0]?.start ?? 0\n const lastRow = virtualRows.at(-1)\n const padBottom = lastRow ? rowVirtualizer.getTotalSize() - lastRow.end : 0\n return {\n windowed,\n virtualRows,\n padTop,\n padBottom,\n measureElement: rowVirtualizer.measureElement,\n }\n}\n\nexport function SubResourcesSection({\n subResources,\n parentSlug,\n parent,\n initialSearch,\n childrenLabel,\n}: SubResourcesSectionProps) {\n // `useSlotFilled`, not \"render it and see\": the column is layout, so the\n // header has to be decided before any cell renders.\n //\n // ONE flag for the header, the cells and the empty row's colSpan. Deriving it\n // separately per site is how a header appears over a column with no cells —\n // the parent check only guarded the cells at first, and the header rendered\n // anyway. The scope lets a plugin that serves only some parents keep the\n // column off every other parent's table; no parent means nothing to decide.\n const showAdminColumn = useSlotFilled(\n 'resourceSubResourceRowActions',\n parent ? { parent } : undefined,\n )\n // Same rule from the data side: only a family of AWS accounts has an account\n // id to show. Decided on the UNFILTERED list, so typing a filter that happens\n // to hide every account does not make the column jump.\n const showAccountColumn = useMemo(\n () => subResources.some((sr) => awsAccountIdOf(sr) !== undefined),\n [subResources],\n )\n const user = usePluginUser()\n // `?sub=<slug>` — the one sub-resource the user asked for, by clicking its row\n // in the search results. It wins over the query-seeded filter below: a query\n // like \"project\" matches every child, so seeding with it would bury the row\n // that was actually clicked.\n const search_ = useSearch({ strict: false })\n const selectedSubSlug = search_.sub\n const selectedSub = useMemo(\n () => subResources.find((sr) => sr.slug === selectedSubSlug) ?? null,\n [subResources, selectedSubSlug],\n )\n const navigate = useNavigate()\n\n // The row to mark as \"this one\". It outlives `?sub=` so clearing the filter\n // brings the siblings back without losing the user's place.\n const [highlightSlug, setHighlightSlug] = useState(selectedSubSlug)\n useEffect(() => {\n if (selectedSubSlug) setHighlightSlug(selectedSubSlug)\n }, [selectedSubSlug])\n\n // The search input only mounts once the selection is gone, so focus it then.\n const searchInputRef = useRef<HTMLInputElement>(null)\n const focusSearchAfterClearRef = useRef(false)\n useEffect(() => {\n if (!selectedSub && focusSearchAfterClearRef.current) {\n focusSearchAfterClearRef.current = false\n searchInputRef.current?.focus()\n }\n }, [selectedSub])\n\n const clearSelection = () => {\n focusSearchAfterClearRef.current = true\n // A navigation, not a replace, so Back restores the single-row view.\n void navigate({\n to: '.',\n search: (prev) => ({ ...prev, sub: undefined }),\n })\n }\n\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 /**\n * The input stays bound to `search`; the TABLE filters on this.\n *\n * Rebuilding the row list is the expensive half of a keystroke — on a large\n * entry it mounts and unmounts hundreds of rows — and doing it in the same\n * render as the input makes the caret wait for it. `useDeferredValue` splits\n * them: React commits the typed character immediately and re-renders the\n * table at low priority, interruptibly. Keep typing and the superseded\n * low-priority render is abandoned rather than finished and thrown away, so\n * a burst costs roughly one filter pass instead of one per character.\n *\n * This only works because the row is memoized. Deferring tells React it MAY\n * skip the expensive subtree between keystrokes; `memo` is what lets it\n * actually skip anything.\n *\n * Not a debounce: there is no fixed delay to tune, nothing is thrown away on\n * a fast typist, and the final result is never late — React yields to input\n * instead of waiting on a timer.\n */\n const deferredSearch = useDeferredValue(search)\n // True while the table is a render behind the box. Used only to dim it, so\n // stale rows read as stale instead of as the answer.\n const isFilterPending = search !== deferredSearch\n\n // useState only reads its argument on the first render, so without this the\n // filter kept the query from whenever this panel first mounted — searching\n // again with the panel already open left the old term in the box.\n useEffect(() => setSearch(seededSearch), [seededSearch])\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 if (selectedSub) return [selectedSub]\n\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (deferredSearch.trim()) {\n const q = deferredSearch.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 awsAccountIdOf(sr)?.includes(q) === true,\n )\n }\n\n return result\n }, [subResources, deferredSearch, tierFilter, selectedSub])\n\n const scrollRef = useRef<HTMLDivElement>(null)\n const { windowed, virtualRows, padTop, padBottom, measureElement } =\n useRowWindow(filtered, scrollRef)\n // Fixed layout for any table big enough to be windowed. With auto layout a\n // column is as wide as its widest MOUNTED cell, so while scrolling a\n // windowed body the columns resized under the cursor — measured at 264px.\n // Keyed on the unfiltered size so a big table keeps one layout while it is\n // filtered below the threshold, rather than jumping as the user types.\n const fixedLayout = subResources.length > VIRTUALIZE_ABOVE\n // Name, Tier, Owner, Approvers, then the two optional columns.\n const columnCount =\n 4 + (showAccountColumn ? 1 : 0) + (showAdminColumn ? 1 : 0)\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 {/* `data-testid`, because the label is now deployment-defined: the test\n kit used to locate this heading by matching the literal string\n \"Sub-Resources (n of m)\", which silently stopped finding it the\n moment a catalog named its children something else. */}\n <div\n data-testid=\"sub-resources-heading\"\n className=\"text-sm font-medium\"\n >\n {/* Each site keeps the exact wording it had before `childrenLabel`\n existed, so a deployment that names nothing renders identically —\n the heading and the placeholder never agreed, and quietly making\n them agree would change every existing catalog. */}\n {childrenLabel ?? 'Sub-Resources'} ({filtered.length} of{' '}\n {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n {selectedSub ? (\n // `?sub=` wins over the search box, so show it where the box would be\n // and make it dismissible — otherwise the only way out is the URL.\n <div className=\"flex flex-1 items-center\">\n <Badge variant=\"secondary\" className=\"h-9 gap-1 pl-3 pr-1 text-sm\">\n Showing: {selectedSub.displayName}\n <button\n type=\"button\"\n aria-label=\"Clear sub-resource filter\"\n onClick={clearSelection}\n className=\"rounded-sm p-1 hover:bg-muted-foreground/20\"\n >\n <X className=\"size-4\" />\n </button>\n </Badge>\n </div>\n ) : (\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n ref={searchInputRef}\n // Verbatim, not lower-cased: the deployment chose the casing and\n // an acronym does not survive it — \"AWS Accounts\" became\n // \"Search aws accounts…\". The no-label default stays lowercase\n // because that is the wording this placeholder already had.\n placeholder={`Search ${childrenLabel ?? '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 )}\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 The vertical scroll sits on the Table's own wrapper rather than here,\n so the sticky header below pins to the element that actually scrolls —\n see the comment on `stickyHeader`. */}\n {/* Dimmed while the rows are a render behind the filter box, so stale\n rows read as stale rather than as the answer. `aria-busy` says the\n same thing to a screen reader. Opacity only — no spinner and no\n layout change, because on a fast filter this lasts one frame and\n anything heavier would strobe. */}\n <div\n className=\"rounded-lg border overflow-hidden\"\n aria-busy={isFilterPending || undefined}\n >\n <Table\n stickyHeader\n containerRef={scrollRef}\n className={`max-h-[400px] transition-opacity ${\n fixedLayout ? '[&>table]:table-fixed' : ''\n } ${isFilterPending ? 'opacity-60' : ''}`}\n >\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead className={fixedLayout ? 'w-[190px]' : undefined}>\n Owner\n </TableHead>\n <TableHead className={fixedLayout ? 'w-[230px]' : undefined}>\n Approvers\n </TableHead>\n {showAccountColumn && (\n <TableHead className=\"w-[140px]\">AWS Account</TableHead>\n )}\n {/* Only when something fills it: an empty column with a header and\n no cells is worse than no column, and the open-source build\n registers no plugins at all. */}\n {showAdminColumn && (\n <TableHead className=\"w-[168px]\">Admin</TableHead>\n )}\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={columnCount}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : windowed ? (\n <>\n {padTop > 0 && (\n <SpacerRow height={padTop} columns={columnCount} />\n )}\n {virtualRows.map((v) => {\n const sr = filtered[v.index]\n if (!sr) return null\n return (\n <SubResourceRow\n key={sr.slug}\n resource={sr}\n parentSlug={parentSlug}\n adminParent={showAdminColumn ? parent : undefined}\n showAccount={showAccountColumn}\n user={user}\n isHighlighted={sr.slug === highlightSlug}\n measureRef={measureElement}\n index={v.index}\n />\n )\n })}\n {padBottom > 0 && (\n <SpacerRow height={padBottom} columns={columnCount} />\n )}\n </>\n ) : (\n filtered.map((sr) => (\n <SubResourceRow\n key={sr.slug}\n resource={sr}\n parentSlug={parentSlug}\n // One prop for \"render the Admin cell, with this parent\".\n // `showAdminColumn` already implies `parent !== undefined`,\n // so this cannot disagree with the header or the colSpan.\n adminParent={showAdminColumn ? parent : undefined}\n showAccount={showAccountColumn}\n user={user}\n isHighlighted={sr.slug === highlightSlug}\n />\n ))\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":["_a"],"mappings":"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"}
@@ -13,7 +13,6 @@ import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
13
13
  import { PersonBadge } from "../components/PersonBadge.js";
14
14
  import { useUser } from "../../../auth/AuthContext.js";
15
15
  import "@tanstack/react-query";
16
- import "../../../auth/AuthModalContext.js";
17
16
  import { InlineEditableField } from "../components/InlineEditableField.js";
18
17
  import { MarkdownText } from "../components/MarkdownText.js";
19
18
  import { ScreenshotGallery } from "../components/ScreenshotGallery.js";