Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/tests-js.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ jobs:
working-directory: ./js
run: pnpm lint

- name: Run Jest Tests
working-directory: ./js
run: pnpm test

- name: Build the static files
working-directory: ./js
run: pnpm build
1 change: 1 addition & 0 deletions js/.husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ if [ -n "$staged_js_files" ]; then
pushd js
pnpm lint:staged
pnpm type:check
pnpm test
popd
else
echo "No TypeScript/JavaScript files changed, skipping lint and type check."
Expand Down
157 changes: 157 additions & 0 deletions js/app/(mainComponents)/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"use client";

import { Box, Flex, Link, Tabs } from "@chakra-ui/react";
import { useQuery } from "@tanstack/react-query";
import NextLink from "next/link";
import { usePathname } from "next/navigation";
import React, { Activity, ReactNode, useEffect, useMemo } from "react";
import { EnvInfo } from "@/components/app/EnvInfo";
import { Filename } from "@/components/app/Filename";
import { StateExporter } from "@/components/app/StateExporter";
import { TopLevelShare } from "@/components/app/StateSharing";
import { StateSynchronizer } from "@/components/app/StateSynchronizer";
import { cacheKeys } from "@/lib/api/cacheKeys";
import { Check, listChecks } from "@/lib/api/checks";
import { trackNavigation } from "@/lib/api/track";
import { useLineageGraphContext } from "@/lib/hooks/LineageGraphContext";
import { useRecceInstanceContext } from "@/lib/hooks/RecceInstanceContext";
import { useRecceServerFlag } from "@/lib/hooks/useRecceServerFlag";

/**
* Route configuration for tabs
*/
const ROUTE_CONFIG = [
{ path: "/lineage", name: "Lineage" },
{ path: "/query", name: "Query" },
{ path: "/checks", name: "Checklist" },
] as const;

interface TabBadgeProps<T> {
queryKey: string[];
fetchCallback: () => Promise<T>;
selectCallback?: (data: T) => number;
}

function TabBadge<T>({
queryKey,
fetchCallback,
selectCallback,
}: TabBadgeProps<T>): ReactNode {
const {
data: count,
isLoading,
error,
} = useQuery({
queryKey: queryKey,
queryFn: fetchCallback,
select: selectCallback,
});

if (isLoading || error || count === 0) {
return <></>;
}

return (
<Box
ml="2px"
maxH={"20px"}
height="80%"
aspectRatio={1}
borderRadius="full"
bg="tomato"
alignContent={"center"}
color="white"
fontSize="xs"
>
{count}
</Box>
);
}

// NavBar component with Next.js Link navigation
export default function NavBar() {
const pathname = usePathname();
const { isDemoSite, isLoading, cloudMode } = useLineageGraphContext();
const { featureToggles } = useRecceInstanceContext();
const { data: flag, isLoading: isFlagLoading } = useRecceServerFlag();
const ChecklistBadge = (
<TabBadge<Check[]>
queryKey={cacheKeys.checks()}
fetchCallback={listChecks}
selectCallback={(checks: Check[]) => {
return checks.filter((check) => !check.is_checked).length;
}}
/>
);
// Track navigation changes
useEffect(() => {
trackNavigation({ from: location.pathname, to: pathname });
}, [pathname]);

// Get current tab value from pathname
const currentTab = useMemo(() => {
if (pathname.startsWith("/checks")) return "/checks";
if (pathname.startsWith("/query")) return "/query";
if (pathname.startsWith("/runs")) return "/runs";
return "/lineage";
}, [pathname]);

return (
<Tabs.Root
colorPalette="iochmara"
value={currentTab}
size="sm"
variant="line"
borderBottom="1px solid lightgray"
px="12px"
>
<Tabs.List
display="grid"
gridTemplateColumns="1fr auto 1fr"
width="100%"
borderBottom="none"
>
{/* Left section: Tabs */}
<Box display="flex" alignItems="center" gap="4px">
{ROUTE_CONFIG.map(({ path, name }) => {
const disable = name === "Query" && flag?.single_env_onboarding;

return (
<Tabs.Trigger
key={path}
value={path}
disabled={isLoading || isFlagLoading || disable}
hidden={disable}
>
<Link asChild>
<NextLink href={path}>{name}</NextLink>
</Link>
<Activity mode={name === "Checklist" ? "visible" : "hidden"}>
{ChecklistBadge}
</Activity>
</Tabs.Trigger>
);
})}
</Box>

{/* Center section: Filename and TopLevelShare */}
<Flex alignItems="center" gap="12px" justifyContent="center">
{!isLoading && !isDemoSite && <Filename />}
{!isLoading &&
!isDemoSite &&
!flag?.single_env_onboarding &&
!featureToggles.disableShare && <TopLevelShare />}
</Flex>

{/* Right section: EnvInfo, StateSynchronizer, StateExporter */}
{!isLoading && (
<Flex justifyContent="right" alignItems="center" mr="8px">
<EnvInfo />
{cloudMode && <StateSynchronizer />}
<StateExporter />
</Flex>
)}
</Tabs.List>
</Tabs.Root>
);
}
90 changes: 90 additions & 0 deletions js/app/(mainComponents)/RecceVersionBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Badge, Code, Link, Text } from "@chakra-ui/react";
import React, { useEffect, useMemo } from "react";
import { toaster } from "@/components/ui/toaster";
import { useVersionNumber } from "@/lib/api/version";

export default function RecceVersionBadge() {
const { version, latestVersion } = useVersionNumber();
const versionFormatRegex = useMemo(
() => new RegExp("^\\d+\\.\\d+\\.\\d+$"),
[],
);

useEffect(() => {
if (versionFormatRegex.test(version) && version !== latestVersion) {
const storageKey = "recce-update-toast-shown";
const hasShownForThisVersion = sessionStorage.getItem(storageKey);
if (hasShownForThisVersion) {
return;
}
// Defer toast creation to next tick to avoid React's flushSync error
// This prevents "flushSync called from inside lifecycle method" when
// the toast library tries to immediately update DOM during render cycle
setTimeout(() => {
toaster.create({
id: "recce-update-available", // Fixed ID prevents duplicates
title: "Update available",
description: (
<span>
A new version of Recce (v{latestVersion}) is available.
<br />
Please run <Code>pip install --upgrade recce</Code> to update
Recce.
<br />
<Link
color="brand.700"
fontWeight={"bold"}
href={`https://github.com/DataRecce/recce/releases/tag/v${latestVersion}`}
_hover={{ textDecoration: "underline" }}
target="_blank"
>
Click here to view the detail of latest release
</Link>
</span>
),
duration: 60 * 1000,
// TODO Fix this at a later update
// containerStyle: {
// background: "rgba(20, 20, 20, 0.6)", // Semi-transparent black
// color: "white", // Ensure text is visible
// backdropFilter: "blur(10px)", // Frosted glass effect
// borderRadius: "8px",
// },
closable: true,
});
sessionStorage.setItem(storageKey, "true");
}, 0);
}
}, [version, latestVersion, versionFormatRegex]);

if (!versionFormatRegex.test(version)) {
// If the version is not in the format of x.y.z, don't apply
return (
<Badge
fontSize="sm"
color="white/80"
variant="outline"
textTransform="uppercase"
>
{version}
</Badge>
);
}

// Link to the release page on GitHub if the version is in the format of x.y.z
return (
<Badge
fontSize="sm"
color="white/80"
variant="outline"
textTransform="uppercase"
>
<Link
href={`https://github.com/DataRecce/recce/releases/tag/v${version}`}
_hover={{ textDecoration: "none" }}
>
<Text color="white/80">{version}</Text>
</Link>
</Badge>
);
}
Loading