Skip to content

Commit bdaf3a1

Browse files
committed
feat(dashboard): frontend implementation
1 parent b896dc8 commit bdaf3a1

File tree

7 files changed

+237
-67
lines changed

7 files changed

+237
-67
lines changed

src/daft-dashboard/frontend/src/app/queries/page.tsx

Lines changed: 99 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ import {
2727
import LoadingPage from "@/components/loading";
2828

2929
import { QuerySummary, useQueries } from "@/hooks/use-queries";
30-
import { toHumanReadableDate } from "@/lib/utils";
31-
import Status from "./status";
30+
import { toHumanReadableDate, getEngineName } from "@/lib/utils";
31+
import Status, { Duration } from "./status";
3232
import Link from "next/link";
3333
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty";
34-
import { ClipboardIcon, Database } from "lucide-react";
34+
import { ClipboardIcon, Database, ExternalLinkIcon } from "lucide-react";
3535
import { Button } from "@/components/ui/button";
36-
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
36+
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
3737
import { dashboardUrl } from "@/components/server-provider";
3838
import { TooltipArrow } from "@radix-ui/react-tooltip";
3939

@@ -42,7 +42,7 @@ const STATUS: string = "state";
4242
// Define status priority for sorting (lower number = higher priority)
4343
const statusPriority: Record<string, number> = {
4444
Pending: 1,
45-
Planning: 2,
45+
Optimizing: 2,
4646
Setup: 3,
4747
Executing: 4,
4848
Finalizing: 5,
@@ -63,18 +63,87 @@ const columnHelper = createColumnHelper<QuerySummary>();
6363
const columns = [
6464
columnHelper.accessor("id", {
6565
header: "Name",
66-
cell: info => info.getValue(),
66+
cell: info => {
67+
const id = info.getValue();
68+
return (
69+
<TooltipProvider>
70+
<Tooltip>
71+
<TooltipTrigger asChild>
72+
<div className="max-w-[150px] truncate">{id}</div>
73+
</TooltipTrigger>
74+
<TooltipContent>
75+
<p>{id}</p>
76+
</TooltipContent>
77+
</Tooltip>
78+
</TooltipProvider>
79+
);
80+
},
6781
sortingFn: "alphanumeric",
6882
}),
6983
columnHelper.accessor("status", {
7084
header: "Status",
71-
cell: info => <Status state={info.getValue()} />,
85+
cell: info => <Status state={info.getValue()} showDuration={false} />,
7286
sortingFn: statusSortingFn,
7387
}),
88+
columnHelper.display({
89+
id: "duration",
90+
header: "Duration",
91+
cell: info => <Duration state={info.row.original.status} />,
92+
}),
93+
columnHelper.accessor("entrypoint", {
94+
header: "Entrypoint",
95+
cell: info => {
96+
const entry = info.getValue();
97+
if (!entry) return "-";
98+
return (
99+
<TooltipProvider>
100+
<Tooltip>
101+
<TooltipTrigger asChild>
102+
<div className="max-w-[200px] truncate">{entry}</div>
103+
</TooltipTrigger>
104+
<TooltipContent className="max-w-none text-sm">
105+
<p className="break-all font-mono">{entry}</p>
106+
</TooltipContent>
107+
</Tooltip>
108+
</TooltipProvider>
109+
);
110+
},
111+
sortingFn: "alphanumeric",
112+
}),
74113
columnHelper.accessor("start_sec", {
75114
header: "Start Time",
76115
cell: info => toHumanReadableDate(info.getValue()),
77116
sortingFn: "basic",
117+
}),
118+
columnHelper.accessor("runner", {
119+
header: "Engine",
120+
cell: info => getEngineName(info.getValue()),
121+
sortingFn: "alphanumeric",
122+
}),
123+
// @ts-ignore
124+
columnHelper.accessor("ray_dashboard_url", {
125+
header: "Ray UI",
126+
cell: info => {
127+
let url = info.getValue();
128+
if (url) {
129+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
130+
url = "http://" + url;
131+
}
132+
return (
133+
<a
134+
href={url}
135+
target="_blank"
136+
rel="noopener noreferrer"
137+
className="flex items-center gap-1 text-blue-500 hover:underline"
138+
onClick={(e) => e.stopPropagation()}
139+
>
140+
Open Ray UI <ExternalLinkIcon className="h-4 w-4" />
141+
</a>
142+
);
143+
}
144+
return null;
145+
},
146+
enableSorting: false,
78147
})
79148
];
80149

@@ -184,6 +253,7 @@ export default function QueryList() {
184253
columns,
185254
getCoreRowModel: getCoreRowModel(),
186255
getSortedRowModel: getSortedRowModel(),
256+
columnResizeMode: "onChange",
187257
initialState: {
188258
sorting: [
189259
{ id: "status", desc: false },
@@ -193,7 +263,7 @@ export default function QueryList() {
193263
});
194264

195265
const spacing = (obj: any) =>
196-
`px-[20px] ${obj.column.columnDef.accessorKey === STATUS ? "w-[60%]" : undefined}`;
266+
`px-[20px]`;
197267

198268
const handleRowClick = (queryId: string) => {
199269
router.push(`/query?id=${queryId}`);
@@ -208,23 +278,31 @@ export default function QueryList() {
208278
}
209279

210280
return (
211-
<div className="space-y-4 max-w-6xl mx-auto">
281+
<div className="space-y-4 max-w-full px-6 mx-auto">
212282
<Header />
213283

214-
<div className="border">
284+
<div className="border rounded-md overflow-hidden">
215285
<Table>
216-
<TableHeader>
286+
<TableHeader className="bg-white">
217287
{table.getHeaderGroups().map(headerGroup => (
218-
<TableRow key={headerGroup.id} className="">
219-
{headerGroup.headers.map(header => (
288+
<TableRow key={headerGroup.id} className="hover:bg-transparent border-b border-zinc-200">
289+
{headerGroup.headers.map((header, index) => (
220290
<TableHead
221291
key={header.id}
222-
className={`text-xs font-mono ${spacing(header)}`}
292+
className={`relative text-xs font-mono font-bold text-black ${spacing(header)} ${index !== headerGroup.headers.length - 1 ? "border-r border-zinc-200" : ""}`}
293+
style={{ width: header.getSize() }}
223294
>
224295
{flexRender(
225296
header.column.columnDef.header,
226297
header.getContext()
227298
)}
299+
<div
300+
onMouseDown={header.getResizeHandler()}
301+
onTouchStart={header.getResizeHandler()}
302+
className={`absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none bg-border/50 hover:bg-primary/50 ${
303+
header.column.getIsResizing() ? "bg-primary" : ""
304+
}`}
305+
/>
228306
</TableHead>
229307
))}
230308
</TableRow>
@@ -238,17 +316,16 @@ export default function QueryList() {
238316
className="hover:bg-zinc-800 transition-colors duration-50 cursor-pointer"
239317
onClick={() => handleRowClick(row.original.id)}
240318
>
241-
{row.getAllCells().map(cell => (
319+
{row.getAllCells().map((cell, index) => (
242320
<TableCell
243321
key={cell.id}
244-
className={`py-[15px] ${spacing(cell)}`}
322+
className={`py-[15px] ${spacing(cell)} ${index !== row.getAllCells().length - 1 ? "border-r border-zinc-800" : ""}`}
323+
style={{ width: cell.column.getSize() }}
245324
>
246-
<div className="truncate">
247-
{flexRender(
248-
cell.column.columnDef.cell,
249-
cell.getContext()
250-
)}
251-
</div>
325+
{flexRender(
326+
cell.column.columnDef.cell,
327+
cell.getContext()
328+
)}
252329
</TableCell>
253330
))}
254331
</TableRow>

src/daft-dashboard/frontend/src/app/queries/status.tsx

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { AnimatedFish, Naruto } from "@/components/icons";
2222

2323
interface StatusBadgeProps {
2424
state: QueryStatus;
25+
showDuration?: boolean;
2526
}
2627

2728
const StatusBadgeInner = ({
@@ -44,7 +45,7 @@ const StatusBadgeInner = ({
4445
<span className={`${main.className} ${textColor} font-bold text-sm`}>
4546
{label}
4647
</span>
47-
<span className={`${main.className} font-semibold text-sm`}>{text}</span>
48+
{text && <span className={`${main.className} font-semibold text-sm`}>{text}</span>}
4849
</div>
4950
);
5051

@@ -77,7 +78,7 @@ const Pending = () => (
7778
/>
7879
);
7980

80-
const Planning = ({ state }: { state: PlanningStatus }) => {
81+
const Planning = ({ state, showDuration }: { state: PlanningStatus, showDuration: boolean }) => {
8182
const [currentTime, setCurrentTime] = useState(() => Date.now());
8283

8384
useEffect(() => {
@@ -95,7 +96,7 @@ const Planning = ({ state }: { state: PlanningStatus }) => {
9596
<LoaderCircle size={16} strokeWidth={3} className="text-orange-500" />
9697
}
9798
label="Optimizing"
98-
text={` for ${toHumanReadableDuration(duration)}`}
99+
text={showDuration ? ` for ${toHumanReadableDuration(duration)}` : undefined}
99100
textColor="text-orange-500"
100101
/>
101102
);
@@ -111,7 +112,7 @@ const Setup = () => (
111112
/>
112113
);
113114

114-
const Running = ({ state }: { state: ExecutingStatus }) => {
115+
const Running = ({ state, showDuration }: { state: ExecutingStatus, showDuration: boolean }) => {
115116
const [currentTime, setCurrentTime] = useState(() => Date.now());
116117

117118
useEffect(() => {
@@ -127,7 +128,7 @@ const Running = ({ state }: { state: ExecutingStatus }) => {
127128
<StatusBadgeInner
128129
icon={<AnimatedFish />}
129130
label="Running"
130-
text={` for ${toHumanReadableDuration(duration)}`}
131+
text={showDuration ? ` for ${toHumanReadableDuration(duration)}` : undefined}
131132
textColor="text-(--daft-accent)"
132133
/>
133134
);
@@ -141,11 +142,11 @@ const Finalizing = () => (
141142
/>
142143
);
143144

144-
const Finished = ({ state }: { state: FinishedStatus }) => (
145+
const Finished = ({ state, showDuration }: { state: FinishedStatus, showDuration: boolean }) => (
145146
<StatusBadgeInner
146147
icon={<Naruto />}
147148
label="Finished"
148-
text={`in ${toHumanReadableDuration(state.duration_sec)}`}
149+
text={showDuration ? `in ${toHumanReadableDuration(state.duration_sec)}` : undefined}
149150
textColor="text-green-500"
150151
/>
151152
);
@@ -184,20 +185,47 @@ const Unknown = () => (
184185
/>
185186
);
186187

187-
export default function Status({ state }: StatusBadgeProps) {
188+
export function Duration({ state }: { state: QueryStatus }) {
189+
const [currentTime, setCurrentTime] = useState(() => Date.now());
190+
191+
useEffect(() => {
192+
// Only set interval if state is running
193+
if (state.status === "Optimizing" || state.status === "Executing") {
194+
const interval = setInterval(() => {
195+
setCurrentTime(Date.now());
196+
}, 1000);
197+
return () => clearInterval(interval);
198+
}
199+
}, [state.status]);
200+
201+
if (state.status === "Finished") {
202+
return <>{toHumanReadableDuration(state.duration_sec)}</>;
203+
}
204+
if (state.status === "Optimizing") {
205+
const duration = Math.round(currentTime / 1000 - state.plan_start_sec);
206+
return <>{toHumanReadableDuration(duration)}</>;
207+
}
208+
if (state.status === "Executing") {
209+
const duration = Math.round(currentTime / 1000 - state.exec_start_sec);
210+
return <>{toHumanReadableDuration(duration)}</>;
211+
}
212+
return <>-</>;
213+
}
214+
215+
export default function Status({ state, showDuration = true }: StatusBadgeProps) {
188216
switch (state.status) {
189217
case "Pending":
190218
return <Pending />;
191219
case "Optimizing":
192-
return <Planning state={state} />;
220+
return <Planning state={state} showDuration={showDuration} />;
193221
case "Setup":
194222
return <Setup />;
195223
case "Executing":
196-
return <Running state={state} />;
224+
return <Running state={state} showDuration={showDuration} />;
197225
case "Finalizing":
198226
return <Finalizing />;
199227
case "Finished":
200-
return <Finished state={state} />;
228+
return <Finished state={state} showDuration={showDuration} />;
201229
case "Failed":
202230
return <Failed state={state} />;
203231
case "Canceled":

0 commit comments

Comments
 (0)