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
92 changes: 55 additions & 37 deletions ui/src/app/[locale]/errors/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CheckCircle, RefreshCw, Copy } from "lucide-react";
import { format } from "date-fns";

/** Base path of the error-tracking endpoints. */
const ERRORS_ENDPOINT = "/api/v1/errors";
/** React Query cache key for the error list. */
const ERRORS_QUERY_KEY = "errors";
/** Filter value that disables source filtering. */
const ALL_SOURCES = "all";

type ErrorEvent = {
id: string;
source_component: string;
Expand All @@ -15,43 +23,53 @@ type ErrorEvent = {
created_at: string;
};

async function fetchErrorEvents(source: string): Promise<ErrorEvent[]> {
const url = source !== ALL_SOURCES
? `${ERRORS_ENDPOINT}?source=${encodeURIComponent(source)}`
: ERRORS_ENDPOINT;

const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch errors: ${res.status}`);
}
const data = await res.json();
return data ?? [];
}

async function resolveErrorEvent(id: string): Promise<void> {
const res = await fetch(`${ERRORS_ENDPOINT}/${encodeURIComponent(id)}/resolve`, {
method: "PATCH",
});
if (!res.ok) {
throw new Error(`Failed to resolve error: ${res.status}`);
}
}

export default function ErrorsPage() {
const [errors, setErrors] = useState<ErrorEvent[]>([]);
const [loading, setLoading] = useState(true);
const [filterSource, setFilterSource] = useState("all");

const fetchErrors = useCallback(async () => {
setLoading(true);
try {
const url = filterSource !== "all"
? `/api/v1/errors?source=${filterSource}`
: `/api/v1/errors`;

const res = await fetch(url);
if (res.ok) {
const data = await res.json();
setErrors(data || []);
}
} catch (e) {
console.error("Failed to fetch errors", e);
} finally {
setLoading(false);
}
}, [filterSource]);

useEffect(() => {
void fetchErrors();
}, [fetchErrors]);

const handleResolve = async (id: string) => {
try {
const res = await fetch(`/api/v1/errors/${id}/resolve`, { method: "PATCH" });
if (res.ok) {
await fetchErrors();
}
} catch (e) {
const [filterSource, setFilterSource] = useState(ALL_SOURCES);
const queryClient = useQueryClient();

const {
data: errors = [],
isFetching: loading,
refetch,
} = useQuery({
queryKey: [ERRORS_QUERY_KEY, filterSource],
queryFn: () => fetchErrorEvents(filterSource),
});

const resolveMutation = useMutation({
mutationFn: resolveErrorEvent,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: [ERRORS_QUERY_KEY] });
},
onError: (e) => {
console.error("Failed to resolve error", e);
}
},
});

const handleResolve = (id: string) => {
resolveMutation.mutate(id);
};

const getSeverityColor = (severity: string) => {
Expand Down Expand Up @@ -89,7 +107,7 @@ export default function ErrorsPage() {
<option value="worker">Worker</option>
</select>
<button
onClick={() => void fetchErrors()}
onClick={() => void refetch()}
className="p-2 hover:bg-white/10 rounded-md transition-colors"
>
<RefreshCw className={`w-5 h-5 ${loading ? "animate-spin" : ""}`} />
Expand Down
15 changes: 6 additions & 9 deletions ui/src/components/file-browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import {
HardDrive,
Check,
} from "lucide-react";
import { formatBytes } from "@/lib/utils";

/** Placeholder rendered where a size or date is unknown. */
const EMPTY_VALUE_LABEL = "—";

interface FileBrowserProps {
onSelect: (path: string) => void;
Expand Down Expand Up @@ -82,15 +86,8 @@ export function FileBrowser({ onSelect, selectedPath, mediaOnly = true }: FileBr
setSearchQuery("");
};

const formatSize = (bytes: number): string => {
if (bytes === 0) return "—";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
};

const formatDate = (timestamp: number): string => {
if (!timestamp) return "—";
if (!timestamp) return EMPTY_VALUE_LABEL;
return new Date(timestamp * 1000).toLocaleDateString();
};

Expand Down Expand Up @@ -250,7 +247,7 @@ export function FileBrowser({ onSelect, selectedPath, mediaOnly = true }: FileBr
</div>
{!entry.is_directory && (
<div className="text-xs text-muted-foreground flex items-center gap-2">
<span>{formatSize(entry.size)}</span>
<span>{formatBytes(entry.size, EMPTY_VALUE_LABEL)}</span>
<span>•</span>
<span>{formatDate(entry.mod_time)}</span>
{entry.extension && (
Expand Down
16 changes: 5 additions & 11 deletions ui/src/components/file-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useState, useCallback, useRef } from "react";
import { useMutation } from "@tanstack/react-query";
import { uploadFile, reportError, UploadProgress, UploadResponse } from "@/lib/api";
import { formatBytes } from "@/lib/utils";
import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button";
import {
Expand Down Expand Up @@ -68,7 +69,7 @@ export function FileUpload({
(file: File) => {
// Validate file size
if (file.size > maxSize) {
setError(`File size exceeds maximum of ${formatSize(maxSize)}`);
setError(`File size exceeds maximum of ${formatBytes(maxSize)}`);
setState("error");
return;
}
Expand Down Expand Up @@ -162,13 +163,6 @@ export function FileUpload({
return <File className="h-8 w-8 text-muted-foreground" />;
};

const formatSize = (bytes: number): string => {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
};

return (
<div className="space-y-4">
<input
Expand Down Expand Up @@ -212,7 +206,7 @@ export function FileUpload({
{isDragging ? "Drop your file here" : "Drag and drop your video file"}
</p>
<p className="text-sm text-muted-foreground mt-1">
or click to browse • Max {formatSize(maxSize)}
or click to browse • Max {formatBytes(maxSize)}
</p>
</div>
<div className="flex gap-2 flex-wrap justify-center text-xs text-muted-foreground">
Expand All @@ -238,7 +232,7 @@ export function FileUpload({
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{selectedFile.name}</p>
<div className="flex items-center gap-3 text-sm text-muted-foreground mt-1">
<span>{formatSize(selectedFile.size)}</span>
<span>{formatBytes(selectedFile.size)}</span>
<span>•</span>
<span>{selectedFile.type || "Unknown type"}</span>
</div>
Expand Down Expand Up @@ -275,7 +269,7 @@ export function FileUpload({
<p className="font-medium truncate">{selectedFile.name}</p>
<div className="flex items-center gap-3 text-sm text-muted-foreground mt-1">
<span>
{formatSize(progress.loaded)} / {formatSize(progress.total)}
{formatBytes(progress.loaded)} / {formatBytes(progress.total)}
</span>
<span>•</span>
<span className="text-violet-400 font-medium">{progress.percentage}%</span>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ const Checkbox = React.forwardRef<
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
Checkbox.displayName = "Checkbox"

export { Checkbox }
8 changes: 4 additions & 4 deletions ui/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
DialogOverlay.displayName = "DialogOverlay"

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
Expand All @@ -51,7 +51,7 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
DialogContent.displayName = "DialogContent"

const DialogHeader = ({
className,
Expand Down Expand Up @@ -94,7 +94,7 @@ const DialogTitle = React.forwardRef<
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
DialogTitle.displayName = "DialogTitle"

const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
Expand All @@ -106,7 +106,7 @@ const DialogDescription = React.forwardRef<
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
DialogDescription.displayName = "DialogDescription"

export {
Dialog,
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/ui/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ const Label = React.forwardRef<
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
Label.displayName = "Label"

export { Label }
11 changes: 9 additions & 2 deletions ui/src/components/ui/popover.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,15 @@ describe('Popover', () => {
expect(screen.getByText('Content')).toBeInTheDocument()
})

fireEvent.pointerDown(screen.getByTestId('outside'))
expect(handleOpenChange).toHaveBeenCalledWith(false)
// Radix defers the popover's dismissal to the click that follows the
// outside pointerdown, so the full interaction has to be simulated.
const outside = screen.getByTestId('outside')
fireEvent.pointerDown(outside)
fireEvent.click(outside)

await waitFor(() => {
expect(handleOpenChange).toHaveBeenCalledWith(false)
})
})
})

Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ const PopoverContent = React.forwardRef<
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
PopoverContent.displayName = "PopoverContent"

export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
2 changes: 1 addition & 1 deletion ui/src/components/ui/progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ const Progress = React.forwardRef<
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
Progress.displayName = "Progress"

export { Progress }
2 changes: 1 addition & 1 deletion ui/src/components/ui/scroll-area.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ describe('ScrollArea', () => {
describe('ScrollBar', () => {
describe('rendering', () => {
it('should have correct displayName', () => {
expect(ScrollBar.displayName).toBe('ScrollAreaScrollbar')
expect(ScrollBar.displayName).toBe('ScrollBar')
})
})

Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const ScrollArea = React.forwardRef<
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
ScrollArea.displayName = "ScrollArea"

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Scrollbar>,
Expand All @@ -43,6 +43,6 @@ const ScrollBar = React.forwardRef<
<ScrollAreaPrimitive.Thumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.Scrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName
ScrollBar.displayName = "ScrollBar"

export { ScrollArea, ScrollBar }
15 changes: 7 additions & 8 deletions ui/src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const SelectTrigger = React.forwardRef<
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
SelectTrigger.displayName = "SelectTrigger"

const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
Expand All @@ -47,7 +47,7 @@ const SelectScrollUpButton = React.forwardRef<
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
SelectScrollUpButton.displayName = "SelectScrollUpButton"

const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
Expand All @@ -64,8 +64,7 @@ const SelectScrollDownButton = React.forwardRef<
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
SelectScrollDownButton.displayName = "SelectScrollDownButton"

const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
Expand Down Expand Up @@ -97,7 +96,7 @@ const SelectContent = React.forwardRef<
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
SelectContent.displayName = "SelectContent"

const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
Expand All @@ -109,7 +108,7 @@ const SelectLabel = React.forwardRef<
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
SelectLabel.displayName = "SelectLabel"

const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
Expand All @@ -132,7 +131,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
SelectItem.displayName = "SelectItem"

const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
Expand All @@ -144,7 +143,7 @@ const SelectSeparator = React.forwardRef<
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
SelectSeparator.displayName = "SelectSeparator"

export {
Select,
Expand Down
Loading
Loading