chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,255 @@
import type { TaskRunStatus } from "@trigger.dev/database";
import { useEffect, useState } from "react";
import { useTypedFetcher } from "remix-typedjson";
import { ExitIcon } from "~/assets/icons/ExitIcon";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { CopyableText } from "~/components/primitives/CopyableText";
import { DateTimeAccurate } from "~/components/primitives/DateTime";
import { Header2 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import * as Property from "~/components/primitives/PropertyTable";
import { Spinner } from "~/components/primitives/Spinner";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { PacketDisplay } from "~/components/runs/v3/PacketDisplay";
import {
TaskRunStatusCombo,
descriptionForTaskRunStatus,
} from "~/components/runs/v3/TaskRunStatus";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
import type { loader as logDetailLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId";
import { v3RunSpanPath } from "~/utils/pathBuilder";
import { LogLevel } from "./LogLevel";
type LogDetailViewProps = {
logId: string;
// If we have the log entry from the list, we can display it immediately
initialLog?: LogEntry;
onClose: () => void;
searchTerm?: string;
};
type LogAttributes = Record<string, unknown> & {
error?: {
message?: string;
};
};
function getDisplayMessage(log: {
message: string;
level: string;
attributes?: LogAttributes;
}): string {
let message = log.message ?? "";
if (log.level === "ERROR") {
const maybeErrorMessage = log.attributes?.error?.message;
if (typeof maybeErrorMessage === "string" && maybeErrorMessage.length > 0) {
message = maybeErrorMessage;
}
}
return message;
}
function formatStringJSON(str: string): string {
return str
.replace(/\\n/g, "\n") // Converts literal "\n" to newline
.replace(/\\t/g, "\t"); // Converts literal "\t" to tab
}
export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDetailViewProps) {
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
const fetcher = useTypedFetcher<typeof logDetailLoader>();
const [error, setError] = useState<string | null>(null);
// Fetch full log details when logId changes
useEffect(() => {
if (!logId) return;
setError(null);
fetcher.load(
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${
environment.slug
}/logs/${encodeURIComponent(logId)}`
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [organization.slug, project.slug, environment.slug, logId]);
// Handle fetch errors
useEffect(() => {
if (fetcher.data && typeof fetcher.data === "object" && "error" in fetcher.data) {
setError(fetcher.data.error as string);
} else if (fetcher.state === "idle" && fetcher.data === null && !initialLog) {
setError("Failed to load log details");
} else {
setError(null);
}
}, [fetcher.data, initialLog, fetcher.state]);
const isLoading = fetcher.state === "loading";
const log = fetcher.data ?? initialLog;
const runStatus = fetcher.data?.runStatus;
const runPath = v3RunSpanPath(
organization,
project,
environment,
{ friendlyId: log?.runId ?? "" },
{ spanId: log?.spanId ?? "" }
);
if (isLoading && !log) {
return (
<div className="flex h-full items-center justify-center">
<Spinner />
</div>
);
}
if (!log) {
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b border-grid-dimmed py-2 pl-3 pr-2">
<Header2>Log Details</Header2>
<Button
onClick={onClose}
variant="minimal/small"
TrailingIcon={ExitIcon}
shortcut={{ key: "esc" }}
shortcutPosition="before-trailing-icon"
className="pl-1"
/>
</div>
<div className="flex flex-1 items-center justify-center">
<Paragraph className="text-text-dimmed">{error ?? "Log not found"}</Paragraph>
</div>
</div>
);
}
return (
<div className="grid h-full grid-rows-[auto_1fr] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between overflow-hidden border-b border-grid-dimmed py-2 pl-3 pr-2">
<Header2 className="truncate">{getDisplayMessage(log)}</Header2>
<Button
onClick={onClose}
variant="minimal/small"
TrailingIcon={ExitIcon}
shortcut={{ key: "esc" }}
shortcutPosition="before-trailing-icon"
className="pl-1"
/>
</div>
<div className="overflow-y-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
<DetailsTab log={log} runPath={runPath} runStatus={runStatus} searchTerm={searchTerm} />
</div>
</div>
);
}
function DetailsTab({
log,
runPath,
runStatus,
searchTerm,
}: {
log: LogEntry & {
attributes?: LogAttributes;
};
runPath: string;
runStatus?: TaskRunStatus;
searchTerm?: string;
}) {
let beautifiedAttributes: string | null = null;
if (log.attributes) {
beautifiedAttributes = JSON.stringify(log.attributes, null, 2);
beautifiedAttributes = formatStringJSON(beautifiedAttributes);
}
const showAttributes = beautifiedAttributes && beautifiedAttributes !== "{}";
const message = getDisplayMessage(log);
return (
<>
<Property.Table>
<Property.Item>
<Property.Label>Run ID</Property.Label>
<Property.Value>
<CopyableText value={log.runId} copyValue={log.runId} asChild />
<LinkButton
to={runPath}
variant="secondary/small"
shortcut={{ key: "v" }}
className="mt-2"
>
View full run
</LinkButton>
</Property.Value>
</Property.Item>
{runStatus && (
<Property.Item>
<Property.Label>Status</Property.Label>
<Property.Value>
<SimpleTooltip
button={<TaskRunStatusCombo status={runStatus} />}
content={descriptionForTaskRunStatus(runStatus)}
disableHoverableContent
className="mt-1"
/>
</Property.Value>
</Property.Item>
)}
<Property.Item>
<Property.Label>Task</Property.Label>
<Property.Value>
<CopyableText value={log.taskIdentifier} copyValue={log.taskIdentifier} asChild />
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Level</Property.Label>
<Property.Value>
<LogLevel level={log.level} />
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Timestamp</Property.Label>
<Property.Value>
<DateTimeAccurate date={log.triggeredTimestamp} />
</Property.Value>
</Property.Item>
</Property.Table>
{/* Message */}
<div className="mb-6 mt-3">
<PacketDisplay
data={message}
dataType="application/json"
title="Message"
searchTerm={searchTerm}
wrap={true}
/>
</div>
{/* Attributes - only available in full log detail */}
{showAttributes && beautifiedAttributes && (
<div className="mb-6">
<PacketDisplay
data={beautifiedAttributes}
dataType="application/json"
title="Attributes"
searchTerm={searchTerm}
/>
</div>
)}
</>
);
}
@@ -0,0 +1,16 @@
import { cn } from "~/utils/cn";
import { getLevelColor } from "~/utils/logUtils";
import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
export function LogLevel({ level }: { level: LogEntry["level"] }) {
return (
<span
className={cn(
"inline-flex items-center rounded border px-1 py-0.5 text-xxs font-medium uppercase tracking-wider",
getLevelColor(level)
)}
>
{level}
</span>
);
}
@@ -0,0 +1,135 @@
import * as Ariakit from "@ariakit/react";
import { IconListTree } from "@tabler/icons-react";
import { type ReactNode } from "react";
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
import {
SelectItem,
SelectList,
SelectPopover,
SelectProvider,
SelectTrigger,
shortcutFromIndex,
} from "~/components/primitives/Select";
import { useSearchParams } from "~/hooks/useSearchParam";
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: "TRACE", label: "Trace", color: "text-purple-400" },
{ level: "INFO", label: "Info", color: "text-blue-400" },
{ level: "WARN", label: "Warning", color: "text-warning" },
{ level: "ERROR", label: "Error", color: "text-error" },
{ level: "DEBUG", label: "Debug", color: "text-text-dimmed" },
];
// In the future we might add other levels or change which are available
function getAvailableLevels(): typeof allLogLevels {
return allLogLevels;
}
function getLevelBadgeColor(level: LogLevel): string {
switch (level) {
case "ERROR":
return "text-error bg-error/10 border-error/20";
case "WARN":
return "text-warning bg-warning/10 border-warning/20";
case "TRACE":
return "text-purple-400 bg-purple-500/10 border-purple-500/20";
case "DEBUG":
return "text-text-dimmed bg-background-raised border-border-bright";
case "INFO":
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
default:
return "text-text-dimmed bg-background-hover border-grid-bright";
}
}
const shortcut = { key: "l" };
export function LogsLevelFilter() {
const { values } = useSearchParams();
const selectedLevels = values("levels");
const hasLevels = selectedLevels.length > 0 && selectedLevels.some((v) => v !== "");
if (hasLevels) {
return <AppliedLevelFilter />;
}
return (
<LevelDropdown
trigger={
<SelectTrigger
icon={<IconListTree className="size-4" />}
variant="secondary/small"
shortcut={shortcut}
tooltipTitle="Filter by level"
className="pl-1.5"
>
<span className="ml-1">Level</span>
</SelectTrigger>
}
/>
);
}
function LevelDropdown({ trigger }: { trigger: ReactNode }) {
const { values, replace } = useSearchParams();
const handleChange = (values: string[]) => {
replace({ levels: values, cursor: undefined, direction: undefined });
};
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))]">
<SelectList>
{availableLevels.map((item, index) => (
<SelectItem
key={item.level}
value={item.level}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
<span
className={cn(
"inline-flex items-center rounded border px-1.5 py-0.5 text-xs font-medium uppercase",
getLevelBadgeColor(item.level)
)}
>
{item.level}
</span>
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
function AppliedLevelFilter() {
const { values, del } = useSearchParams();
const levels = values("levels");
if (levels.length === 0 || levels.every((v) => v === "")) {
return null;
}
return (
<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>
}
/>
);
}
@@ -0,0 +1,153 @@
import * as Ariakit from "@ariakit/react";
import { FingerPrintIcon } from "@heroicons/react/20/solid";
import { useCallback, useState } from "react";
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
import { Button } from "~/components/primitives/Buttons";
import { FormError } from "~/components/primitives/FormError";
import { Input } from "~/components/primitives/Input";
import { Label } from "~/components/primitives/Label";
import { SelectPopover, SelectProvider, SelectTrigger } from "~/components/primitives/Select";
import { useSearchParams } from "~/hooks/useSearchParam";
import { FilterMenuProvider } from "~/components/runs/v3/SharedFilters";
import { makeFriendlyIdValidator } from "~/utils/friendlyId";
const shortcut = { key: "i" };
const validateRunId = makeFriendlyIdValidator("run", "Run");
export function LogsRunIdFilter() {
const { value } = useSearchParams();
const runIdValue = value("runId");
if (runIdValue) {
return <AppliedRunIdFilter />;
}
return (
<FilterMenuProvider>
{(search, setSearch) => (
<RunIdDropdown
trigger={
<SelectTrigger
icon={<FingerPrintIcon className="size-4" />}
variant="secondary/small"
shortcut={shortcut}
tooltipTitle="Filter by run ID"
className="pl-1.5"
>
<span className="ml-1">Run ID</span>
</SelectTrigger>
}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
function RunIdDropdown({
trigger,
clearSearchValue,
onClose,
}: {
trigger: React.ReactNode;
clearSearchValue: () => void;
onClose?: () => void;
}) {
const [open, setOpen] = useState<boolean | undefined>();
const { value, replace } = useSearchParams();
const runIdValue = value("runId");
const [runId, setRunId] = useState(runIdValue);
const apply = useCallback(() => {
clearSearchValue();
replace({
cursor: undefined,
direction: undefined,
runId: runId === "" ? undefined : runId?.toString(),
});
setOpen(false);
}, [runId, replace, clearSearchValue]);
const error = runId ? validateRunId(runId) : undefined;
return (
<SelectProvider virtualFocus={true} open={open} setOpen={setOpen}>
{trigger}
<SelectPopover
hideOnEnter={false}
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
className="max-w-[min(32ch,var(--popover-available-width))]"
>
<div className="flex flex-col gap-4 p-3">
<div className="flex flex-col gap-1">
<Label>Run ID</Label>
<Input
placeholder="run_"
value={runId ?? ""}
onChange={(e) => setRunId(e.target.value)}
variant="small"
className="w-[27ch] font-mono"
spellCheck={false}
/>
{error ? <FormError>{error}</FormError> : null}
</div>
<div className="flex justify-between gap-1 border-t border-grid-dimmed pt-3">
<Button variant="tertiary/small" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
disabled={error !== undefined || !runId}
variant="secondary/small"
shortcut={{
modifiers: ["mod"],
key: "Enter",
enabledOnInputElements: true,
}}
onClick={() => apply()}
>
Apply
</Button>
</div>
</div>
</SelectPopover>
</SelectProvider>
);
}
function AppliedRunIdFilter() {
const { value, del } = useSearchParams();
const runId = value("runId");
if (!runId) {
return null;
}
return (
<FilterMenuProvider>
{(search, setSearch) => (
<RunIdDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
<AppliedFilter
label="Run ID"
icon={<FingerPrintIcon className="size-4" />}
value={runId}
onRemove={() => del(["runId", "cursor", "direction"])}
variant="secondary/small"
/>
</Ariakit.Select>
}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
@@ -0,0 +1,241 @@
import { ArrowPathIcon } from "@heroicons/react/20/solid";
import { useEffect, useRef, useState } from "react";
import { RunsIcon } from "~/assets/icons/RunsIcon";
import { LogLevelTooltipInfo } from "~/components/LogLevelTooltipInfo";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
import { cn } from "~/utils/cn";
import { highlightSearchText } from "~/utils/logUtils";
import { v3RunSpanPath } from "~/utils/pathBuilder";
import { DateTimeAccurate } from "../primitives/DateTime";
import { Paragraph } from "../primitives/Paragraph";
import { Spinner } from "../primitives/Spinner";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellMenu,
TableHeader,
TableHeaderCell,
TableRow,
type TableVariant,
} from "../primitives/Table";
import { LogLevel } from "./LogLevel";
type LogsTableProps = {
logs: LogEntry[];
searchTerm?: string;
isLoading?: boolean;
isLoadingMore?: boolean;
hasMore?: boolean;
onLoadMore?: () => void;
onCheckForMore?: () => void;
variant?: TableVariant;
selectedLogId?: string;
onLogSelect?: (logId: string) => void;
};
// Inner shadow for level highlighting (better scroll performance than border-l)
function getLevelBoxShadow(level: LogEntry["level"]): string {
switch (level) {
case "ERROR":
return "inset 2px 0 0 0 rgb(239, 68, 68)";
case "WARN":
return "inset 2px 0 0 0 rgb(234, 179, 8)";
case "INFO":
return "inset 2px 0 0 0 rgb(59, 130, 246)";
case "TRACE":
return "inset 2px 0 0 0 rgb(168, 85, 247)";
case "DEBUG":
default:
return "none";
}
}
export function LogsTable({
logs,
searchTerm,
isLoading = false,
isLoadingMore = false,
hasMore = false,
onLoadMore,
onCheckForMore,
selectedLogId,
onLogSelect,
}: LogsTableProps) {
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
const loadMoreRef = useRef<HTMLDivElement>(null);
const [showLoadMoreSpinner, setShowLoadMoreSpinner] = useState(false);
// Show load more spinner only after 0.2 seconds of loading time
useEffect(() => {
if (!isLoadingMore) {
setShowLoadMoreSpinner(false);
return;
}
const timer = setTimeout(() => {
setShowLoadMoreSpinner(true);
}, 200);
return () => clearTimeout(timer);
}, [isLoadingMore]);
// Intersection observer for infinite scroll
useEffect(() => {
if (!hasMore || isLoadingMore || !onLoadMore) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
onLoadMore();
}
},
{ threshold: 0.1 }
);
const currentRef = loadMoreRef.current;
if (currentRef) {
observer.observe(currentRef);
}
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, [hasMore, isLoadingMore, onLoadMore]);
return (
<div className="relative h-full overflow-auto border-t scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
<Table variant="compact/mono" containerClassName="overflow-visible" showTopBorder={false}>
<TableHeader className="sticky top-0 z-10">
<TableRow>
<TableHeaderCell className="min-w-48 whitespace-nowrap">Time</TableHeaderCell>
<TableHeaderCell className="min-w-24 whitespace-nowrap">Run</TableHeaderCell>
<TableHeaderCell className="min-w-32 whitespace-nowrap">Task</TableHeaderCell>
<TableHeaderCell
className="min-w-24 whitespace-nowrap"
tooltip={<LogLevelTooltipInfo />}
disableTooltipHoverableContent
>
Level
</TableHeaderCell>
<TableHeaderCell className="w-full min-w-0">Message</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{logs.length === 0 ? (
<BlankState isLoading={isLoading} onRefresh={() => window.location.reload()} />
) : (
logs.map((log) => {
const isSelected = selectedLogId === log.id;
const runPath = v3RunSpanPath(
organization,
project,
environment,
{ friendlyId: log.runId },
{ spanId: log.spanId }
);
const handleRowClick = () => onLogSelect?.(log.id);
return (
<TableRow
key={log.id}
className={cn(
"cursor-pointer transition-colors",
isSelected ? "bg-background-hover" : "hover:bg-background-dimmed"
)}
isSelected={isSelected}
>
<TableCell
className="whitespace-nowrap tabular-nums"
onClick={handleRowClick}
hasAction
style={{
boxShadow: getLevelBoxShadow(log.level),
}}
>
<DateTimeAccurate date={log.triggeredTimestamp} hour12={false} />
</TableCell>
<TableCell className="min-w-24 cursor-pointer" onClick={handleRowClick}>
<span className="font-mono text-xs">{log.runId}</span>
</TableCell>
<TableCell className="min-w-32" onClick={handleRowClick} hasAction>
<span className="font-mono text-xs">{log.taskIdentifier}</span>
</TableCell>
<TableCell onClick={handleRowClick} hasAction>
<LogLevel level={log.level} />
</TableCell>
<TableCell className="max-w-0 truncate" onClick={handleRowClick} hasAction>
<span className="block truncate font-mono text-xs" title={log.message}>
{highlightSearchText(log.message, searchTerm)}
</span>
</TableCell>
<TableCellMenu
className="pl-32"
hiddenButtons={
<LinkButton
to={runPath}
variant="minimal/small"
TrailingIcon={RunsIcon}
trailingIconClassName="text-text-bright"
className="h-5.5 pl-1.5 pr-2"
>
<span className="text-[0.6875rem] text-text-bright">View run</span>
</LinkButton>
}
/>
</TableRow>
);
})
)}
</TableBody>
</Table>
{/* Infinite scroll trigger */}
{hasMore && logs.length > 0 && (
<div ref={loadMoreRef} className="flex items-center justify-center py-12">
<div className={cn("flex items-center gap-2", !showLoadMoreSpinner && "invisible")}>
<Spinner /> <span className="text-text-dimmed">Loading more</span>
</div>
</div>
)}
{/* Show all logs message with check for more button */}
{!hasMore && logs.length > 0 && (
<div className="flex items-center justify-center py-12">
<div className="flex flex-col items-center gap-3">
<span className="text-text-dimmed">Showing all {logs.length} logs</span>
</div>
</div>
)}
</div>
);
}
function BlankState({ isLoading, onRefresh }: { isLoading?: boolean; onRefresh?: () => void }) {
if (isLoading) return <TableBlankRow colSpan={6}></TableBlankRow>;
const handleRefresh = onRefresh ?? (() => window.location.reload());
return (
<TableBlankRow colSpan={6}>
<div className="flex flex-col items-center justify-center gap-6">
<Paragraph className="w-auto" variant="base/bright">
No logs match your filters. Try refreshing or modifying your filters.
</Paragraph>
<div className="flex items-center gap-2">
<Button LeadingIcon={ArrowPathIcon} variant="tertiary/medium" onClick={handleRefresh}>
Refresh
</Button>
</div>
</div>
</TableBlankRow>
);
}
@@ -0,0 +1,175 @@
import type { TaskTriggerSource } from "@trigger.dev/database";
import type { ReactNode } from "react";
import { useMemo } from "react";
import * as Ariakit from "@ariakit/react";
import {
ComboBox,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectList,
SelectPopover,
SelectProvider,
SelectTrigger,
} from "~/components/primitives/Select";
import { useSearchParams } from "~/hooks/useSearchParam";
import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource";
import { TasksIcon } from "~/assets/icons/TasksIcon";
import { appliedSummary, FilterMenuProvider } from "~/components/runs/v3/SharedFilters";
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
const shortcut = { key: "t" };
type TaskOption = {
slug: string;
triggerSource: TaskTriggerSource;
isInLatestDeployment: boolean;
};
interface LogsTaskFilterProps {
possibleTasks: TaskOption[];
}
export function LogsTaskFilter({ possibleTasks }: LogsTaskFilterProps) {
const { values, replace: _replace, del } = useSearchParams();
const selectedTasks = values("tasks");
if (selectedTasks.length === 0 || selectedTasks.every((v) => v === "")) {
return (
<FilterMenuProvider>
{(search, setSearch) => (
<TasksDropdown
trigger={
<SelectTrigger
icon={<TasksIcon className="size-4" />}
variant="secondary/small"
shortcut={shortcut}
tooltipTitle="Filter by task"
className="pl-1.5"
>
<span className="ml-1">Tasks</span>
</SelectTrigger>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
possibleTasks={possibleTasks}
/>
)}
</FilterMenuProvider>
);
}
return (
<FilterMenuProvider>
{(search, setSearch) => (
<TasksDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
<AppliedFilter
label="Task"
icon={<TasksIcon className="size-4" />}
value={appliedSummary(
selectedTasks.map((v) => {
const task = possibleTasks.find((task) => task.slug === v);
return task ? task.slug : v;
})
)}
onRemove={() => del(["tasks", "cursor", "direction"])}
variant="secondary/small"
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
possibleTasks={possibleTasks}
/>
)}
</FilterMenuProvider>
);
}
function TasksDropdown({
trigger,
clearSearchValue,
searchValue,
onClose,
possibleTasks,
}: {
trigger: ReactNode;
clearSearchValue: () => void;
searchValue: string;
onClose?: () => void;
possibleTasks: TaskOption[];
}) {
const { values, replace } = useSearchParams();
const handleChange = (values: string[]) => {
clearSearchValue();
replace({ tasks: values, cursor: undefined, direction: undefined });
};
const filtered = useMemo(() => {
return possibleTasks.filter((item) => {
return item.slug.toLowerCase().includes(searchValue.toLowerCase());
});
}, [searchValue, possibleTasks]);
return (
<SelectProvider value={values("tasks")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(360px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder={"Filter by task..."} value={searchValue} />
<SelectList>
{filtered
.filter((item) => item.isInLatestDeployment)
.map((item) => (
<SelectItem
key={`${item.triggerSource}-${item.slug}`}
value={item.slug}
className="text-text-bright"
icon={
<TaskTriggerSourceIcon source={item.triggerSource} className="size-4 flex-none" />
}
>
{item.slug}
</SelectItem>
))}
{filtered.some((item) => !item.isInLatestDeployment) && (
<SelectGroup>
<SelectGroupLabel>Archived</SelectGroupLabel>
{filtered
.filter((item) => !item.isInLatestDeployment)
.map((item) => (
<SelectItem
key={`${item.triggerSource}-${item.slug}`}
value={item.slug}
className="text-text-bright"
icon={
<span className="opacity-50">
<TaskTriggerSourceIcon
source={item.triggerSource}
className="size-4 flex-none"
/>
</span>
}
>
{item.slug}
</SelectItem>
))}
</SelectGroup>
)}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
@@ -0,0 +1,59 @@
import * as Ariakit from "@ariakit/react";
import { SelectTrigger } from "~/components/primitives/Select";
import { useSearchParams } from "~/hooks/useSearchParam";
import { appliedSummary, FilterMenuProvider } from "~/components/runs/v3/SharedFilters";
import { filterIcon, VersionsDropdown } from "~/components/runs/v3/RunFilters";
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
const shortcut = { key: "v" };
export function LogsVersionFilter() {
const { values, del } = useSearchParams();
const selectedVersions = values("versions");
if (selectedVersions.length === 0 || selectedVersions.every((v) => v === "")) {
return (
<FilterMenuProvider>
{(search, setSearch) => (
<VersionsDropdown
trigger={
<SelectTrigger
icon={filterIcon("versions")}
variant="secondary/small"
shortcut={shortcut}
tooltipTitle="Filter by version"
className="pl-1.5"
>
<span className="ml-1">Versions</span>
</SelectTrigger>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
return (
<FilterMenuProvider>
{(search, setSearch) => (
<VersionsDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
<AppliedFilter
label="Versions"
icon={filterIcon("versions")}
value={appliedSummary(selectedVersions)}
onRemove={() => del(["versions", "cursor", "direction"])}
variant="secondary/small"
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}