Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Loading