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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@microsoft/fetch-event-source": "^2.0.1",
"@next/third-parties": "^15.5.4",
"radix-ui": "^1.4.3",
"@radix-ui/themes": "^3.2.1",
"@tanstack/react-query": "^5.76.0",
"@tanstack/react-query-next-experimental": "^5.76.0",
Expand All @@ -53,6 +53,7 @@
"next-auth": "^5.0.0-beta.29",
"nuqs": "^2.6.0",
"posthog-node": "^5.9.2",
"radix-ui": "^1.4.3",
"react": "19.1.1",
"react-day-picker": "^9.6.7",
"react-dom": "19.1.1",
Expand Down
45 changes: 42 additions & 3 deletions src/app/(app)/settings/code-review/[repositoryId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
"use client";

import { useEffect } from "react";
import { Suspense, useEffect } from "react";
import { useParams } from "next/navigation";
import { GetStartedChecklist } from "@components/system/get-started-checklist";
import { Button } from "@components/ui/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@components/ui/sheet";
import { useSuspenseGetParameterByKey } from "@services/parameters/hooks";
import { LanguageValue, ParametersConfigKey } from "@services/parameters/types";
import { usePermission } from "@services/permissions/hooks";
import { Action, ResourceType } from "@services/permissions/types";
import { Eye } from "lucide-react";
import { FormProvider, useForm } from "react-hook-form";
import { useSelectedTeamId } from "src/core/providers/selected-team-context";
import { cn } from "src/core/utils/components";

import { DryRunSidebar } from "../_components/dry-run-sidebar";
import { type CodeReviewFormType } from "../_types";
import { useCodeReviewConfig } from "../../_components/context";

Expand All @@ -28,11 +40,12 @@ export default function Layout(props: React.PropsWithChildren) {
);

const params = useParams();
const repositoryId = params.repositoryId as string;

const canEdit = usePermission(
Action.Update,
ResourceType.CodeReviewSettings,
params.repositoryId as string,
repositoryId,
);

const form = useForm<CodeReviewFormType>({
Expand All @@ -50,5 +63,31 @@ export default function Layout(props: React.PropsWithChildren) {
form.reset({ ...config, language: parameters.configValue });
}, [config?.id]);

return <FormProvider {...form}>{props.children}</FormProvider>;
// const getStarted = GetStartedChecklist();
const getStartedVisible = true;

return (
<FormProvider {...form}>
{props.children}

<Sheet>
<Suspense>
<SheetTrigger asChild>
<Button
size="lg"
variant="primary"
className={cn(
"fixed right-5 z-50",
getStartedVisible ? "bottom-25" : "bottom-5",
)}>
<Eye />
Preview
</Button>
</SheetTrigger>

<DryRunSidebar />
</Suspense>
</Sheet>
</FormProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";
import { SyntaxHighlight } from "@components/ui/syntax-highlight";

export const CodeDiff = ({
existingCode,
improvedCode,
language,
}: {
existingCode?: string;
improvedCode?: string;
language?: string;
}) => {
if (!existingCode && !improvedCode) {
return (
<div className="text-text-tertiary italic">
No code snippet available.
</div>
);
}

return (
<pre className="overflow-x-auto rounded-md bg-black/50 p-4 font-mono text-sm">
<code>
<div className="space-y-3">
<div>
<p className="text-text-secondary mb-2 text-xs font-semibold">
Existing Code:
</p>
<SyntaxHighlight
language={language as any}
className="text-xs">
{existingCode}
</SyntaxHighlight>
</div>

<div>
<p className="text-text-secondary mb-2 text-xs font-semibold">
Improved Code:
</p>
<SyntaxHighlight
language={language as any}
className="text-xs">
{improvedCode}
</SyntaxHighlight>
</div>
</div>
</code>
</pre>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Eye } from "lucide-react";

export const EmptyState = () => (
<div className="flex h-full flex-1 flex-col items-center justify-center space-y-4 rounded-lg p-4 py-16 text-center">
<div className="bg-card-lv2 flex h-16 w-16 items-center justify-center rounded-full">
<Eye className="text-primary-light h-8 w-8" />
</div>
<div className="space-y-1">
<h3 className="text-lg font-semibold">Ready to preview?</h3>
<p className="text-text-tertiary max-w-sm text-sm">
Select a Pull Request and generate a preview to see how kodus
will analyze the code with current settings.
</p>
</div>
</div>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { useEffect, useState } from "react";
import { Badge } from "@components/ui/badge";
import { Button } from "@components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@components/ui/popover";
import { listDryRuns } from "@services/dryRun/fetch";
import { IDryRunData } from "@services/dryRun/types";
import { ChevronsUpDown, Loader2 } from "lucide-react";
import { useSelectedTeamId } from "src/core/providers/selected-team-context";

import { statusMap } from ".";
import { useCodeReviewRouteParams } from "../../../_hooks";

const formatHistoryDate = (dateString: string | Date) => {
const date = dateString instanceof Date ? dateString : new Date(dateString);
return date.toLocaleString(undefined, {
dateStyle: "short",
timeStyle: "short",
});
};

export const SelectHistoryItem = (props: {
id?: string;
open: boolean;
onOpenChange: (open: boolean) => void;
disabled?: boolean;
value: string | null; // correlationId
onChange: (value: string) => void; // setCorrelationId
}) => {
const {
id = "select-history-item",
open,
onOpenChange,
disabled,
onChange,
value,
} = props;

const { teamId } = useSelectedTeamId();
const { repositoryId, directoryId } = useCodeReviewRouteParams();

const [history, setHistory] = useState<IDryRunData[]>([]);
const [isHistoryLoading, setIsHistoryLoading] = useState(false);

useEffect(() => {
const fetchHistory = async () => {
if (!teamId || !repositoryId) return;
setIsHistoryLoading(true);
try {
const historyData = await listDryRuns(teamId, {
repositoryId,
directoryId,
});
setHistory(historyData);
} catch (err) {
console.error("Failed to fetch dry run history:", err);
setHistory([]);
} finally {
setIsHistoryLoading(false);
}
};

fetchHistory();
}, [teamId, repositoryId, directoryId]);

const selectedItem = history.find((item) => item.id === value);

const historyGroupedByRepository = history.reduce(
(acc, current) => {
if (!acc[current.repositoryName]) acc[current.repositoryName] = [];
acc[current.repositoryName].push(current);
return acc;
},
{} as Record<string, typeof history>,
);

return (
<Popover open={open} onOpenChange={onOpenChange} modal>
<PopoverTrigger asChild>
<Button
id={id}
type="button"
size="lg"
variant="helper"
disabled={disabled || isHistoryLoading}
className="flex min-h-16 w-full justify-between">
<div className="flex w-full items-center">
{isHistoryLoading ? (
<span className="flex flex-1 items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Loading history...
</span>
) : !selectedItem ? (
<span className="flex-1">
No past preview selected
</span>
) : (
<div className="flex-1">
<span className="text-primary-light text-xs">
{selectedItem.repositoryName}
</span>
<span className="text-text-secondary line-clamp-1 wrap-anywhere">
<strong>
#{selectedItem.prNumber} -{" "}
{selectedItem.prTitle}
</strong>{" "}
{formatHistoryDate(selectedItem.createdAt)}
</span>
</div>
)}
</div>
<ChevronsUpDown className="-mr-2 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>

<PopoverContent
align="start"
className="w-[var(--radix-popover-trigger-width)] p-0">
<Command
className="w-full"
filter={(value, search) => {
const item = history.find((h) => h.id === value);
if (item) {
const prNumberString = item.prNumber.toString();
const repositoryName =
item.repositoryName.toLowerCase();
const searchLower = search.toLowerCase();

if (
prNumberString.includes(searchLower) ||
`#${prNumberString}`.includes(searchLower) ||
repositoryName.includes(searchLower)
) {
return 1;
}
}
return 0;
}}>
<CommandInput placeholder="Search by PR number or repository" />
<CommandList className="overflow-y-auto">
<CommandEmpty className="flex h-full items-center justify-center">
No past preview found.
</CommandEmpty>
<div className="max-h-72">
{Object.entries(historyGroupedByRepository).map(
([repoName, items]) => (
<CommandGroup
heading={repoName}
key={repoName}>
{items.map((item) => (
<CommandItem
key={item.id}
value={item.id}
onSelect={() =>
onChange(item.id)
}
className="flex flex-col items-start">
<span className="text-text-secondary line-clamp-1">
<Badge className="mr-2">
{item.status
? statusMap[
item.status
]
: "Unknown"}
</Badge>
<strong className="mr-2 font-mono">
#{item.prNumber} -{" "}
{item.prTitle}
</strong>
</span>
<span className="text-text-tertiary text-xs">
{formatHistoryDate(
item.createdAt,
)}
</span>
</CommandItem>
))}
</CommandGroup>
),
)}
</div>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
Loading