Skip to content

Commit 882154e

Browse files
committed
Query fixes and removed span details from logs side panel
1 parent 30a7ad1 commit 882154e

14 files changed

Lines changed: 1136 additions & 129 deletions

apps/webapp/app/components/logs/LogDetailView.tsx

Lines changed: 330 additions & 71 deletions
Large diffs are not rendered by default.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import * as Ariakit from "@ariakit/react";
2+
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
3+
import { type ReactNode, useMemo } from "react";
4+
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
5+
import {
6+
ComboBox,
7+
SelectItem,
8+
SelectList,
9+
SelectPopover,
10+
SelectProvider,
11+
SelectTrigger,
12+
shortcutFromIndex,
13+
} from "~/components/primitives/Select";
14+
import { useSearchParams } from "~/hooks/useSearchParam";
15+
import { FilterMenuProvider, appliedSummary } from "~/components/runs/v3/SharedFilters";
16+
import type { LogLevel } from "~/presenters/v3/LogsListPresenter.server";
17+
import { cn } from "~/utils/cn";
18+
19+
const logLevels: { level: LogLevel; label: string; color: string }[] = [
20+
{ level: "ERROR", label: "Error", color: "text-error" },
21+
{ level: "WARN", label: "Warning", color: "text-warning" },
22+
{ level: "INFO", label: "Info", color: "text-blue-400" },
23+
{ level: "LOG", label: "Log", color: "text-text-dimmed" },
24+
{ level: "DEBUG", label: "Debug", color: "text-charcoal-400" },
25+
{ level: "TRACE", label: "Trace", color: "text-charcoal-500" },
26+
];
27+
28+
function getLevelBadgeColor(level: LogLevel): string {
29+
switch (level) {
30+
case "ERROR":
31+
return "text-error bg-error/10 border-error/20";
32+
case "WARN":
33+
return "text-warning bg-warning/10 border-warning/20";
34+
case "DEBUG":
35+
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
36+
case "INFO":
37+
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
38+
case "TRACE":
39+
return "text-charcoal-500 bg-charcoal-800 border-charcoal-700";
40+
case "LOG":
41+
default:
42+
return "text-text-dimmed bg-charcoal-750 border-charcoal-700";
43+
}
44+
}
45+
46+
const shortcut = { key: "l" };
47+
48+
export function LogsLevelFilter() {
49+
const { values } = useSearchParams();
50+
const selectedLevels = values("levels");
51+
const hasLevels = selectedLevels.length > 0 && selectedLevels.some((v) => v !== "");
52+
53+
if (hasLevels) {
54+
return <AppliedLevelFilter />;
55+
}
56+
57+
return (
58+
<FilterMenuProvider>
59+
{(search, setSearch) => (
60+
<LevelDropdown
61+
trigger={
62+
<SelectTrigger
63+
icon={<ExclamationTriangleIcon className="size-4" />}
64+
variant="secondary/small"
65+
shortcut={shortcut}
66+
tooltipTitle="Filter by level"
67+
>
68+
Level
69+
</SelectTrigger>
70+
}
71+
searchValue={search}
72+
clearSearchValue={() => setSearch("")}
73+
/>
74+
)}
75+
</FilterMenuProvider>
76+
);
77+
}
78+
79+
function LevelDropdown({
80+
trigger,
81+
clearSearchValue,
82+
searchValue,
83+
onClose,
84+
}: {
85+
trigger: ReactNode;
86+
clearSearchValue: () => void;
87+
searchValue: string;
88+
onClose?: () => void;
89+
}) {
90+
const { values, replace } = useSearchParams();
91+
92+
const handleChange = (values: string[]) => {
93+
clearSearchValue();
94+
replace({ levels: values, cursor: undefined, direction: undefined });
95+
};
96+
97+
const filtered = useMemo(() => {
98+
return logLevels.filter((item) =>
99+
item.label.toLowerCase().includes(searchValue.toLowerCase())
100+
);
101+
}, [searchValue]);
102+
103+
return (
104+
<SelectProvider value={values("levels")} setValue={handleChange} virtualFocus={true}>
105+
{trigger}
106+
<SelectPopover
107+
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
108+
hideOnEscape={() => {
109+
if (onClose) {
110+
onClose();
111+
return false;
112+
}
113+
return true;
114+
}}
115+
>
116+
<ComboBox placeholder="Filter by level..." value={searchValue} />
117+
<SelectList>
118+
{filtered.map((item, index) => (
119+
<SelectItem
120+
key={item.level}
121+
value={item.level}
122+
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
123+
>
124+
<span
125+
className={cn(
126+
"inline-flex items-center rounded border px-1.5 py-0.5 text-xs font-medium uppercase",
127+
getLevelBadgeColor(item.level)
128+
)}
129+
>
130+
{item.level}
131+
</span>
132+
</SelectItem>
133+
))}
134+
</SelectList>
135+
</SelectPopover>
136+
</SelectProvider>
137+
);
138+
}
139+
140+
function AppliedLevelFilter() {
141+
const { values, del } = useSearchParams();
142+
const levels = values("levels");
143+
144+
if (levels.length === 0 || levels.every((v) => v === "")) {
145+
return null;
146+
}
147+
148+
return (
149+
<FilterMenuProvider>
150+
{(search, setSearch) => (
151+
<LevelDropdown
152+
trigger={
153+
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
154+
<AppliedFilter
155+
label="Level"
156+
icon={<ExclamationTriangleIcon className="size-4" />}
157+
value={appliedSummary(levels)}
158+
onRemove={() => del(["levels", "cursor", "direction"])}
159+
variant="secondary/small"
160+
/>
161+
</Ariakit.Select>
162+
}
163+
searchValue={search}
164+
clearSearchValue={() => setSearch("")}
165+
/>
166+
)}
167+
</FilterMenuProvider>
168+
);
169+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import * as Ariakit from "@ariakit/react";
2+
import { FingerPrintIcon } from "@heroicons/react/20/solid";
3+
import { useCallback, useState } from "react";
4+
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
5+
import { Button } from "~/components/primitives/Buttons";
6+
import { FormError } from "~/components/primitives/FormError";
7+
import { Input } from "~/components/primitives/Input";
8+
import { Label } from "~/components/primitives/Label";
9+
import {
10+
SelectPopover,
11+
SelectProvider,
12+
SelectTrigger,
13+
} from "~/components/primitives/Select";
14+
import { useSearchParams } from "~/hooks/useSearchParam";
15+
import { FilterMenuProvider } from "~/components/runs/v3/SharedFilters";
16+
17+
const shortcut = { key: "r" };
18+
19+
export function LogsRunIdFilter() {
20+
const { value } = useSearchParams();
21+
const runIdValue = value("runId");
22+
23+
if (runIdValue) {
24+
return <AppliedRunIdFilter />;
25+
}
26+
27+
return (
28+
<FilterMenuProvider>
29+
{(search, setSearch) => (
30+
<RunIdDropdown
31+
trigger={
32+
<SelectTrigger
33+
icon={<FingerPrintIcon className="size-4" />}
34+
variant="secondary/small"
35+
shortcut={shortcut}
36+
tooltipTitle="Filter by run ID"
37+
>
38+
Run ID
39+
</SelectTrigger>
40+
}
41+
clearSearchValue={() => setSearch("")}
42+
/>
43+
)}
44+
</FilterMenuProvider>
45+
);
46+
}
47+
48+
function RunIdDropdown({
49+
trigger,
50+
clearSearchValue,
51+
onClose,
52+
}: {
53+
trigger: React.ReactNode;
54+
clearSearchValue: () => void;
55+
onClose?: () => void;
56+
}) {
57+
const [open, setOpen] = useState<boolean | undefined>();
58+
const { value, replace } = useSearchParams();
59+
const runIdValue = value("runId");
60+
61+
const [runId, setRunId] = useState(runIdValue);
62+
63+
const apply = useCallback(() => {
64+
clearSearchValue();
65+
replace({
66+
cursor: undefined,
67+
direction: undefined,
68+
runId: runId === "" ? undefined : runId?.toString(),
69+
});
70+
71+
setOpen(false);
72+
}, [runId, replace, clearSearchValue]);
73+
74+
let error: string | undefined = undefined;
75+
if (runId) {
76+
if (!runId.startsWith("run_")) {
77+
error = "Run IDs start with 'run_'";
78+
} else if (runId.length !== 25 && runId.length !== 29) {
79+
error = "Run IDs are 25 or 29 characters long";
80+
}
81+
}
82+
83+
return (
84+
<SelectProvider virtualFocus={true} open={open} setOpen={setOpen}>
85+
{trigger}
86+
<SelectPopover
87+
hideOnEnter={false}
88+
hideOnEscape={() => {
89+
if (onClose) {
90+
onClose();
91+
return false;
92+
}
93+
return true;
94+
}}
95+
className="max-w-[min(32ch,var(--popover-available-width))]"
96+
>
97+
<div className="flex flex-col gap-4 p-3">
98+
<div className="flex flex-col gap-1">
99+
<Label>Run ID</Label>
100+
<Input
101+
placeholder="run_"
102+
value={runId ?? ""}
103+
onChange={(e) => setRunId(e.target.value)}
104+
variant="small"
105+
className="w-[27ch] font-mono"
106+
spellCheck={false}
107+
/>
108+
{error ? <FormError>{error}</FormError> : null}
109+
</div>
110+
<div className="flex justify-between gap-1 border-t border-grid-dimmed pt-3">
111+
<Button variant="tertiary/small" onClick={() => setOpen(false)}>
112+
Cancel
113+
</Button>
114+
<Button
115+
disabled={error !== undefined || !runId}
116+
variant="secondary/small"
117+
shortcut={{
118+
modifiers: ["mod"],
119+
key: "Enter",
120+
enabledOnInputElements: true,
121+
}}
122+
onClick={() => apply()}
123+
>
124+
Apply
125+
</Button>
126+
</div>
127+
</div>
128+
</SelectPopover>
129+
</SelectProvider>
130+
);
131+
}
132+
133+
function AppliedRunIdFilter() {
134+
const { value, del } = useSearchParams();
135+
136+
const runId = value("runId");
137+
if (!runId) {
138+
return null;
139+
}
140+
141+
return (
142+
<FilterMenuProvider>
143+
{(search, setSearch) => (
144+
<RunIdDropdown
145+
trigger={
146+
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
147+
<AppliedFilter
148+
label="Run ID"
149+
icon={<FingerPrintIcon className="size-4" />}
150+
value={runId}
151+
onRemove={() => del(["runId", "cursor", "direction"])}
152+
variant="secondary/small"
153+
/>
154+
</Ariakit.Select>
155+
}
156+
clearSearchValue={() => setSearch("")}
157+
/>
158+
)}
159+
</FilterMenuProvider>
160+
);
161+
}

0 commit comments

Comments
 (0)