@igstack/app-catalog-frontend-core 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +0 -1
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.d.ts +1 -1
- package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +0 -1
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +0 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +0 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js +10 -4
- package/dist/esm/modules/appCatalog/ui/pages/ServiceDesksPage.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/integration/mock-backend/magazines.ts +7 -4
- package/src/__tests__/integration/serviceDesks.integration.test.ts +46 -6
- package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +7 -7
- package/src/__tests__/ui/components/header/VersionDisplay.test.tsx +1 -1
- package/src/modules/appCatalog/context/mergeFrontendBuildId.ts +1 -1
- package/src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx +15 -5
|
@@ -9,7 +9,6 @@ import "clsx";
|
|
|
9
9
|
import "tailwind-merge";
|
|
10
10
|
import "@tanstack/react-query-devtools";
|
|
11
11
|
import "next-themes";
|
|
12
|
-
import "../../auth/AuthModalContext.js";
|
|
13
12
|
import "@radix-ui/react-dialog";
|
|
14
13
|
import "lucide-react";
|
|
15
14
|
import "../../config/GlobalConfigContext.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: mergeFrontendBuildId(\n data?.versions ?? {},\n uiSettings.frontendBuildId,\n ),\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":"
|
|
1
|
+
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\nimport { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: mergeFrontendBuildId(\n data?.versions ?? {},\n uiSettings.frontendBuildId,\n ),\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":";;;;;;;;;;;;;;;;;;;AAyBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAE3C,QAAM,aAAa,cAAA;AAEnB,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,YAAW,6BAAM,cAAa,CAAA;AAAA,MAC9B;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,UAAS,6BAAM,YAAW,CAAA;AAAA,MAC1B,SAAQ,6BAAM,WAAU,CAAA;AAAA,MACxB,UAAU;AAAA,SACR,6BAAM,aAAY,CAAA;AAAA,QAClB,WAAW;AAAA,MAAA;AAAA,IACb;AAAA,IAEF;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,OAAAC,MAAA,6BAAM,aAAN,gBAAAA,IAAgB,YAAhB,gBAAAD,IAAyB,iBAAgB,SAAS;AACpD,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,wCAAM,aAAN,mBAAgB,YAAhB,mBAAyB,WAAW,CAAC;AAEzC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AppVersionInfo } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
/**
|
|
3
|
-
* Merge the (
|
|
3
|
+
* Merge the (company-agnostic) frontend build id into the backend-provided
|
|
4
4
|
* version info's `frontend` slot.
|
|
5
5
|
*
|
|
6
6
|
* The build id is baked into the deployed JS bundle at build time (e.g. a CI
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mergeFrontendBuildId.js","sources":["../../../../../src/modules/appCatalog/context/mergeFrontendBuildId.ts"],"sourcesContent":["import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'\n\n/**\n * Merge the (
|
|
1
|
+
{"version":3,"file":"mergeFrontendBuildId.js","sources":["../../../../../src/modules/appCatalog/context/mergeFrontendBuildId.ts"],"sourcesContent":["import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'\n\n/**\n * Merge the (company-agnostic) frontend build id into the backend-provided\n * version info's `frontend` slot.\n *\n * The build id is baked into the deployed JS bundle at build time (e.g. a CI\n * pipeline id via a Vite env var). We PREPEND it to the resolved frontend-core\n * npm version instead of replacing it, so the footer surfaces both:\n * `#<buildId> · <frontend-core version> (<sha>)`.\n *\n * - No build id → return versions unchanged.\n * - build id === 'local' → show `local` (local dev, no meaningful SHA/version).\n * - otherwise → prepend `#<buildId> · ` and preserve the version + sha + shaUrl.\n */\nexport function mergeFrontendBuildId(\n versions: AppVersionInfo,\n frontendBuildId: string | undefined,\n): AppVersionInfo {\n if (!frontendBuildId) return versions\n if (frontendBuildId === 'local') {\n return { ...versions, frontend: { displayName: 'local' } }\n }\n const be = versions.frontend\n return {\n ...versions,\n frontend: be\n ? { ...be, displayName: `#${frontendBuildId} · ${be.displayName}` }\n : { displayName: `#${frontendBuildId}` },\n }\n}\n"],"names":[],"mappings":"AAeO,SAAS,qBACd,UACA,iBACgB;AAChB,MAAI,CAAC,gBAAiB,QAAO;AAC7B,MAAI,oBAAoB,SAAS;AAC/B,WAAO,EAAE,GAAG,UAAU,UAAU,EAAE,aAAa,UAAQ;AAAA,EACzD;AACA,QAAM,KAAK,SAAS;AACpB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU,KACN,EAAE,GAAG,IAAI,aAAa,IAAI,eAAe,MAAM,GAAG,WAAW,OAC7D,EAAE,aAAa,IAAI,eAAe,GAAA;AAAA,EAAG;AAE7C;"}
|
|
@@ -9,7 +9,6 @@ import "clsx";
|
|
|
9
9
|
import "tailwind-merge";
|
|
10
10
|
import "@tanstack/react-query-devtools";
|
|
11
11
|
import "next-themes";
|
|
12
|
-
import "../../../auth/AuthModalContext.js";
|
|
13
12
|
import "@radix-ui/react-dialog";
|
|
14
13
|
import "../../../config/GlobalConfigContext.js";
|
|
15
14
|
import "../../../pluginCore/PluginManagerContext.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\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\n const email = person?.email\n\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={email ? `${displayName} (${email})` : displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n {email && <CopyEmailButton email={email} />}\n </Badge>\n )\n}\n\nfunction CopyEmailButton({ email }: { email: 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 (e: React.MouseEvent) => {\n e.stopPropagation()\n navigator.clipboard.writeText(email)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [email],\n )\n\n return (\n <button\n onClick={handleCopy}\n className=\"ml-0.5 hover:text-primary transition-colors\"\n title={copied ? 'Copied!' : `Copy ${email}`}\n type=\"button\"\n >\n {copied ? (\n <Check className=\"size-3 text-green-600\" />\n ) : (\n <Copy className=\"size-3\" />\n )}\n </button>\n )\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\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\n const email = person?.email\n\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={email ? `${displayName} (${email})` : displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n {email && <CopyEmailButton email={email} />}\n </Badge>\n )\n}\n\nfunction CopyEmailButton({ email }: { email: 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 (e: React.MouseEvent) => {\n e.stopPropagation()\n navigator.clipboard.writeText(email)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [email],\n )\n\n return (\n <button\n onClick={handleCopy}\n className=\"ml-0.5 hover:text-primary transition-colors\"\n title={copied ? 'Copied!' : `Copy ${email}`}\n type=\"button\"\n >\n {copied ? (\n <Check className=\"size-3 text-green-600\" />\n ) : (\n <Copy className=\"size-3\" />\n )}\n </button>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;AAUO,SAAS,YAAY,EAAE,QAA0B;AACtD,QAAM,EAAE,QAAA,IAAY,qBAAA;AACpB,QAAM,SAAS,gBAAgB,SAAS,IAAI;AAE5C,QAAM,cAAc,SAChB,GAAG,OAAO,SAAS,IAAI,OAAO,QAAQ,GAAG,KAAA,KAAU,OACnD;AAEJ,QAAM,QAAQ,iCAAQ;AAEtB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAU;AAAA,MACV,OAAO,QAAQ,GAAG,WAAW,KAAK,KAAK,MAAM;AAAA,MAE7C,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,QACxB;AAAA,QACA,SAAS,oBAAC,iBAAA,EAAgB,MAAA,CAAc;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG/C;AAEA,SAAS,gBAAgB,EAAE,SAA4B;AACrD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA8B,IAAI;AAErD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AAAA,IACzD;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,aAAa;AAAA,IACjB,CAAC,MAAwB;AACvB,QAAE,gBAAA;AACF,gBAAU,UAAU,UAAU,KAAK;AACnC,gBAAU,IAAI;AACd,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AACvD,iBAAW,UAAU,WAAW,MAAM,UAAU,KAAK,GAAG,GAAI;AAAA,IAC9D;AAAA,IACA,CAAC,KAAK;AAAA,EAAA;AAGR,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAS;AAAA,MACT,WAAU;AAAA,MACV,OAAO,SAAS,YAAY,QAAQ,KAAK;AAAA,MACzC,MAAK;AAAA,MAEJ,UAAA,6BACE,OAAA,EAAM,WAAU,yBAAwB,IAEzC,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAIjC;"}
|
|
@@ -13,7 +13,6 @@ import "clsx";
|
|
|
13
13
|
import "tailwind-merge";
|
|
14
14
|
import "@tanstack/react-query-devtools";
|
|
15
15
|
import "next-themes";
|
|
16
|
-
import "../../../auth/AuthModalContext.js";
|
|
17
16
|
import "@radix-ui/react-dialog";
|
|
18
17
|
import "../../../config/GlobalConfigContext.js";
|
|
19
18
|
import "../../../pluginCore/PluginManagerContext.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { 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 } 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'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\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}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n const [search, setSearch] = useState('')\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 }\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 </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={4}\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 {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 </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 } 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'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\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}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n const [search, setSearch] = useState('')\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 }\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 </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={4}\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 {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 </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -11,7 +11,6 @@ import "clsx";
|
|
|
11
11
|
import "tailwind-merge";
|
|
12
12
|
import "@tanstack/react-query-devtools";
|
|
13
13
|
import "next-themes";
|
|
14
|
-
import "../../../auth/AuthModalContext.js";
|
|
15
14
|
import "@radix-ui/react-dialog";
|
|
16
15
|
import "../../../config/GlobalConfigContext.js";
|
|
17
16
|
import "../../../pluginCore/PluginManagerContext.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TierVariantsSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/TierVariantsSection.tsx"],"sourcesContent":["import type {\n AppAccessRequest,\n AppApprovalMethod,\n TierVariant,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, ExternalLinkIcon, Settings, Users } from 'lucide-react'\nimport { useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Badge } from '~/ui/badge'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { PersonBadge } from './PersonBadge'\n\ninterface TierVariantsSectionProps {\n tiers: TierVariant[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nfunction getAccessIcon(type: string): React.ReactNode {\n switch (type) {\n case 'service':\n return <Bot className=\"size-4 text-primary shrink-0\" />\n case 'personTeam':\n return <Users className=\"size-4 text-primary shrink-0\" />\n default:\n return <Settings className=\"size-4 text-primary shrink-0\" />\n }\n}\n\n/** Compact inline access detail for a tier row */\nfunction TierAccessDetail({\n accessRequest,\n methods,\n}: {\n accessRequest: AppAccessRequest\n methods: AppApprovalMethod[]\n}) {\n const [expanded, setExpanded] = useState(false)\n const method = methods.find(\n (m) => m.slug === accessRequest.approvalMethodSlug,\n )\n\n const hasExtra =\n accessRequest.comments ||\n accessRequest.urls?.length ||\n accessRequest.approverPersonSlugs?.length ||\n accessRequest.postApprovalInstructions\n\n return (\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-1.5\">\n {method && getAccessIcon(method.type)}\n {method?.type === 'service' && method.config.url ? (\n <a\n href={method.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {method.displayName}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-sm\">\n {method?.displayName ?? accessRequest.approvalMethodSlug}\n </span>\n )}\n {hasExtra && (\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"text-xs text-muted-foreground hover:text-primary ml-1\"\n >\n {expanded ? 'less' : 'more...'}\n </button>\n )}\n </div>\n {expanded && (\n <div className=\"pl-5 space-y-1.5 text-xs\">\n {accessRequest.comments && (\n <div className=\"text-muted-foreground prose prose-xs max-w-none\">\n <ReactMarkdown>{accessRequest.comments}</ReactMarkdown>\n </div>\n )}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div className=\"flex flex-col gap-0.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline inline-flex items-center gap-1\"\n >\n {urlObj.label || urlObj.url.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ))}\n </div>\n )}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nexport function TierVariantsSection({ tiers }: TierVariantsSectionProps) {\n const { approvalMethods } = useAppCatalogContext()\n\n if (tiers.length === 0) return null\n\n return (\n <div className=\"space-y-2\">\n <div className=\"text-sm font-medium\">Environment Tiers</div>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-[100px]\">Tier</TableHead>\n <TableHead>Name</TableHead>\n <TableHead>URL</TableHead>\n <TableHead>Access</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tiers.map((tier) => (\n <TableRow key={tier.tierSlug}>\n <TableCell>\n <Badge\n variant={getTierBadgeVariant(tier.tierSlug)}\n className={getTierBadgeClassName(tier.tierSlug)}\n >\n {getTierDisplayLabel(tier.tierSlug)}\n </Badge>\n </TableCell>\n <TableCell className=\"font-medium\">\n {tier.displayName ?? tier.tierSlug}\n </TableCell>\n <TableCell>\n {tier.appUrl ? (\n <a\n href={tier.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {tier.appUrl.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {tier.accessRequest ? (\n <TierAccessDetail\n accessRequest={tier.accessRequest}\n methods={approvalMethods}\n />\n ) : (\n <span className=\"text-muted-foreground\">Same as app</span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TierVariantsSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/TierVariantsSection.tsx"],"sourcesContent":["import type {\n AppAccessRequest,\n AppApprovalMethod,\n TierVariant,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, ExternalLinkIcon, Settings, Users } from 'lucide-react'\nimport { useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Badge } from '~/ui/badge'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { PersonBadge } from './PersonBadge'\n\ninterface TierVariantsSectionProps {\n tiers: TierVariant[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nfunction getAccessIcon(type: string): React.ReactNode {\n switch (type) {\n case 'service':\n return <Bot className=\"size-4 text-primary shrink-0\" />\n case 'personTeam':\n return <Users className=\"size-4 text-primary shrink-0\" />\n default:\n return <Settings className=\"size-4 text-primary shrink-0\" />\n }\n}\n\n/** Compact inline access detail for a tier row */\nfunction TierAccessDetail({\n accessRequest,\n methods,\n}: {\n accessRequest: AppAccessRequest\n methods: AppApprovalMethod[]\n}) {\n const [expanded, setExpanded] = useState(false)\n const method = methods.find(\n (m) => m.slug === accessRequest.approvalMethodSlug,\n )\n\n const hasExtra =\n accessRequest.comments ||\n accessRequest.urls?.length ||\n accessRequest.approverPersonSlugs?.length ||\n accessRequest.postApprovalInstructions\n\n return (\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-1.5\">\n {method && getAccessIcon(method.type)}\n {method?.type === 'service' && method.config.url ? (\n <a\n href={method.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {method.displayName}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-sm\">\n {method?.displayName ?? accessRequest.approvalMethodSlug}\n </span>\n )}\n {hasExtra && (\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"text-xs text-muted-foreground hover:text-primary ml-1\"\n >\n {expanded ? 'less' : 'more...'}\n </button>\n )}\n </div>\n {expanded && (\n <div className=\"pl-5 space-y-1.5 text-xs\">\n {accessRequest.comments && (\n <div className=\"text-muted-foreground prose prose-xs max-w-none\">\n <ReactMarkdown>{accessRequest.comments}</ReactMarkdown>\n </div>\n )}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div className=\"flex flex-col gap-0.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline inline-flex items-center gap-1\"\n >\n {urlObj.label || urlObj.url.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ))}\n </div>\n )}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nexport function TierVariantsSection({ tiers }: TierVariantsSectionProps) {\n const { approvalMethods } = useAppCatalogContext()\n\n if (tiers.length === 0) return null\n\n return (\n <div className=\"space-y-2\">\n <div className=\"text-sm font-medium\">Environment Tiers</div>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-[100px]\">Tier</TableHead>\n <TableHead>Name</TableHead>\n <TableHead>URL</TableHead>\n <TableHead>Access</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tiers.map((tier) => (\n <TableRow key={tier.tierSlug}>\n <TableCell>\n <Badge\n variant={getTierBadgeVariant(tier.tierSlug)}\n className={getTierBadgeClassName(tier.tierSlug)}\n >\n {getTierDisplayLabel(tier.tierSlug)}\n </Badge>\n </TableCell>\n <TableCell className=\"font-medium\">\n {tier.displayName ?? tier.tierSlug}\n </TableCell>\n <TableCell>\n {tier.appUrl ? (\n <a\n href={tier.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary hover:underline inline-flex items-center gap-1\"\n >\n {tier.appUrl.replace(/^https?:\\/\\//, '')}\n <ExternalLinkIcon className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n <TableCell>\n {tier.accessRequest ? (\n <TierAccessDetail\n accessRequest={tier.accessRequest}\n methods={approvalMethods}\n />\n ) : (\n <span className=\"text-muted-foreground\">Same as app</span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -19,8 +19,10 @@ function ServiceDesksPage() {
|
|
|
19
19
|
/* @__PURE__ */ jsx(InputGroup, { className: "max-w-sm", children: /* @__PURE__ */ jsx(
|
|
20
20
|
InputGroupInput,
|
|
21
21
|
{
|
|
22
|
+
autoFocus: true,
|
|
22
23
|
value: search,
|
|
23
24
|
onChange: (e) => setSearch(e.target.value),
|
|
25
|
+
onFocus: (e) => e.target.select(),
|
|
24
26
|
placeholder: "Search service desks by name…",
|
|
25
27
|
"aria-label": "Search service desks"
|
|
26
28
|
}
|
|
@@ -36,19 +38,23 @@ function ServiceDesksPage() {
|
|
|
36
38
|
] }) }),
|
|
37
39
|
/* @__PURE__ */ jsx(TableBody, { children: desks.map((desk) => {
|
|
38
40
|
const url = desk.config.url;
|
|
41
|
+
const description = desk.config.description;
|
|
39
42
|
return /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
40
|
-
/* @__PURE__ */
|
|
43
|
+
/* @__PURE__ */ jsxs(TableCell, { className: "font-medium align-top", children: [
|
|
44
|
+
/* @__PURE__ */ jsx("span", { "data-testid": "service-desk-name", children: desk.displayName }),
|
|
45
|
+
description && /* @__PURE__ */ jsx("span", { className: "block text-xs font-normal text-muted-foreground", children: description })
|
|
46
|
+
] }),
|
|
41
47
|
/* @__PURE__ */ jsx(TableCell, { children: url ? /* @__PURE__ */ jsxs(
|
|
42
48
|
"a",
|
|
43
49
|
{
|
|
44
50
|
href: url,
|
|
45
51
|
target: "_blank",
|
|
46
52
|
rel: "noopener noreferrer",
|
|
47
|
-
className: "inline-flex items-center gap-1 text-primary hover:underline",
|
|
53
|
+
className: "inline-flex items-center gap-1 text-primary hover:underline break-all",
|
|
48
54
|
title: url,
|
|
49
55
|
children: [
|
|
50
|
-
"
|
|
51
|
-
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3" })
|
|
56
|
+
url.replace(/^https?:\/\//, ""),
|
|
57
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" })
|
|
52
58
|
]
|
|
53
59
|
}
|
|
54
60
|
) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" }) })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServiceDesksPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx"],"sourcesContent":["import { ExternalLink } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { InputGroup, InputGroupInput } from '~/ui/input-group'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\n\n/**\n * Service Desks view (#9): a searchable table of all service-desk approval\n * methods (type === 'service'), each with a link that opens its portal in a new\n * tab. Data rides in on the existing app-catalog query (context.approvalMethods).\n */\nexport function ServiceDesksPage() {\n const { approvalMethods } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n\n const desks = useMemo(() => {\n const services = approvalMethods.filter((m) => m.type === 'service')\n const q = search.trim().toLowerCase()\n const filtered = q\n ? services.filter((m) => m.displayName.toLowerCase().includes(q))\n : services\n return [...filtered].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n }, [approvalMethods, search])\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0 gap-4\">\n <InputGroup className=\"max-w-sm\">\n <InputGroupInput\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search service desks by name…\"\n aria-label=\"Search service desks\"\n />\n </InputGroup>\n\n <div className=\"flex-1 min-h-0 overflow-y-auto\">\n {desks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No service desks found{search && ` for \"${search}\"`}.\n </p>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Service Desk</TableHead>\n <TableHead>Link</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {desks.map((desk) => {\n const url = desk.config.url\n return (\n <TableRow key={desk.slug}>\n <TableCell className=\"font-medium\">\n {desk.displayName}\n </TableCell>\n <TableCell>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 text-primary hover:underline\"\n title={url}\n >\n
|
|
1
|
+
{"version":3,"file":"ServiceDesksPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/ServiceDesksPage.tsx"],"sourcesContent":["import { ExternalLink } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { InputGroup, InputGroupInput } from '~/ui/input-group'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\n\n/**\n * Service Desks view (#9): a searchable table of all service-desk approval\n * methods (type === 'service'), each with a link that opens its portal in a new\n * tab. Data rides in on the existing app-catalog query (context.approvalMethods).\n */\nexport function ServiceDesksPage() {\n const { approvalMethods } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n\n const desks = useMemo(() => {\n const services = approvalMethods.filter((m) => m.type === 'service')\n const q = search.trim().toLowerCase()\n const filtered = q\n ? services.filter((m) => m.displayName.toLowerCase().includes(q))\n : services\n return [...filtered].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n }, [approvalMethods, search])\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0 gap-4\">\n <InputGroup className=\"max-w-sm\">\n <InputGroupInput\n autoFocus\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n onFocus={(e) => e.target.select()}\n placeholder=\"Search service desks by name…\"\n aria-label=\"Search service desks\"\n />\n </InputGroup>\n\n <div className=\"flex-1 min-h-0 overflow-y-auto\">\n {desks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No service desks found{search && ` for \"${search}\"`}.\n </p>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Service Desk</TableHead>\n <TableHead>Link</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {desks.map((desk) => {\n const url = desk.config.url\n const description = desk.config.description\n return (\n <TableRow key={desk.slug}>\n <TableCell className=\"font-medium align-top\">\n <span data-testid=\"service-desk-name\">\n {desk.displayName}\n </span>\n {description && (\n <span className=\"block text-xs font-normal text-muted-foreground\">\n {description}\n </span>\n )}\n </TableCell>\n <TableCell>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 text-primary hover:underline break-all\"\n title={url}\n >\n {url.replace(/^https?:\\/\\//, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </Table>\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;AAkBO,SAAS,mBAAmB;AACjC,QAAM,EAAE,gBAAA,IAAoB,qBAAA;AAC5B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AAEvC,QAAM,QAAQ,QAAQ,MAAM;AAC1B,UAAM,WAAW,gBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,SAAS;AACnE,UAAM,IAAI,OAAO,KAAA,EAAO,YAAA;AACxB,UAAM,WAAW,IACb,SAAS,OAAO,CAAC,MAAM,EAAE,YAAY,YAAA,EAAc,SAAS,CAAC,CAAC,IAC9D;AACJ,WAAO,CAAC,GAAG,QAAQ,EAAE;AAAA,MAAK,CAAC,GAAG,MAC5B,EAAE,YAAY,cAAc,EAAE,WAAW;AAAA,IAAA;AAAA,EAE7C,GAAG,CAAC,iBAAiB,MAAM,CAAC;AAE5B,SACE,qBAAC,OAAA,EAAI,WAAU,sCACb,UAAA;AAAA,IAAA,oBAAC,YAAA,EAAW,WAAU,YACpB,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAS;AAAA,QACT,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,UAAU,EAAE,OAAO,KAAK;AAAA,QACzC,SAAS,CAAC,MAAM,EAAE,OAAO,OAAA;AAAA,QACzB,aAAY;AAAA,QACZ,cAAW;AAAA,MAAA;AAAA,IAAA,GAEf;AAAA,IAEA,oBAAC,OAAA,EAAI,WAAU,kCACZ,UAAA,MAAM,WAAW,IAChB,qBAAC,KAAA,EAAE,WAAU,iCAAgC,UAAA;AAAA,MAAA;AAAA,MACpB,UAAU,SAAS,MAAM;AAAA,MAAI;AAAA,IAAA,EAAA,CACtD,yBAEC,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,aAAA,EACC,+BAAC,UAAA,EACC,UAAA;AAAA,QAAA,oBAAC,aAAU,UAAA,eAAA,CAAY;AAAA,QACvB,oBAAC,aAAU,UAAA,OAAA,CAAI;AAAA,MAAA,EAAA,CACjB,EAAA,CACF;AAAA,MACA,oBAAC,WAAA,EACE,UAAA,MAAM,IAAI,CAAC,SAAS;AACnB,cAAM,MAAM,KAAK,OAAO;AACxB,cAAM,cAAc,KAAK,OAAO;AAChC,oCACG,UAAA,EACC,UAAA;AAAA,UAAA,qBAAC,WAAA,EAAU,WAAU,yBACnB,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,eAAY,qBACf,UAAA,KAAK,aACR;AAAA,YACC,eACC,oBAAC,QAAA,EAAK,WAAU,mDACb,UAAA,YAAA,CACH;AAAA,UAAA,GAEJ;AAAA,UACA,oBAAC,aACE,UAAA,MACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAM;AAAA,cACN,QAAO;AAAA,cACP,KAAI;AAAA,cACJ,WAAU;AAAA,cACV,OAAO;AAAA,cAEN,UAAA;AAAA,gBAAA,IAAI,QAAQ,gBAAgB,EAAE;AAAA,gBAC/B,oBAAC,cAAA,EAAa,WAAU,kBAAA,CAAkB;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA,IAG5C,oBAAC,QAAA,EAAK,WAAU,yBAAwB,eAAC,EAAA,CAE7C;AAAA,QAAA,EAAA,GA1Ba,KAAK,IA2BpB;AAAA,MAEJ,CAAC,EAAA,CACH;AAAA,IAAA,EAAA,CACF,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -134,8 +134,8 @@
|
|
|
134
134
|
"vite-plugin-static-copy": "^3.1.4",
|
|
135
135
|
"vite-plugin-svgr": "^4.2.0",
|
|
136
136
|
"vitest": "^4.1.2",
|
|
137
|
-
"@igstack/app-catalog-
|
|
138
|
-
"@igstack/app-catalog-
|
|
137
|
+
"@igstack/app-catalog-backend-core": "0.8.1",
|
|
138
|
+
"@igstack/app-catalog-shared-core": "0.8.1"
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
141
141
|
"react": "19.1.2",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"vite": "^6.3.5",
|
|
145
145
|
"vite-plugin-svgr": "^4.2.0"
|
|
146
146
|
},
|
|
147
|
-
"gitHead": "
|
|
147
|
+
"gitHead": "16c12c4d457cd8ca88e30ffba17e8b6c23e4d527",
|
|
148
148
|
"scripts": {
|
|
149
149
|
"build": "vite build",
|
|
150
150
|
"build:lenient": "vite build --mode lenient",
|
|
@@ -36,13 +36,16 @@ function fullMagazine(
|
|
|
36
36
|
slug: 'it-helpdesk',
|
|
37
37
|
type: 'service',
|
|
38
38
|
displayName: 'IT Help Desk',
|
|
39
|
-
config: {
|
|
39
|
+
config: {
|
|
40
|
+
url: 'https://helpdesk.example.com',
|
|
41
|
+
description: 'IT Infrastructure support desk',
|
|
42
|
+
},
|
|
40
43
|
})
|
|
41
44
|
backendCfg.withApprovalMethod({
|
|
42
|
-
slug: '
|
|
45
|
+
slug: 'ops-helpdesk',
|
|
43
46
|
type: 'service',
|
|
44
|
-
displayName: '
|
|
45
|
-
config: { url: 'https://
|
|
47
|
+
displayName: 'Ops Helpdesk',
|
|
48
|
+
config: { url: 'https://opsdesk.example.com' },
|
|
46
49
|
})
|
|
47
50
|
const managerApproval = backendCfg.withApprovalMethod({
|
|
48
51
|
slug: 'manager-approval',
|
|
@@ -8,7 +8,7 @@ import { magazine } from './mock-backend/magazines'
|
|
|
8
8
|
|
|
9
9
|
// #9: a header toggle (Apps | Service Desks) + a /service-desks route showing a
|
|
10
10
|
// searchable table of all type:'service' approval methods with open links.
|
|
11
|
-
// magazine.full() seeds two service desks (IT Help Desk,
|
|
11
|
+
// magazine.full() seeds two service desks (IT Help Desk, Ops Helpdesk) and
|
|
12
12
|
// two custom methods (Manager Approval, Self-Service) which must NOT appear.
|
|
13
13
|
|
|
14
14
|
function serviceDeskTable(): HTMLElement {
|
|
@@ -19,7 +19,10 @@ function serviceDeskTable(): HTMLElement {
|
|
|
19
19
|
function deskNames(): string[] {
|
|
20
20
|
const rows = within(serviceDeskTable()).getAllByRole('row')
|
|
21
21
|
return rows
|
|
22
|
-
.map(
|
|
22
|
+
.map(
|
|
23
|
+
(r) =>
|
|
24
|
+
within(r).queryByTestId('service-desk-name')?.textContent.trim() ?? '',
|
|
25
|
+
)
|
|
23
26
|
.filter(Boolean)
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -35,7 +38,7 @@ describe('Service Desks view (#9)', () => {
|
|
|
35
38
|
|
|
36
39
|
const names = deskNames()
|
|
37
40
|
expect(names).toContain('IT Help Desk')
|
|
38
|
-
expect(names).toContain('
|
|
41
|
+
expect(names).toContain('Ops Helpdesk')
|
|
39
42
|
// custom-type methods are not service desks
|
|
40
43
|
expect(names).not.toContain('Manager Approval')
|
|
41
44
|
expect(names).not.toContain('Self-Service')
|
|
@@ -47,6 +50,34 @@ describe('Service Desks view (#9)', () => {
|
|
|
47
50
|
const link = within(itRow).getByRole('link')
|
|
48
51
|
expect(link).toHaveAttribute('href', 'https://helpdesk.example.com')
|
|
49
52
|
expect(link).toHaveAttribute('target', '_blank')
|
|
53
|
+
// The link shows the full URL without the scheme, not generic "Open" text.
|
|
54
|
+
expect(link).toHaveTextContent('helpdesk.example.com')
|
|
55
|
+
expect(link).not.toHaveTextContent('Open')
|
|
56
|
+
expect(link).not.toHaveTextContent('https://')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('shows the service desk description as subtext when present', async () => {
|
|
60
|
+
await given(magazine.full(), {
|
|
61
|
+
initialRoute: '/service-desks',
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
expect(screen.getByLabelText('Search service desks')).toBeInTheDocument()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// IT Help Desk has a description; it renders in the same name cell as subtext.
|
|
69
|
+
const itRow = within(serviceDeskTable())
|
|
70
|
+
.getAllByRole('row')
|
|
71
|
+
.find((r) => r.textContent.includes('IT Help Desk'))!
|
|
72
|
+
const nameCell = within(itRow).getAllByRole('cell')[0]
|
|
73
|
+
expect(nameCell).toHaveTextContent('IT Infrastructure support desk')
|
|
74
|
+
|
|
75
|
+
// Ops Helpdesk has no description — its name cell shows only the name.
|
|
76
|
+
const uxRow = within(serviceDeskTable())
|
|
77
|
+
.getAllByRole('row')
|
|
78
|
+
.find((r) => r.textContent.includes('Ops Helpdesk'))!
|
|
79
|
+
const uxNameCell = within(uxRow).getAllByRole('cell')[0]!
|
|
80
|
+
expect(uxNameCell.textContent.trim()).toBe('Ops Helpdesk')
|
|
50
81
|
})
|
|
51
82
|
|
|
52
83
|
it('filters the desks by search', async () => {
|
|
@@ -55,13 +86,13 @@ describe('Service Desks view (#9)', () => {
|
|
|
55
86
|
initialRoute: '/service-desks',
|
|
56
87
|
})
|
|
57
88
|
|
|
58
|
-
await waitFor(() => expect(deskNames()).toContain('
|
|
89
|
+
await waitFor(() => expect(deskNames()).toContain('Ops Helpdesk'))
|
|
59
90
|
|
|
60
|
-
await user.type(screen.getByLabelText('Search service desks'), '
|
|
91
|
+
await user.type(screen.getByLabelText('Search service desks'), 'Ops')
|
|
61
92
|
|
|
62
93
|
await waitFor(() => {
|
|
63
94
|
const names = deskNames()
|
|
64
|
-
expect(names).toContain('
|
|
95
|
+
expect(names).toContain('Ops Helpdesk')
|
|
65
96
|
expect(names).not.toContain('IT Help Desk')
|
|
66
97
|
})
|
|
67
98
|
})
|
|
@@ -82,4 +113,13 @@ describe('Service Desks view (#9)', () => {
|
|
|
82
113
|
})
|
|
83
114
|
expect(screen.getByLabelText('Search service desks')).toBeInTheDocument()
|
|
84
115
|
})
|
|
116
|
+
|
|
117
|
+
it('autofocuses the search input when the view loads (parity with Apps)', async () => {
|
|
118
|
+
await given(magazine.full(), {
|
|
119
|
+
initialRoute: '/service-desks',
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const search = await screen.findByLabelText('Search service desks')
|
|
123
|
+
await waitFor(() => expect(search).toHaveFocus())
|
|
124
|
+
})
|
|
85
125
|
})
|
|
@@ -50,14 +50,14 @@ describe('searchResources', () => {
|
|
|
50
50
|
describe('child resource search', () => {
|
|
51
51
|
const childResources: Resource[] = [
|
|
52
52
|
makeChildResource({
|
|
53
|
-
slug: 'aws-
|
|
54
|
-
displayName: '
|
|
53
|
+
slug: 'aws-acme-pipelines-dev',
|
|
54
|
+
displayName: 'acme-pipelines-biomarkers-ici-dev',
|
|
55
55
|
parentSlug: 'aws-console',
|
|
56
|
-
aliases: ['
|
|
56
|
+
aliases: ['000000000001'],
|
|
57
57
|
}),
|
|
58
58
|
makeChildResource({
|
|
59
|
-
slug: 'aws-
|
|
60
|
-
displayName: '
|
|
59
|
+
slug: 'aws-acme-infosec-dev',
|
|
60
|
+
displayName: 'acme-infosec-dev',
|
|
61
61
|
parentSlug: 'aws-console',
|
|
62
62
|
aliases: [],
|
|
63
63
|
}),
|
|
@@ -72,13 +72,13 @@ describe('searchResources', () => {
|
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
it('finds app by child resource alias (account ID)', () => {
|
|
75
|
-
const results = searchResources(allResources, '
|
|
75
|
+
const results = searchResources(allResources, '000000000001')
|
|
76
76
|
expect(results).toHaveLength(1)
|
|
77
77
|
expect(results[0]!.slug).toBe('aws-console')
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
it('does not match child resources when none provided', () => {
|
|
81
|
-
const results = searchResources(apps, '
|
|
81
|
+
const results = searchResources(apps, '000000000001')
|
|
82
82
|
expect(results).toHaveLength(0)
|
|
83
83
|
})
|
|
84
84
|
|
|
@@ -27,7 +27,7 @@ describe('VersionDisplay footer', () => {
|
|
|
27
27
|
'href',
|
|
28
28
|
'https://github.com/lislon/app-catalog/commit/abc1234',
|
|
29
29
|
)
|
|
30
|
-
// Backend line carries the
|
|
30
|
+
// Backend line carries the deployment SHA suffix
|
|
31
31
|
expect(screen.getByText(/#12345 \(deadbee\)/)).toBeInTheDocument()
|
|
32
32
|
})
|
|
33
33
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Merge the (
|
|
4
|
+
* Merge the (company-agnostic) frontend build id into the backend-provided
|
|
5
5
|
* version info's `frontend` slot.
|
|
6
6
|
*
|
|
7
7
|
* The build id is baked into the deployed JS bundle at build time (e.g. a CI
|
|
@@ -35,8 +35,10 @@ export function ServiceDesksPage() {
|
|
|
35
35
|
<div className="flex flex-col flex-1 min-h-0 gap-4">
|
|
36
36
|
<InputGroup className="max-w-sm">
|
|
37
37
|
<InputGroupInput
|
|
38
|
+
autoFocus
|
|
38
39
|
value={search}
|
|
39
40
|
onChange={(e) => setSearch(e.target.value)}
|
|
41
|
+
onFocus={(e) => e.target.select()}
|
|
40
42
|
placeholder="Search service desks by name…"
|
|
41
43
|
aria-label="Search service desks"
|
|
42
44
|
/>
|
|
@@ -58,10 +60,18 @@ export function ServiceDesksPage() {
|
|
|
58
60
|
<TableBody>
|
|
59
61
|
{desks.map((desk) => {
|
|
60
62
|
const url = desk.config.url
|
|
63
|
+
const description = desk.config.description
|
|
61
64
|
return (
|
|
62
65
|
<TableRow key={desk.slug}>
|
|
63
|
-
<TableCell className="font-medium">
|
|
64
|
-
|
|
66
|
+
<TableCell className="font-medium align-top">
|
|
67
|
+
<span data-testid="service-desk-name">
|
|
68
|
+
{desk.displayName}
|
|
69
|
+
</span>
|
|
70
|
+
{description && (
|
|
71
|
+
<span className="block text-xs font-normal text-muted-foreground">
|
|
72
|
+
{description}
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
65
75
|
</TableCell>
|
|
66
76
|
<TableCell>
|
|
67
77
|
{url ? (
|
|
@@ -69,11 +79,11 @@ export function ServiceDesksPage() {
|
|
|
69
79
|
href={url}
|
|
70
80
|
target="_blank"
|
|
71
81
|
rel="noopener noreferrer"
|
|
72
|
-
className="inline-flex items-center gap-1 text-primary hover:underline"
|
|
82
|
+
className="inline-flex items-center gap-1 text-primary hover:underline break-all"
|
|
73
83
|
title={url}
|
|
74
84
|
>
|
|
75
|
-
|
|
76
|
-
<ExternalLink className="size-3" />
|
|
85
|
+
{url.replace(/^https?:\/\//, '')}
|
|
86
|
+
<ExternalLink className="size-3 shrink-0" />
|
|
77
87
|
</a>
|
|
78
88
|
) : (
|
|
79
89
|
<span className="text-muted-foreground">—</span>
|