Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2d568af
refactor: move deployments page
ogzhanolguncu Aug 26, 2025
8470e9d
feat: add actions to nvabar
ogzhanolguncu Aug 26, 2025
2419d7b
feat: add sub tab navigation
ogzhanolguncu Aug 26, 2025
10357c8
feat: add details sectiopn
ogzhanolguncu Aug 26, 2025
d6f7a64
fix: build error
ogzhanolguncu Aug 26, 2025
d031471
refactor: sections
ogzhanolguncu Aug 27, 2025
f2bcb36
feat: add animated project details
ogzhanolguncu Aug 27, 2025
b97d800
feat: add active deployments section
ogzhanolguncu Aug 27, 2025
c40791f
feat: add domains section
ogzhanolguncu Aug 27, 2025
0a27b46
feat: add build logs section
ogzhanolguncu Aug 28, 2025
d942a48
feat: add log ops
ogzhanolguncu Aug 28, 2025
56f6d3e
feat: add env update section
ogzhanolguncu Aug 28, 2025
e3832dd
refactor: use proper layout
ogzhanolguncu Aug 28, 2025
800de67
refactor: animations for envs
ogzhanolguncu Aug 28, 2025
5276583
refactor: allow details open by default
ogzhanolguncu Aug 28, 2025
5cb86b0
fix: details mount
ogzhanolguncu Aug 28, 2025
558b0e4
fix: ui inconsistencies
ogzhanolguncu Aug 29, 2025
7b49a8b
feat: get active project details from tRPC
ogzhanolguncu Aug 29, 2025
f44fd9e
feat: use tRPC
ogzhanolguncu Aug 29, 2025
97c78e8
feat: add env fetch
ogzhanolguncu Aug 29, 2025
3ae47b9
fix: import paths
ogzhanolguncu Aug 29, 2025
dcd5153
fix: import paths
ogzhanolguncu Aug 29, 2025
82c3860
Merge branch 'main' into project-details-ui
ogzhanolguncu Aug 29, 2025
294d449
fix: build issue
ogzhanolguncu Aug 29, 2025
20aa3f4
fix: coderabbit issues
ogzhanolguncu Aug 31, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useFilters } from "../../../../hooks/use-filters";
export const DeploymentListSearch = () => {
const { filters, updateFilters } = useFilters();

const queryLLMForStructuredOutput = trpc.deploy.deployment.search.useMutation({
const queryLLMForStructuredOutput = trpc.deploy.project.deployment.search.useMutation({
onSuccess(data) {
if (data?.filters.length === 0 || !data) {
toast.error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function useDeploymentsListQuery() {
fetchNextPage,
isFetchingNextPage,
isLoading: isLoadingInitial,
} = trpc.deploy.deployment.list.useInfiniteQuery(queryParams, {
} = trpc.deploy.project.deployment.list.useInfiniteQuery(queryParams, {
getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 30_000,
refetchOnMount: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client";

import { DeploymentsListControlCloud } from "./components/control-cloud";
import { DeploymentsListControls } from "./components/controls";
import { DeploymentsList } from "./components/table/deployments-list";

export default function Deployments() {
return (
<div className="flex flex-col">
<DeploymentsListControls />
<DeploymentsListControlCloud />
<DeploymentsList />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { cn } from "@unkey/ui/src/lib/utils";
import type { ReactNode } from "react";

export function Card({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn("border border-gray-4 w-full", className)}>{children}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { IconProps } from "@unkey/icons/src/props";
import { Button } from "@unkey/ui";
import { cn } from "@unkey/ui/src/lib/utils";

type FilterButtonProps = {
isActive: boolean;
count: number;
onClick: () => void;
icon: React.ComponentType<IconProps>;
label: string;
};

export const FilterButton = ({
isActive,
count,
onClick,
icon: Icon,
label,
}: FilterButtonProps) => (
<Button
variant="primary"
className={cn(
"text-xs h-[26px] border-none hover:bg-grayA-4",
isActive ? "bg-gray-12 hover:bg-grayA-12" : "bg-grayA-3 text-grayA-9",
)}
onClick={onClick}
>
<Icon size="sm-regular" className={isActive ? "" : "text-grayA-9"} />
<span className={isActive ? "" : "text-grayA-9"}>{label}</span>
<div className="rounded size-[18px] flex items-center justify-center text-[10px] leading-4 bg-gray-4 text-black dark:text-white">
{count}
</div>
</Button>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { trpc } from "@/lib/trpc/client";
import { format } from "date-fns";
import { useEffect, useMemo, useRef, useState } from "react";

type LogEntry = {
timestamp: string;
level?: "info" | "warning" | "error";
message: string;
};

type LogFilter = "all" | "errors" | "warnings";

type UseDeploymentLogsProps = {
deploymentId: string;
};

type UseDeploymentLogsReturn = {
// State
logFilter: LogFilter;
searchTerm: string;
isExpanded: boolean;
showFade: boolean;
// Computed
filteredLogs: LogEntry[];
logCounts: {
total: number;
errors: number;
warnings: number;
};
// Loading state
isLoading: boolean;
// Actions
setLogFilter: (filter: LogFilter) => void;
setSearchTerm: (term: string) => void;
setExpanded: (expanded: boolean) => void;
handleScroll: (e: React.UIEvent<HTMLDivElement>) => void;
handleFilterChange: (filter: LogFilter) => void;
handleSearchChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
// Refs
scrollRef: React.RefObject<HTMLDivElement>;
};

export function useDeploymentLogs({
deploymentId,
}: UseDeploymentLogsProps): UseDeploymentLogsReturn {
const [logFilter, setLogFilter] = useState<LogFilter>("all");
const [searchTerm, setSearchTerm] = useState("");
const [isExpanded, setIsExpanded] = useState(false);
const [showFade, setShowFade] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);

// Fetch logs via tRPC
const { data: logsData, isLoading } = trpc.deploy.project.activeDeployment.buildLogs.useQuery({
deploymentId,
});

// Transform tRPC logs to match the expected format
const logs = useMemo((): LogEntry[] => {
if (!logsData?.logs) {
return [];
}

return logsData.logs.map((log) => ({
timestamp: format(new Date(log.timestamp), "HH:mm:ss.SSS"),
level: log.level,
message: log.message,
}));
}, [logsData]);

// Auto-expand when logs are fetched
useEffect(() => {
if (logsData?.logs && logsData.logs.length > 0) {
setIsExpanded(true);
}
}, [logsData]);

// Calculate log counts
const logCounts = useMemo(
() => ({
total: logs.length,
errors: logs.filter((log) => log.level === "error").length,
warnings: logs.filter((log) => log.level === "warning").length,
}),
[logs],
);

// Filter logs by level and search term
const filteredLogs = useMemo(() => {
let filtered = logs;

// Apply level filter
if (logFilter === "errors") {
filtered = logs.filter((log) => log.level === "error");
} else if (logFilter === "warnings") {
filtered = logs.filter((log) => log.level === "warning");
}

// Apply search filter
if (searchTerm.trim()) {
filtered = filtered.filter(
(log) =>
log.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
log.timestamp.includes(searchTerm) ||
log.level?.toLowerCase().includes(searchTerm.toLowerCase()),
);
}

return filtered;
}, [logs, logFilter, searchTerm]);

const resetScroll = () => {
if (scrollRef.current) {
scrollRef.current.scrollTop = 0;
setShowFade(true);
}
};

const setExpanded = (expanded: boolean) => {
setIsExpanded(expanded);
if (!expanded) {
setTimeout(resetScroll, 50);
}
};

const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
setShowFade(!isAtBottom);
};

const handleFilterChange = (filter: LogFilter) => {
setLogFilter(filter);
resetScroll();
};

const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
resetScroll();
};

return {
// State
logFilter,
searchTerm,
isExpanded,
showFade,
// Computed
filteredLogs,
logCounts,
// Loading state
isLoading,
// Actions
setLogFilter,
setSearchTerm,
setExpanded,
handleScroll,
handleFilterChange,
handleSearchChange,
// Refs
scrollRef,
};
}
Loading