@igstack/app-catalog-frontend-core 0.6.0 → 0.6.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/__tests__/modules/appCatalog/utils/mergeFrontendBuildId.test.d.ts +1 -0
- package/dist/esm/__tests__/ui/components/header/VersionDisplay.test.d.ts +0 -0
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +5 -8
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.d.ts +15 -0
- package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.js +15 -0
- package/dist/esm/modules/appCatalog/context/mergeFrontendBuildId.js.map +1 -0
- package/dist/esm/ui/components/header/Header.d.ts +4 -0
- package/dist/esm/ui/components/header/Header.js +40 -17
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/package.json +4 -3
- package/src/__tests__/modules/appCatalog/utils/mergeFrontendBuildId.test.ts +42 -0
- package/src/__tests__/ui/components/header/VersionDisplay.test.tsx +38 -0
- package/src/modules/appCatalog/context/AppCatalogContext.tsx +5 -11
- package/src/modules/appCatalog/context/mergeFrontendBuildId.ts +31 -0
- package/src/ui/components/header/Header.tsx +39 -16
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
File without changes
|
|
@@ -15,6 +15,7 @@ import "@radix-ui/react-tooltip";
|
|
|
15
15
|
import "../../../ui/empty.js";
|
|
16
16
|
import "@tanstack/react-router";
|
|
17
17
|
import "../../../ui/button.js";
|
|
18
|
+
import { mergeFrontendBuildId } from "./mergeFrontendBuildId.js";
|
|
18
19
|
const AppCatalogContext = createContext(void 0);
|
|
19
20
|
function AppCatalogProvider({ children }) {
|
|
20
21
|
var _a, _b;
|
|
@@ -30,14 +31,10 @@ function AppCatalogProvider({ children }) {
|
|
|
30
31
|
approvalMethods: (data == null ? void 0 : data.approvalMethods) ?? [],
|
|
31
32
|
persons: (data == null ? void 0 : data.persons) ?? [],
|
|
32
33
|
groups: (data == null ? void 0 : data.groups) ?? [],
|
|
33
|
-
versions:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
displayName: uiSettings.frontendBuildId === "local" ? "local" : `#${uiSettings.frontendBuildId}`
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
34
|
+
versions: mergeFrontendBuildId(
|
|
35
|
+
(data == null ? void 0 : data.versions) ?? {},
|
|
36
|
+
uiSettings.frontendBuildId
|
|
37
|
+
)
|
|
41
38
|
}),
|
|
42
39
|
[
|
|
43
40
|
data == null ? void 0 : data.approvalMethods,
|
|
@@ -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'\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:
|
|
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;"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AppVersionInfo } from '@igstack/app-catalog-backend-core';
|
|
2
|
+
/**
|
|
3
|
+
* Merge the (natera-agnostic) frontend build id into the backend-provided
|
|
4
|
+
* version info's `frontend` slot.
|
|
5
|
+
*
|
|
6
|
+
* The build id is baked into the deployed JS bundle at build time (e.g. a CI
|
|
7
|
+
* pipeline id via a Vite env var). We PREPEND it to the resolved frontend-core
|
|
8
|
+
* npm version instead of replacing it, so the footer surfaces both:
|
|
9
|
+
* `#<buildId> · <frontend-core version> (<sha>)`.
|
|
10
|
+
*
|
|
11
|
+
* - No build id → return versions unchanged.
|
|
12
|
+
* - build id === 'local' → show `local` (local dev, no meaningful SHA/version).
|
|
13
|
+
* - otherwise → prepend `#<buildId> · ` and preserve the version + sha + shaUrl.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mergeFrontendBuildId(versions: AppVersionInfo, frontendBuildId: string | undefined): AppVersionInfo;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function mergeFrontendBuildId(versions, frontendBuildId) {
|
|
2
|
+
if (!frontendBuildId) return versions;
|
|
3
|
+
if (frontendBuildId === "local") {
|
|
4
|
+
return { ...versions, frontend: { displayName: "local" } };
|
|
5
|
+
}
|
|
6
|
+
const be = versions.frontend;
|
|
7
|
+
return {
|
|
8
|
+
...versions,
|
|
9
|
+
frontend: be ? { ...be, displayName: `#${frontendBuildId} · ${be.displayName}` } : { displayName: `#${frontendBuildId}` }
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
mergeFrontendBuildId
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=mergeFrontendBuildId.js.map
|
|
@@ -0,0 +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 (natera-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;"}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { AppVersionInfo } from '@igstack/app-catalog-backend-core';
|
|
1
2
|
import { default as React } from 'react';
|
|
3
|
+
export declare function VersionDisplay({ versions }: {
|
|
4
|
+
versions: AppVersionInfo;
|
|
5
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
2
6
|
export interface HeaderProps {
|
|
3
7
|
middle?: React.ReactNode;
|
|
4
8
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsxs, jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Link } from "@tanstack/react-router";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { LogOut } from "lucide-react";
|
|
@@ -13,32 +13,54 @@ import { useAuthModal } from "../../../modules/auth/AuthModalContext.js";
|
|
|
13
13
|
import { Button } from "../../button.js";
|
|
14
14
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuItem } from "../../dropdown-menu.js";
|
|
15
15
|
import { Skeleton } from "../../skeleton.js";
|
|
16
|
+
function ShaSuffix({ version }) {
|
|
17
|
+
if (!version.sha) return null;
|
|
18
|
+
if (version.shaUrl) {
|
|
19
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
20
|
+
" ",
|
|
21
|
+
/* @__PURE__ */ jsxs(
|
|
22
|
+
"a",
|
|
23
|
+
{
|
|
24
|
+
href: version.shaUrl,
|
|
25
|
+
target: "_blank",
|
|
26
|
+
rel: "noopener noreferrer",
|
|
27
|
+
className: "text-muted-foreground hover:text-primary",
|
|
28
|
+
onClick: (e) => e.stopPropagation(),
|
|
29
|
+
children: [
|
|
30
|
+
"(",
|
|
31
|
+
version.sha,
|
|
32
|
+
")"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
] });
|
|
37
|
+
}
|
|
38
|
+
return /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
39
|
+
" (",
|
|
40
|
+
version.sha,
|
|
41
|
+
")"
|
|
42
|
+
] });
|
|
43
|
+
}
|
|
16
44
|
function VersionItem({
|
|
17
45
|
label,
|
|
18
46
|
version
|
|
19
47
|
}) {
|
|
20
|
-
|
|
21
|
-
|
|
48
|
+
return /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", title: label, children: [
|
|
49
|
+
label,
|
|
50
|
+
":",
|
|
51
|
+
" ",
|
|
52
|
+
version.url ? /* @__PURE__ */ jsx(
|
|
22
53
|
"a",
|
|
23
54
|
{
|
|
24
55
|
href: version.url,
|
|
25
56
|
target: "_blank",
|
|
26
57
|
rel: "noopener noreferrer",
|
|
27
|
-
className: "text-
|
|
58
|
+
className: "text-primary hover:text-primary/80 transition-colors font-medium",
|
|
28
59
|
onClick: (e) => e.stopPropagation(),
|
|
29
|
-
|
|
30
|
-
children: [
|
|
31
|
-
label,
|
|
32
|
-
": ",
|
|
33
|
-
version.displayName
|
|
34
|
-
]
|
|
60
|
+
children: version.displayName
|
|
35
61
|
}
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
return /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", title: label, children: [
|
|
39
|
-
label,
|
|
40
|
-
": ",
|
|
41
|
-
version.displayName
|
|
62
|
+
) : version.displayName,
|
|
63
|
+
/* @__PURE__ */ jsx(ShaSuffix, { version })
|
|
42
64
|
] });
|
|
43
65
|
}
|
|
44
66
|
function VersionDisplay({ versions }) {
|
|
@@ -154,6 +176,7 @@ function Header({ middle }) {
|
|
|
154
176
|
] });
|
|
155
177
|
}
|
|
156
178
|
export {
|
|
157
|
-
Header
|
|
179
|
+
Header,
|
|
180
|
+
VersionDisplay
|
|
158
181
|
};
|
|
159
182
|
//# sourceMappingURL=Header.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Header.js","sources":["../../../../../src/ui/components/header/Header.tsx"],"sourcesContent":["import type {\n AppVersionInfo,\n VersionInfo,\n} from '@igstack/app-catalog-backend-core'\nimport { Link } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { LogOut } from 'lucide-react'\nimport React, { use } from 'react'\nimport AppCatalogLogo from '~/assets/app-catalog.svg?react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { ThemeSwitcher } from '~/components/ThemeSwitcher'\nimport { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'\nimport {\n useAuth,\n useAuthActions,\n useIsAuthenticated,\n useUser,\n} from '~/modules/auth'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Button } from '~/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '~/ui/dropdown-menu'\nimport { Skeleton } from '~/ui/skeleton'\n\nfunction VersionItem({\n label,\n version,\n}: {\n label: string\n version: VersionInfo\n}) {\n if (version.url) {\n return (\n <a\n href={version.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-xs text-primary hover:text-primary/80 transition-colors font-medium\"\n onClick={(e) => e.stopPropagation()}\n title={label}\n >\n {label}: {version.displayName}\n </a>\n )\n }\n return (\n <span className=\"text-xs text-muted-foreground\" title={label}>\n {label}: {version.displayName}\n </span>\n )\n}\n\nfunction VersionDisplay({ versions }: { versions: AppVersionInfo }) {\n const isLocal =\n versions.backend?.displayName === 'local' && !versions.coreVersion\n\n if (isLocal) {\n return (\n <span className=\"text-xs text-muted-foreground\" title=\"Local\">\n local\n </span>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-0.5\">\n {versions.backend && (\n <VersionItem label=\"Pipeline\" version={versions.backend} />\n )}\n {versions.coreVersion && (\n <VersionItem label=\"Core\" version={versions.coreVersion} />\n )}\n {versions.frontend && (\n <VersionItem label=\"FE\" version={versions.frontend} />\n )}\n </div>\n )\n}\n\nexport interface HeaderProps {\n middle?: React.ReactNode\n}\n\nexport function Header({ middle }: HeaderProps) {\n const { isLoading } = useAuth()\n const isAuthenticated = useIsAuthenticated()\n const user = useUser()\n const { logout, devLogin } = useAuthActions()\n const { open: openLoginModal } = useAuthModal()\n const appCatalogContextMaybe = use(AppCatalogContext)\n const trpc = useTRPC()\n const { data: providersData } = useQuery(\n trpc.auth.getProviders.queryOptions(),\n )\n\n const handleLogout = async () => {\n try {\n await logout()\n } catch (error) {\n console.error('Logout failed:', error)\n }\n }\n\n const handleLoginClick = () => {\n // Preserve the current URL for redirect after login\n const currentUrl = window.location.pathname + window.location.search\n openLoginModal(currentUrl)\n }\n\n return (\n <div className=\"flex items-center mb-4 pb-4 gap-4 border-b\">\n <div className=\"flex items-center gap-4\">\n <Link to=\"/\">\n <div className=\"flex items-center gap-2\">\n <AppCatalogLogo className=\"h-16 w-16\" />\n <div className=\"flex flex-col\">\n <span className=\"text-lg font-bold\">App Catalog</span>\n {appCatalogContextMaybe?.versions && (\n <VersionDisplay versions={appCatalogContextMaybe.versions} />\n )}\n </div>\n </div>\n </Link>\n </div>\n {middle && <div className=\"flex-1\">{middle}</div>}\n <div className=\"flex items-center gap-3 ml-auto\">\n <ThemeSwitcher />\n {isLoading ? (\n <Skeleton className=\"w-8 h-8 rounded-full\" />\n ) : !isAuthenticated ? (\n <div className=\"flex items-center gap-2\">\n {providersData?.devLoginEnabled && (\n <button\n type=\"button\"\n onClick={devLogin}\n className=\"text-xs font-medium px-2 py-1 rounded bg-amber-500/20 text-amber-600 dark:text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 cursor-pointer\"\n >\n DEV Login\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleLoginClick}\n className=\"text-sm font-medium text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n Login\n </button>\n </div>\n ) : (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-8 h-8 rounded-full p-0 hover:bg-accent hover:text-accent-foreground cursor-pointer\"\n aria-label={`User menu for ${user?.name || 'User'}`}\n data-testid=\"user-avatar-button\"\n >\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-full h-full rounded-full bg-primary text-primary-foreground text-xs font-semibold\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <div className=\"px-2 py-3 flex items-center gap-3\">\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-10 h-10 rounded-full object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-primary text-primary-foreground text-sm font-semibold shrink-0\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-xs text-muted-foreground truncate\">\n @{user?.email?.split('@')[0] || 'user'}\n </p>\n <p className=\"text-sm font-medium truncate\">\n {user?.name || 'User'}\n </p>\n </div>\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleLogout}>\n <LogOut className=\"h-4 w-4 mr-2\" />\n <span>Sign out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )\n}\n"],"names":["AppCatalogLogo"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Header.js","sources":["../../../../../src/ui/components/header/Header.tsx"],"sourcesContent":["import type {\n AppVersionInfo,\n VersionInfo,\n} from '@igstack/app-catalog-backend-core'\nimport { Link } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { LogOut } from 'lucide-react'\nimport React, { use } from 'react'\nimport AppCatalogLogo from '~/assets/app-catalog.svg?react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { ThemeSwitcher } from '~/components/ThemeSwitcher'\nimport { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'\nimport {\n useAuth,\n useAuthActions,\n useIsAuthenticated,\n useUser,\n} from '~/modules/auth'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Button } from '~/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '~/ui/dropdown-menu'\nimport { Skeleton } from '~/ui/skeleton'\n\nfunction ShaSuffix({ version }: { version: VersionInfo }) {\n if (!version.sha) return null\n if (version.shaUrl) {\n return (\n <>\n {' '}\n <a\n href={version.shaUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n onClick={(e) => e.stopPropagation()}\n >\n ({version.sha})\n </a>\n </>\n )\n }\n return <span className=\"text-muted-foreground\"> ({version.sha})</span>\n}\n\nfunction VersionItem({\n label,\n version,\n}: {\n label: string\n version: VersionInfo\n}) {\n // Outer element is a <span> (not <a>) so the optional SHA link can be a\n // sibling anchor without producing invalid nested <a> markup.\n return (\n <span className=\"text-xs text-muted-foreground\" title={label}>\n {label}:{' '}\n {version.url ? (\n <a\n href={version.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:text-primary/80 transition-colors font-medium\"\n onClick={(e) => e.stopPropagation()}\n >\n {version.displayName}\n </a>\n ) : (\n version.displayName\n )}\n <ShaSuffix version={version} />\n </span>\n )\n}\n\nexport function VersionDisplay({ versions }: { versions: AppVersionInfo }) {\n const isLocal =\n versions.backend?.displayName === 'local' && !versions.coreVersion\n\n if (isLocal) {\n return (\n <span className=\"text-xs text-muted-foreground\" title=\"Local\">\n local\n </span>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-0.5\">\n {versions.backend && (\n <VersionItem label=\"Pipeline\" version={versions.backend} />\n )}\n {versions.coreVersion && (\n <VersionItem label=\"Core\" version={versions.coreVersion} />\n )}\n {versions.frontend && (\n <VersionItem label=\"FE\" version={versions.frontend} />\n )}\n </div>\n )\n}\n\nexport interface HeaderProps {\n middle?: React.ReactNode\n}\n\nexport function Header({ middle }: HeaderProps) {\n const { isLoading } = useAuth()\n const isAuthenticated = useIsAuthenticated()\n const user = useUser()\n const { logout, devLogin } = useAuthActions()\n const { open: openLoginModal } = useAuthModal()\n const appCatalogContextMaybe = use(AppCatalogContext)\n const trpc = useTRPC()\n const { data: providersData } = useQuery(\n trpc.auth.getProviders.queryOptions(),\n )\n\n const handleLogout = async () => {\n try {\n await logout()\n } catch (error) {\n console.error('Logout failed:', error)\n }\n }\n\n const handleLoginClick = () => {\n // Preserve the current URL for redirect after login\n const currentUrl = window.location.pathname + window.location.search\n openLoginModal(currentUrl)\n }\n\n return (\n <div className=\"flex items-center mb-4 pb-4 gap-4 border-b\">\n <div className=\"flex items-center gap-4\">\n <Link to=\"/\">\n <div className=\"flex items-center gap-2\">\n <AppCatalogLogo className=\"h-16 w-16\" />\n <div className=\"flex flex-col\">\n <span className=\"text-lg font-bold\">App Catalog</span>\n {appCatalogContextMaybe?.versions && (\n <VersionDisplay versions={appCatalogContextMaybe.versions} />\n )}\n </div>\n </div>\n </Link>\n </div>\n {middle && <div className=\"flex-1\">{middle}</div>}\n <div className=\"flex items-center gap-3 ml-auto\">\n <ThemeSwitcher />\n {isLoading ? (\n <Skeleton className=\"w-8 h-8 rounded-full\" />\n ) : !isAuthenticated ? (\n <div className=\"flex items-center gap-2\">\n {providersData?.devLoginEnabled && (\n <button\n type=\"button\"\n onClick={devLogin}\n className=\"text-xs font-medium px-2 py-1 rounded bg-amber-500/20 text-amber-600 dark:text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 cursor-pointer\"\n >\n DEV Login\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleLoginClick}\n className=\"text-sm font-medium text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n Login\n </button>\n </div>\n ) : (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-8 h-8 rounded-full p-0 hover:bg-accent hover:text-accent-foreground cursor-pointer\"\n aria-label={`User menu for ${user?.name || 'User'}`}\n data-testid=\"user-avatar-button\"\n >\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-full h-full rounded-full bg-primary text-primary-foreground text-xs font-semibold\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <div className=\"px-2 py-3 flex items-center gap-3\">\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-10 h-10 rounded-full object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-primary text-primary-foreground text-sm font-semibold shrink-0\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-xs text-muted-foreground truncate\">\n @{user?.email?.split('@')[0] || 'user'}\n </p>\n <p className=\"text-sm font-medium truncate\">\n {user?.name || 'User'}\n </p>\n </div>\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleLogout}>\n <LogOut className=\"h-4 w-4 mr-2\" />\n <span>Sign out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )\n}\n"],"names":["AppCatalogLogo"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.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-backend-core": "0.6.
|
|
138
|
-
"@igstack/app-catalog-shared-core": "0.6.
|
|
137
|
+
"@igstack/app-catalog-backend-core": "0.6.1",
|
|
138
|
+
"@igstack/app-catalog-shared-core": "0.6.1"
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
141
141
|
"react": "19.1.2",
|
|
@@ -144,6 +144,7 @@
|
|
|
144
144
|
"vite": "^6.3.5",
|
|
145
145
|
"vite-plugin-svgr": "^4.2.0"
|
|
146
146
|
},
|
|
147
|
+
"gitHead": "e83dc91aa65e1699c3fe578db7a62d81c4b27857",
|
|
147
148
|
"scripts": {
|
|
148
149
|
"build": "vite build",
|
|
149
150
|
"build:lenient": "vite build --mode lenient",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'
|
|
3
|
+
|
|
4
|
+
describe('mergeFrontendBuildId', () => {
|
|
5
|
+
it('returns versions unchanged when no build id', () => {
|
|
6
|
+
const versions = { frontend: { displayName: '0.4.0', sha: 'abc1234' } }
|
|
7
|
+
expect(mergeFrontendBuildId(versions, undefined)).toEqual(versions)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('shows local (dropping SHA) when build id is local', () => {
|
|
11
|
+
const versions = { frontend: { displayName: '0.4.0', sha: 'abc1234' } }
|
|
12
|
+
const out = mergeFrontendBuildId(versions, 'local')
|
|
13
|
+
expect(out.frontend).toEqual({ displayName: 'local' })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('prepends pipeline id while preserving fe-core version + sha + url', () => {
|
|
17
|
+
const versions = {
|
|
18
|
+
frontend: {
|
|
19
|
+
displayName: '0.4.0-alpha-x',
|
|
20
|
+
sha: 'abc1234',
|
|
21
|
+
shaUrl: 'https://github.com/lislon/app-catalog/commit/abc1234',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
const out = mergeFrontendBuildId(versions, '12345')
|
|
25
|
+
expect(out.frontend?.displayName).toBe('#12345 · 0.4.0-alpha-x')
|
|
26
|
+
expect(out.frontend?.sha).toBe('abc1234')
|
|
27
|
+
expect(out.frontend?.shaUrl).toBe(
|
|
28
|
+
'https://github.com/lislon/app-catalog/commit/abc1234',
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('shows only pipeline id when backend gave no frontend slot', () => {
|
|
33
|
+
const out = mergeFrontendBuildId({}, '12345')
|
|
34
|
+
expect(out.frontend).toEqual({ displayName: '#12345' })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('does not mutate the input', () => {
|
|
38
|
+
const versions = { frontend: { displayName: '0.4.0' } }
|
|
39
|
+
mergeFrontendBuildId(versions, '99')
|
|
40
|
+
expect(versions.frontend.displayName).toBe('0.4.0')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import '@testing-library/jest-dom/vitest'
|
|
4
|
+
|
|
5
|
+
import { VersionDisplay } from '~/ui/components/header/Header'
|
|
6
|
+
|
|
7
|
+
describe('VersionDisplay footer', () => {
|
|
8
|
+
it('renders the frontend-core version and its short SHA as a commit link', () => {
|
|
9
|
+
render(
|
|
10
|
+
<VersionDisplay
|
|
11
|
+
versions={{
|
|
12
|
+
backend: { displayName: '#12345 (deadbee)' },
|
|
13
|
+
coreVersion: { displayName: 'be 0.4.0 / fe 0.3.1' },
|
|
14
|
+
frontend: {
|
|
15
|
+
displayName: '#12345 · 0.3.1-alpha-x',
|
|
16
|
+
sha: 'abc1234',
|
|
17
|
+
shaUrl: 'https://github.com/lislon/app-catalog/commit/abc1234',
|
|
18
|
+
},
|
|
19
|
+
}}
|
|
20
|
+
/>,
|
|
21
|
+
)
|
|
22
|
+
// FE line shows pipeline id + fe-core version
|
|
23
|
+
expect(screen.getByText(/0\.3\.1-alpha-x/)).toBeInTheDocument()
|
|
24
|
+
// SHA rendered and linked to the commit
|
|
25
|
+
const shaLink = screen.getByText('(abc1234)').closest('a')
|
|
26
|
+
expect(shaLink).toHaveAttribute(
|
|
27
|
+
'href',
|
|
28
|
+
'https://github.com/lislon/app-catalog/commit/abc1234',
|
|
29
|
+
)
|
|
30
|
+
// Backend line carries the natera SHA suffix
|
|
31
|
+
expect(screen.getByText(/#12345 \(deadbee\)/)).toBeInTheDocument()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('shows plain "local" for local dev', () => {
|
|
35
|
+
render(<VersionDisplay versions={{ backend: { displayName: 'local' } }} />)
|
|
36
|
+
expect(screen.getByText('local')).toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -11,6 +11,7 @@ import type { ReactNode } from 'react'
|
|
|
11
11
|
import { createContext, use, useEffect, useMemo } from 'react'
|
|
12
12
|
import { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'
|
|
13
13
|
import { useUiSettings } from '~/context/UiSettingsContext'
|
|
14
|
+
import { mergeFrontendBuildId } from '~/modules/appCatalog/context/mergeFrontendBuildId'
|
|
14
15
|
|
|
15
16
|
export interface AppCatalogContextIface {
|
|
16
17
|
resources: Resource[]
|
|
@@ -44,17 +45,10 @@ export function AppCatalogProvider({ children }: AppCatalogProviderProps) {
|
|
|
44
45
|
approvalMethods: data?.approvalMethods ?? [],
|
|
45
46
|
persons: data?.persons ?? [],
|
|
46
47
|
groups: data?.groups ?? [],
|
|
47
|
-
versions:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
displayName:
|
|
52
|
-
uiSettings.frontendBuildId === 'local'
|
|
53
|
-
? 'local'
|
|
54
|
-
: `#${uiSettings.frontendBuildId}`,
|
|
55
|
-
},
|
|
56
|
-
}),
|
|
57
|
-
},
|
|
48
|
+
versions: mergeFrontendBuildId(
|
|
49
|
+
data?.versions ?? {},
|
|
50
|
+
uiSettings.frontendBuildId,
|
|
51
|
+
),
|
|
58
52
|
}),
|
|
59
53
|
[
|
|
60
54
|
data?.approvalMethods,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AppVersionInfo } from '@igstack/app-catalog-backend-core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merge the (natera-agnostic) frontend build id into the backend-provided
|
|
5
|
+
* version info's `frontend` slot.
|
|
6
|
+
*
|
|
7
|
+
* The build id is baked into the deployed JS bundle at build time (e.g. a CI
|
|
8
|
+
* pipeline id via a Vite env var). We PREPEND it to the resolved frontend-core
|
|
9
|
+
* npm version instead of replacing it, so the footer surfaces both:
|
|
10
|
+
* `#<buildId> · <frontend-core version> (<sha>)`.
|
|
11
|
+
*
|
|
12
|
+
* - No build id → return versions unchanged.
|
|
13
|
+
* - build id === 'local' → show `local` (local dev, no meaningful SHA/version).
|
|
14
|
+
* - otherwise → prepend `#<buildId> · ` and preserve the version + sha + shaUrl.
|
|
15
|
+
*/
|
|
16
|
+
export function mergeFrontendBuildId(
|
|
17
|
+
versions: AppVersionInfo,
|
|
18
|
+
frontendBuildId: string | undefined,
|
|
19
|
+
): AppVersionInfo {
|
|
20
|
+
if (!frontendBuildId) return versions
|
|
21
|
+
if (frontendBuildId === 'local') {
|
|
22
|
+
return { ...versions, frontend: { displayName: 'local' } }
|
|
23
|
+
}
|
|
24
|
+
const be = versions.frontend
|
|
25
|
+
return {
|
|
26
|
+
...versions,
|
|
27
|
+
frontend: be
|
|
28
|
+
? { ...be, displayName: `#${frontendBuildId} · ${be.displayName}` }
|
|
29
|
+
: { displayName: `#${frontendBuildId}` },
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -27,6 +27,27 @@ import {
|
|
|
27
27
|
} from '~/ui/dropdown-menu'
|
|
28
28
|
import { Skeleton } from '~/ui/skeleton'
|
|
29
29
|
|
|
30
|
+
function ShaSuffix({ version }: { version: VersionInfo }) {
|
|
31
|
+
if (!version.sha) return null
|
|
32
|
+
if (version.shaUrl) {
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{' '}
|
|
36
|
+
<a
|
|
37
|
+
href={version.shaUrl}
|
|
38
|
+
target="_blank"
|
|
39
|
+
rel="noopener noreferrer"
|
|
40
|
+
className="text-muted-foreground hover:text-primary"
|
|
41
|
+
onClick={(e) => e.stopPropagation()}
|
|
42
|
+
>
|
|
43
|
+
({version.sha})
|
|
44
|
+
</a>
|
|
45
|
+
</>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
return <span className="text-muted-foreground"> ({version.sha})</span>
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
function VersionItem({
|
|
31
52
|
label,
|
|
32
53
|
version,
|
|
@@ -34,28 +55,30 @@ function VersionItem({
|
|
|
34
55
|
label: string
|
|
35
56
|
version: VersionInfo
|
|
36
57
|
}) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<a
|
|
40
|
-
href={version.url}
|
|
41
|
-
target="_blank"
|
|
42
|
-
rel="noopener noreferrer"
|
|
43
|
-
className="text-xs text-primary hover:text-primary/80 transition-colors font-medium"
|
|
44
|
-
onClick={(e) => e.stopPropagation()}
|
|
45
|
-
title={label}
|
|
46
|
-
>
|
|
47
|
-
{label}: {version.displayName}
|
|
48
|
-
</a>
|
|
49
|
-
)
|
|
50
|
-
}
|
|
58
|
+
// Outer element is a <span> (not <a>) so the optional SHA link can be a
|
|
59
|
+
// sibling anchor without producing invalid nested <a> markup.
|
|
51
60
|
return (
|
|
52
61
|
<span className="text-xs text-muted-foreground" title={label}>
|
|
53
|
-
{label}:
|
|
62
|
+
{label}:{' '}
|
|
63
|
+
{version.url ? (
|
|
64
|
+
<a
|
|
65
|
+
href={version.url}
|
|
66
|
+
target="_blank"
|
|
67
|
+
rel="noopener noreferrer"
|
|
68
|
+
className="text-primary hover:text-primary/80 transition-colors font-medium"
|
|
69
|
+
onClick={(e) => e.stopPropagation()}
|
|
70
|
+
>
|
|
71
|
+
{version.displayName}
|
|
72
|
+
</a>
|
|
73
|
+
) : (
|
|
74
|
+
version.displayName
|
|
75
|
+
)}
|
|
76
|
+
<ShaSuffix version={version} />
|
|
54
77
|
</span>
|
|
55
78
|
)
|
|
56
79
|
}
|
|
57
80
|
|
|
58
|
-
function VersionDisplay({ versions }: { versions: AppVersionInfo }) {
|
|
81
|
+
export function VersionDisplay({ versions }: { versions: AppVersionInfo }) {
|
|
59
82
|
const isLocal =
|
|
60
83
|
versions.backend?.displayName === 'local' && !versions.coreVersion
|
|
61
84
|
|