Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
63 changes: 63 additions & 0 deletions apps/webapp/app/components/LogLevelTooltipInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { BookOpenIcon } from "@heroicons/react/20/solid";
import { LinkButton } from "./primitives/Buttons";
import { Header3 } from "./primitives/Headers";
import { Paragraph } from "./primitives/Paragraph";

export function LogLevelTooltipInfo() {
return (
<div className="flex max-w-xs flex-col gap-4 p-1 pb-2">
<div>
<Header3>Log Levels</Header3>
<Paragraph variant="small" className="text-text-dimmed">
Structured logging helps you debug and monitor your tasks.
</Paragraph>
</div>
<div>
<div className="mb-0.5">
<Header3 className="text-blue-400">Info</Header3>
</div>
<Paragraph variant="small" className="text-text-dimmed">
General informational messages about task execution.
</Paragraph>
</div>
<div>
<div className="mb-0.5">
<Header3 className="text-warning">Warn</Header3>
</div>
<Paragraph variant="small" className="text-text-dimmed">
Warning messages indicating potential issues that don't prevent execution.
</Paragraph>
</div>
<div>
<div className="mb-0.5">
<Header3 className="text-error">Error</Header3>
</div>
<Paragraph variant="small" className="text-text-dimmed">
Error messages for failures and exceptions during task execution.
</Paragraph>
</div>
<div>
<div className="mb-0.5">
<Header3 className="text-charcoal-400">Debug</Header3>
</div>
<Paragraph variant="small" className="text-text-dimmed">
Detailed diagnostic information for development and debugging.
</Paragraph>
</div>
<div className="border-t border-charcoal-700 pt-4">
<Header3>Tracing & Spans</Header3>
<Paragraph variant="small" className="text-text-dimmed">
Automatically track the flow of your code through task triggers, attempts, and HTTP
requests. Create custom traces to monitor specific operations.
</Paragraph>
</div>
<LinkButton
to="https://trigger.dev/docs/logging#tracing-and-spans"
variant="docs/small"
LeadingIcon={BookOpenIcon}
>
Read docs
</LinkButton>
</div>
);
}
64 changes: 32 additions & 32 deletions apps/webapp/app/components/logs/LogDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
import { getLevelColor, getKindColor, getKindLabel } from "~/utils/logUtils";
import { getLevelColor } from "~/utils/logUtils";
import { v3RunSpanPath, v3RunsPath, v3DeploymentVersionPath } from "~/utils/pathBuilder";
import type { loader as logDetailLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId";
import { TaskRunStatusCombo, descriptionForTaskRunStatus } from "~/components/runs/v3/TaskRunStatus";
Expand Down Expand Up @@ -94,16 +94,34 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet
const isLoading = fetcher.state === "loading";
const log = fetcher.data ?? initialLog;

// Handle Escape key to close panel
const runPath = v3RunSpanPath(
organization,
project,
environment,
{ friendlyId: log?.runId ?? "" },
{ spanId: log?.spanId ?? "" }
);

// Handle keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (target && (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.tagName === "SELECT" ||
target.contentEditable === "true"
)) {
return;
}

if (e.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
}, [onClose, log, runPath, isLoading]);

if (isLoading && !log) {
return (
Expand All @@ -129,36 +147,18 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet
);
}

const runPath = v3RunSpanPath(
organization,
project,
environment,
{ friendlyId: log.runId },
{ spanId: log.spanId }
);

return (
<div className="flex h-full flex-col overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between border-b border-grid-dimmed px-4 py-3">
<div className="flex items-center gap-2">
<span
className={cn(
"inline-flex items-center rounded border px-1.5 py-0.5 text-xs font-medium",
getKindColor(log.kind)
)}
>
{getKindLabel(log.kind)}
</span>
<span
className={cn(
"inline-flex items-center rounded border px-1.5 py-0.5 text-xs font-medium uppercase",
getLevelColor(log.level)
)}
>
{log.level}
</span>
</div>
<div className="flex items-center justify-between border-b border-grid-dimmed px-2 py-2">
<span
className={cn(
"inline-flex items-center rounded border px-1.5 py-0.5 text-xs font-medium uppercase tracking-wider",
getLevelColor(log.level)
)}
>
{log.level}
</span>
<Button variant="minimal/small" onClick={onClose} shortcut={{ key: "esc" }}>
<XMarkIcon className="size-5" />
</Button>
Expand All @@ -185,8 +185,8 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet
</TabButton>
</TabContainer>
<Link to={runPath} target="_blank" rel="noopener noreferrer">
<Button variant="secondary/small" LeadingIcon={ArrowTopRightOnSquareIcon}>
View Full Run
<Button variant="minimal/small" LeadingIcon={ArrowTopRightOnSquareIcon} shortcut={{ key: "v" }}>
View full run
</Button>
</Link>
</div>
Expand Down
125 changes: 39 additions & 86 deletions apps/webapp/app/components/logs/LogsLevelFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as Ariakit from "@ariakit/react";
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { type ReactNode, useMemo } from "react";
import { IconListTree } from "@tabler/icons-react";
import { type ReactNode } from "react";
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
import {
ComboBox,
SelectItem,
SelectList,
SelectPopover,
Expand All @@ -12,24 +11,20 @@ import {
shortcutFromIndex,
} from "~/components/primitives/Select";
import { useSearchParams } from "~/hooks/useSearchParam";
import { FilterMenuProvider, appliedSummary } from "~/components/runs/v3/SharedFilters";
import { appliedSummary } from "~/components/runs/v3/SharedFilters";
import type { LogLevel } from "~/presenters/v3/LogsListPresenter.server";
import { cn } from "~/utils/cn";

const allLogLevels: { level: LogLevel; label: string; color: string }[] = [
{ level: "ERROR", label: "Error", color: "text-error" },
{ level: "WARN", label: "Warning", color: "text-warning" },
{ level: "INFO", label: "Info", color: "text-blue-400" },
{ level: "CANCELLED", label: "Cancelled", color: "text-charcoal-400" },
{ level: "WARN", label: "Warning", color: "text-warning" },
{ level: "ERROR", label: "Error", color: "text-error" },
{ level: "DEBUG", label: "Debug", color: "text-charcoal-400" },
{ level: "TRACE", label: "Trace", color: "text-charcoal-500" },
];

function getAvailableLevels(showDebug: boolean): typeof allLogLevels {
if (showDebug) {
return allLogLevels;
}
return allLogLevels.filter((level) => level.level !== "DEBUG");
// In the future we might add other levels or change which are available
function getAvailableLevels(): typeof allLogLevels {
return allLogLevels;
}

function getLevelBadgeColor(level: LogLevel): string {
Expand All @@ -42,92 +37,57 @@ function getLevelBadgeColor(level: LogLevel): string {
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
case "INFO":
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
case "TRACE":
return "text-charcoal-500 bg-charcoal-800 border-charcoal-700";
case "CANCELLED":
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
default:
return "text-text-dimmed bg-charcoal-750 border-charcoal-700";
}
}

const shortcut = { key: "l" };

export function LogsLevelFilter({ showDebug = false }: { showDebug?: boolean }) {
export function LogsLevelFilter() {
const { values } = useSearchParams();
const selectedLevels = values("levels");
const hasLevels = selectedLevels.length > 0 && selectedLevels.some((v) => v !== "");

if (hasLevels) {
return <AppliedLevelFilter showDebug={showDebug} />;
return <AppliedLevelFilter/>;
}

return (
<FilterMenuProvider>
{(search, setSearch) => (
<LevelDropdown
trigger={
<SelectTrigger
icon={<ExclamationTriangleIcon className="size-4" />}
variant="secondary/small"
shortcut={shortcut}
tooltipTitle="Filter by level"
>
Level
</SelectTrigger>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
showDebug={showDebug}
/>
)}
</FilterMenuProvider>
<LevelDropdown
trigger={
<SelectTrigger
icon={<IconListTree className="size-4" />}
variant="secondary/small"
shortcut={shortcut}
tooltipTitle="Filter by level"
>
Level
</SelectTrigger>
}
/>
);
}

function LevelDropdown({
trigger,
clearSearchValue,
searchValue,
onClose,
showDebug = false,
}: {
trigger: ReactNode;
clearSearchValue: () => void;
searchValue: string;
onClose?: () => void;
showDebug?: boolean;
}) {
const { values, replace } = useSearchParams();

const handleChange = (values: string[]) => {
clearSearchValue();
replace({ levels: values, cursor: undefined, direction: undefined });
};

const availableLevels = getAvailableLevels(showDebug);
const filtered = useMemo(() => {
return availableLevels.filter((item) =>
item.label.toLowerCase().includes(searchValue.toLowerCase())
);
}, [searchValue, availableLevels]);
const availableLevels = getAvailableLevels();

return (
<SelectProvider value={values("levels")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder="Filter by level..." value={searchValue} />
<SelectPopover className="min-w-0 max-w-[min(240px,var(--popover-available-width))]">
<SelectList>
{filtered.map((item, index) => (
{availableLevels.map((item, index) => (
<SelectItem
key={item.level}
value={item.level}
Expand All @@ -149,7 +109,7 @@ function LevelDropdown({
);
}

function AppliedLevelFilter({ showDebug = false }: { showDebug?: boolean }) {
function AppliedLevelFilter() {
const { values, del } = useSearchParams();
const levels = values("levels");

Expand All @@ -158,25 +118,18 @@ function AppliedLevelFilter({ showDebug = false }: { showDebug?: boolean }) {
}

return (
<FilterMenuProvider>
{(search, setSearch) => (
<LevelDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
<AppliedFilter
label="Level"
icon={<ExclamationTriangleIcon className="size-4" />}
value={appliedSummary(levels)}
onRemove={() => del(["levels", "cursor", "direction"])}
variant="secondary/small"
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
showDebug={showDebug}
/>
)}
</FilterMenuProvider>
<LevelDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
<AppliedFilter
label="Level"
icon={<IconListTree className="size-4" />}
value={appliedSummary(levels)}
onRemove={() => del(["levels", "cursor", "direction"])}
variant="secondary/small"
/>
</Ariakit.Select>
}
/>
);
}
2 changes: 1 addition & 1 deletion apps/webapp/app/components/logs/LogsRunIdFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { useSearchParams } from "~/hooks/useSearchParam";
import { FilterMenuProvider } from "~/components/runs/v3/SharedFilters";

const shortcut = { key: "r" };
const shortcut = { key: "i" };

export function LogsRunIdFilter() {
const { value } = useSearchParams();
Expand Down
Loading
Loading