Improved run page and replaying (#1240)
* PropertyTable component changed to use sub components * Separate run span component * Early WIP on tabs that use search query * Shortcut key tabs for the span panel * The span timeline is working * When runs get expired, update the OTEL event with an error * Improved the expired error message * Reveal env vars when editing * Tightened things up a bit * Added detail tab properties * Progress dashed line * Move the env label next to the Run number title * Top level cancel/replay buttons * Replay with a different payload and environment * Fix for non json payloads * Hide the clear/copy buttons * UI improvements with large payloads * Close the panels when you replay/cancel * Added the new timeline to spans * Use the u-turn left icon for replay * Added an index for spanId on TaskRun * Remove replay/cancel buttons the span view * Replay shortcut works inside the code editor * Split the log/span inspector between Overview and Detail as well * More improvements to the inspector * Context and output improvements * Focus on run working * Added version to run.ctx * Added some padding to the detail view * Added context tab with shortcut * Only load the replay data when the dialog is open * Replaying uses the tags from the original run * Links are now text links * Removed version links for now because we don’t have dropdown filters for them yet * Tabs are now outside of the scrollview * The inspector is now 30% of the width by default * Allow replaying and editing SuperJSON payloads * Deleted unused CodeGroup file * Increase the tags limit to 5, do the limiting on the server * The admin tooltip now always shows basic org, project and user info * Fix for schedule inspector disabled state layout * Remove new unused span metadata and context * Removed unused import
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Added version to ctx.run
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -5,8 +6,13 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "~/components/primitives/Tooltip";
|
||||
import { useIsImpersonating } from "~/hooks/useOrganizations";
|
||||
import { useHasAdminAccess } from "~/hooks/useUser";
|
||||
import {
|
||||
useIsImpersonating,
|
||||
useOptionalOrganization,
|
||||
useOrganization,
|
||||
} from "~/hooks/useOrganizations";
|
||||
import { useOptionalProject, useProject } from "~/hooks/useProject";
|
||||
import { useHasAdminAccess, useUser } from "~/hooks/useUser";
|
||||
|
||||
export function AdminDebugTooltip({ children }: { children: React.ReactNode }) {
|
||||
const hasAdminAccess = useHasAdminAccess();
|
||||
@@ -22,10 +28,46 @@ export function AdminDebugTooltip({ children }: { children: React.ReactNode }) {
|
||||
<TooltipTrigger>
|
||||
<ShieldCheckIcon className="size-5" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex max-h-[90vh] items-center gap-1 overflow-y-auto">
|
||||
{children}
|
||||
<TooltipContent className="max-h-[90vh] overflow-y-auto">
|
||||
<Content>{children}</Content>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content({ children }: { children: React.ReactNode }) {
|
||||
const organization = useOptionalOrganization();
|
||||
const project = useOptionalProject();
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 divide-y divide-slate-700">
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>User ID</Property.Label>
|
||||
<Property.Value>{user.id}</Property.Value>
|
||||
</Property.Item>
|
||||
{organization && (
|
||||
<Property.Item>
|
||||
<Property.Label>Org ID</Property.Label>
|
||||
<Property.Value>{organization.id}</Property.Value>
|
||||
</Property.Item>
|
||||
)}
|
||||
{project && (
|
||||
<>
|
||||
<Property.Item>
|
||||
<Property.Label>Project ID</Property.Label>
|
||||
<Property.Value>{project.id}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Project ref</Property.Label>
|
||||
<Property.Value>{project.ref}</Property.Value>
|
||||
</Property.Item>
|
||||
</>
|
||||
)}
|
||||
</Property.Table>
|
||||
<div className="pt-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -241,16 +241,12 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
onMouseEnter={() => setMouseOver(true)}
|
||||
onMouseLeave={() => setMouseOver(false)}
|
||||
className={cn(
|
||||
"absolute right-3 z-50 transition-colors duration-100 hover:cursor-pointer",
|
||||
showChrome ? "top-10" : "top-3",
|
||||
"absolute right-3 z-50 transition-colors duration-100 hover:cursor-pointer",
|
||||
showChrome ? "top-10" : "top-2.5",
|
||||
copied ? "text-emerald-500" : "text-charcoal-500 hover:text-charcoal-300"
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
<ClipboardCheck className="h-5 w-5" />
|
||||
) : (
|
||||
<Clipboard className="h-5 w-5" />
|
||||
)}
|
||||
{copied ? <ClipboardCheck className="size-4" /> : <Clipboard className="size-4" />}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="text-xs">
|
||||
{copied ? "Copied" : "Copy"}
|
||||
@@ -391,8 +387,8 @@ function Chrome({ title }: { title?: string }) {
|
||||
|
||||
export function TitleRow({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<Paragraph variant="base/bright" className="w-full border-b border-grid-dimmed py-2.5">
|
||||
<div className="flex items-center justify-between px-3">
|
||||
<Paragraph variant="small/bright" className="w-full border-b border-grid-dimmed py-2">
|
||||
{title}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
@@ -107,39 +107,51 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
}, 1500);
|
||||
}, [view]);
|
||||
|
||||
const showButtons = showClearButton || showCopyButton;
|
||||
|
||||
return (
|
||||
<div className={cn(opts.className, "grid grid-rows-[2.5rem_1fr]")}>
|
||||
<div className="mx-3 flex items-center justify-end gap-2 border-b border-grid-dimmed">
|
||||
{showClearButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal/small"
|
||||
TrailingIcon={TrashIcon}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clear();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
{showCopyButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal/small"
|
||||
TrailingIcon={copied ? CheckIcon : ClipboardIcon}
|
||||
trailingIconClassName={copied ? "text-green-500 group-hover:text-green-500" : undefined}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
copy();
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
opts.className,
|
||||
"grid",
|
||||
showButtons ? "grid-rows-[2.5rem_1fr]" : "grid-rows-[1fr]"
|
||||
)}
|
||||
>
|
||||
{showButtons && (
|
||||
<div className="mx-3 flex items-center justify-end gap-2 border-b border-grid-dimmed">
|
||||
{showClearButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal/small"
|
||||
TrailingIcon={TrashIcon}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clear();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
{showCopyButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal/small"
|
||||
TrailingIcon={copied ? CheckIcon : ClipboardIcon}
|
||||
trailingIconClassName={
|
||||
copied ? "text-green-500 group-hover:text-green-500" : undefined
|
||||
}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
copy();
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="w-full overflow-auto"
|
||||
ref={editor}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { LoadingBarDivider } from "./LoadingBarDivider";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { Tabs, TabsProps } from "./Tabs";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type WithChildren = {
|
||||
children: React.ReactNode;
|
||||
@@ -34,7 +35,7 @@ export function NavBar({ children }: WithChildren) {
|
||||
}
|
||||
|
||||
type PageTitleProps = {
|
||||
title: string;
|
||||
title: ReactNode;
|
||||
backButton?: {
|
||||
to: string;
|
||||
text: string;
|
||||
|
||||
@@ -1,40 +1,31 @@
|
||||
import { ReactNode } from "react";
|
||||
import { type ReactNode } from "react";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function PropertyTable({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
type ChildrenClassName = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("grid grid-cols-[auto,1fr] items-center gap-x-4 gap-y-2", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type PropertyProps = {
|
||||
label: ReactNode;
|
||||
labelClassName?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function Property({ label, labelClassName, children }: PropertyProps) {
|
||||
return (
|
||||
<>
|
||||
<div className={labelClassName}>
|
||||
{typeof label === "string" ? <Paragraph variant="small">{label}</Paragraph> : label}
|
||||
</div>
|
||||
<div>
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant="small/bright">{children}</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
function PropertyTable({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return <div className={cn("flex flex-col gap-y-3", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
function PropertyItem({ children, className }: ChildrenClassName) {
|
||||
return <div className={cn("flex flex-col gap-0 text-sm", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
function PropertyLabel({ children, className }: ChildrenClassName) {
|
||||
return <div className={cn("font-medium text-text-bright", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
function PropertyValue({ children, className }: ChildrenClassName) {
|
||||
return <div className={cn("text-text-dimmed", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export {
|
||||
PropertyTable as Table,
|
||||
PropertyItem as Item,
|
||||
PropertyLabel as Label,
|
||||
PropertyValue as Value,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { NavLink, useLocation } from "@remix-run/react";
|
||||
import { Link, NavLink, useLocation } from "@remix-run/react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ReactNode, useRef } from "react";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { projectPubSub } from "~/v3/services/projectPubSub.server";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
|
||||
export type TabsProps = {
|
||||
tabs: {
|
||||
@@ -13,28 +18,106 @@ export type TabsProps = {
|
||||
|
||||
export function Tabs({ tabs, className, layoutId }: TabsProps) {
|
||||
return (
|
||||
<div className={cn(`flex flex-row gap-x-6 border-b border-grid-bright`, className)}>
|
||||
<TabContainer className={className}>
|
||||
{tabs.map((tab, index) => (
|
||||
<NavLink key={index} to={tab.to} className="group flex flex-col items-center pt-1" end>
|
||||
{({ isActive, isPending }) => (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm transition duration-200",
|
||||
isActive || isPending ? "text-indigo-500" : "text-charcoal-200"
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</span>
|
||||
{isActive || isPending ? (
|
||||
<motion.div layoutId={layoutId} className="mt-1 h-0.5 w-full bg-indigo-500" />
|
||||
) : (
|
||||
<div className="mt-1 h-0.5 w-full bg-charcoal-500 opacity-0 transition duration-200 group-hover:opacity-100" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
<TabLink key={index} to={tab.to} layoutId={layoutId}>
|
||||
{tab.label}
|
||||
</TabLink>
|
||||
))}
|
||||
</TabContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabContainer({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={cn(`flex flex-row gap-x-6 border-b border-grid-bright`, className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabLink({
|
||||
to,
|
||||
children,
|
||||
layoutId,
|
||||
}: {
|
||||
to: string;
|
||||
children: ReactNode;
|
||||
layoutId: string;
|
||||
}) {
|
||||
return (
|
||||
<NavLink to={to} className="group flex flex-col items-center pt-1" end>
|
||||
{({ isActive, isPending }) => {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm transition duration-200",
|
||||
isActive || isPending ? "text-text-bright" : "text-text-bright"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
{isActive || isPending ? (
|
||||
<motion.div layoutId={layoutId} className="mt-1 h-0.5 w-full bg-indigo-500" />
|
||||
) : (
|
||||
<div className="mt-1 h-0.5 w-full bg-charcoal-500 opacity-0 transition duration-200 group-hover:opacity-100" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabButton({
|
||||
isActive,
|
||||
layoutId,
|
||||
shortcut,
|
||||
...props
|
||||
}: {
|
||||
isActive: boolean;
|
||||
shortcut?: ShortcutDefinition;
|
||||
layoutId: string;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
if (shortcut) {
|
||||
useShortcutKeys({
|
||||
shortcut: shortcut,
|
||||
action: () => {
|
||||
if (ref.current) {
|
||||
ref.current.click();
|
||||
}
|
||||
},
|
||||
disabled: props.disabled,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn("group flex flex-col items-center pt-1", props.className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm transition duration-200",
|
||||
isActive ? "text-text-bright" : "text-text-bright"
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
{shortcut && <ShortcutKey className={cn("")} shortcut={shortcut} variant={"small"} />}
|
||||
</div>
|
||||
{isActive ? (
|
||||
<motion.div layoutId={layoutId} className="mt-1 h-0.5 w-full bg-indigo-500" />
|
||||
) : (
|
||||
<div className="mt-1 h-0.5 w-full bg-charcoal-500 opacity-0 transition duration-200 group-hover:opacity-100" />
|
||||
)}
|
||||
</>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ export function LiveTimer({
|
||||
}, updateInterval);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [startTime]);
|
||||
}, [startTime, endTime]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{formatDuration(startTime, now, {
|
||||
{formatDuration(startTime, endTime ?? now, {
|
||||
style: "short",
|
||||
maxDecimalPoints: 0,
|
||||
units: ["d", "h", "m", "s"],
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { ArrowPathIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useFetcher, useNavigation } from "@remix-run/react";
|
||||
import { Form, useFetcher, useNavigation, useSubmit } from "@remix-run/react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { UseDataFunctionReturn, useTypedFetcher } from "remix-typedjson";
|
||||
import { JSONEditor } from "~/components/code/JSONEditor";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
} from "~/components/primitives/Dialog";
|
||||
import { DialogContent, DialogDescription, DialogHeader } from "~/components/primitives/Dialog";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||
import { ButtonSpinner, Spinner } from "~/components/primitives/Spinner";
|
||||
import { type loader } from "~/routes/resources.taskruns.$runParam.replay";
|
||||
|
||||
type ReplayRunDialogProps = {
|
||||
runFriendlyId: string;
|
||||
@@ -14,31 +18,141 @@ type ReplayRunDialogProps = {
|
||||
};
|
||||
|
||||
export function ReplayRunDialog({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const formAction = `/resources/taskruns/${runFriendlyId}/replay`;
|
||||
const isLoading = navigation.formAction === formAction;
|
||||
|
||||
return (
|
||||
<DialogContent key="replay">
|
||||
<DialogHeader>Replay this run?</DialogHeader>
|
||||
<DialogDescription>
|
||||
Replaying a run will create a new run with the same payload and environment as the original.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Form action={formAction} method="post">
|
||||
<input type="hidden" name="failedRedirect" value={failedRedirect} />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary/small"
|
||||
LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon}
|
||||
disabled={isLoading}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
>
|
||||
{isLoading ? "Replaying..." : "Replay run"}
|
||||
</Button>
|
||||
</Form>
|
||||
</DialogFooter>
|
||||
<DialogContent key={`replay`} className="md:max-w-3xl">
|
||||
<ReplayContent runFriendlyId={runFriendlyId} failedRedirect={failedRedirect} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
function ReplayContent({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) {
|
||||
const fetcher = useTypedFetcher<typeof loader>();
|
||||
const isLoading = fetcher.state !== "idle";
|
||||
|
||||
useEffect(() => {
|
||||
fetcher.load(`/resources/taskruns/${runFriendlyId}/replay`);
|
||||
}, [runFriendlyId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>Replay this run</DialogHeader>
|
||||
{isLoading ? (
|
||||
<div className="grid place-items-center p-6">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : fetcher.data ? (
|
||||
<ReplayForm
|
||||
{...fetcher.data}
|
||||
failedRedirect={failedRedirect}
|
||||
runFriendlyId={runFriendlyId}
|
||||
/>
|
||||
) : (
|
||||
<>Failed to get run data</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ReplayForm({
|
||||
payload,
|
||||
payloadType,
|
||||
environment,
|
||||
environments,
|
||||
failedRedirect,
|
||||
runFriendlyId,
|
||||
}: UseDataFunctionReturn<typeof loader> & { failedRedirect: string; runFriendlyId: string }) {
|
||||
const navigation = useNavigation();
|
||||
const submit = useSubmit();
|
||||
const currentJson = useRef<string>(payload);
|
||||
const formAction = `/resources/taskruns/${runFriendlyId}/replay`;
|
||||
const isSubmitting = navigation.formAction === formAction;
|
||||
|
||||
const editablePayload =
|
||||
payloadType === "application/json" || payloadType === "application/super+json";
|
||||
|
||||
const submitForm = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const data: Record<string, string> = {
|
||||
environment: formData.get("environment") as string,
|
||||
failedRedirect: formData.get("failedRedirect") as string,
|
||||
};
|
||||
|
||||
if (editablePayload) {
|
||||
data.payload = currentJson.current;
|
||||
}
|
||||
|
||||
submit(data, {
|
||||
action: formAction,
|
||||
method: "post",
|
||||
});
|
||||
e.preventDefault();
|
||||
},
|
||||
[currentJson]
|
||||
);
|
||||
|
||||
return (
|
||||
<Form action={formAction} method="post" onSubmit={(e) => submitForm(e)} className="pt-2">
|
||||
{editablePayload ? (
|
||||
<>
|
||||
<Header3 spacing>Payload</Header3>
|
||||
<div className="mb-3 max-h-[70vh] overflow-y-auto rounded-sm border border-grid-dimmed bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<JSONEditor
|
||||
defaultValue={currentJson.current}
|
||||
readOnly={false}
|
||||
basicSetup
|
||||
onChange={(v) => {
|
||||
currentJson.current = v;
|
||||
}}
|
||||
showClearButton={false}
|
||||
showCopyButton={false}
|
||||
height="100%"
|
||||
min-height="100%"
|
||||
max-height="100%"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<InputGroup>
|
||||
<Label>Environment</Label>
|
||||
<Select
|
||||
id="environment"
|
||||
name="environment"
|
||||
placeholder="Select an environment"
|
||||
defaultValue={environment.id}
|
||||
items={environments}
|
||||
dropdownIcon
|
||||
variant="tertiary/medium"
|
||||
className="w-fit pl-2"
|
||||
text={(value) => {
|
||||
const env = environments.find((env) => env.id === value)!;
|
||||
return (
|
||||
<div className="flex items-center pr-2">
|
||||
<EnvironmentLabel environment={env} userName={env.userName} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{(matches) =>
|
||||
matches.map((env) => (
|
||||
<SelectItem key={env.id} value={env.id}>
|
||||
<EnvironmentLabel environment={env} userName={env.userName} />
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</InputGroup>
|
||||
<input type="hidden" name="failedRedirect" value={failedRedirect} />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary/medium"
|
||||
LeadingIcon={isSubmitting ? ButtonSpinner : undefined}
|
||||
disabled={isSubmitting}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter", enabledOnInputElements: true }}
|
||||
className="mt-5"
|
||||
>
|
||||
{isSubmitting ? "Replaying..." : "Replay run"}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTimeAccurate } from "~/components/primitives/DateTime";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
|
||||
type SpanEventsProps = {
|
||||
@@ -34,8 +34,8 @@ function SpanEventHeader({
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Header2 className={titleClassName}>{title}</Header2>
|
||||
<Paragraph variant="small">
|
||||
<Header3 className={titleClassName}>{title}</Header3>
|
||||
<Paragraph variant="extra-small">
|
||||
<DateTimeAccurate date={time} />
|
||||
</Paragraph>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ function SpanEvent({ spanEvent }: { spanEvent: OtelSpanEvent }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SpanEventError({
|
||||
export function SpanEventError({
|
||||
spanEvent,
|
||||
exception,
|
||||
}: {
|
||||
@@ -65,7 +65,7 @@ function SpanEventError({
|
||||
exception: ExceptionEventProperties;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 p-3">
|
||||
<div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 px-3 pb-3 pt-2">
|
||||
<SpanEventHeader
|
||||
title={exception.type ?? "Error"}
|
||||
time={spanEvent.time}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { prisma } from "~/db.server";
|
||||
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
|
||||
|
||||
export const MAX_TAGS_PER_RUN = 5;
|
||||
|
||||
export async function createTag({ tag, projectId }: { tag: string; projectId: string }) {
|
||||
if (tag.trim().length === 0) return;
|
||||
return prisma.taskRunTag.upsert({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { millisecondsToNanoseconds } from "@trigger.dev/core/v3";
|
||||
import { createTreeFromFlatItems, flattenTree } from "~/components/primitives/TreeView/TreeView";
|
||||
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
import { eventRepository } from "~/v3/eventRepository.server";
|
||||
@@ -33,6 +34,7 @@ export class RunPresenter {
|
||||
traceId: true,
|
||||
spanId: true,
|
||||
friendlyId: true,
|
||||
status: true,
|
||||
runtimeEnvironment: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -71,6 +73,9 @@ export class RunPresenter {
|
||||
number: run.number,
|
||||
friendlyId: run.friendlyId,
|
||||
traceId: run.traceId,
|
||||
spanId: run.spanId,
|
||||
status: run.status,
|
||||
isFinished: FINISHED_STATUSES.includes(run.status),
|
||||
environment: {
|
||||
id: run.runtimeEnvironment.id,
|
||||
organizationId: run.runtimeEnvironment.organizationId,
|
||||
@@ -127,6 +132,9 @@ export class RunPresenter {
|
||||
number: run.number,
|
||||
friendlyId: run.friendlyId,
|
||||
traceId: run.traceId,
|
||||
spanId: run.spanId,
|
||||
status: run.status,
|
||||
isFinished: FINISHED_STATUSES.includes(run.status),
|
||||
environment: {
|
||||
id: run.runtimeEnvironment.id,
|
||||
organizationId: run.runtimeEnvironment.organizationId,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { prettyPrintPacket } from "@trigger.dev/core/v3";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Context, MachinePresetName, prettyPrintPacket } from "@trigger.dev/core/v3";
|
||||
import { FINISHED_STATUSES, RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { eventRepository } from "~/v3/eventRepository.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { machineDefinition } from "@trigger.dev/platform/v3";
|
||||
import { machinePresetFromName } from "~/v3/machinePresets.server";
|
||||
|
||||
type Result = Awaited<ReturnType<SpanPresenter["call"]>>;
|
||||
export type Span = NonNullable<Result>["event"];
|
||||
export type Span = NonNullable<NonNullable<Result>["span"]>;
|
||||
export type SpanRun = NonNullable<NonNullable<Result>["run"]>;
|
||||
|
||||
export class SpanPresenter extends BasePresenter {
|
||||
public async call({
|
||||
@@ -30,6 +33,223 @@ export class SpanPresenter extends BasePresenter {
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
const run = await this.getRun(spanId);
|
||||
if (run) {
|
||||
return {
|
||||
type: "run" as const,
|
||||
run,
|
||||
};
|
||||
}
|
||||
|
||||
//get the run
|
||||
const span = await this.getSpan(runFriendlyId, spanId);
|
||||
|
||||
if (!span) {
|
||||
throw new Error("Span not found");
|
||||
}
|
||||
|
||||
return {
|
||||
type: "span" as const,
|
||||
span,
|
||||
};
|
||||
}
|
||||
|
||||
async getRun(spanId: string) {
|
||||
const run = await this._replica.taskRun.findFirst({
|
||||
select: {
|
||||
traceId: true,
|
||||
//metadata
|
||||
number: true,
|
||||
taskIdentifier: true,
|
||||
friendlyId: true,
|
||||
isTest: true,
|
||||
tags: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
machinePreset: true,
|
||||
lockedToVersion: {
|
||||
select: {
|
||||
version: true,
|
||||
sdkVersion: true,
|
||||
},
|
||||
},
|
||||
//status + duration
|
||||
status: true,
|
||||
startedAt: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
queuedAt: true,
|
||||
//idempotency
|
||||
idempotencyKey: true,
|
||||
//delayed
|
||||
delayUntil: true,
|
||||
//ttl
|
||||
ttl: true,
|
||||
expiredAt: true,
|
||||
//queue
|
||||
queue: true,
|
||||
concurrencyKey: true,
|
||||
//schedule
|
||||
schedule: {
|
||||
select: {
|
||||
friendlyId: true,
|
||||
generatorExpression: true,
|
||||
timezone: true,
|
||||
generatorDescription: true,
|
||||
},
|
||||
},
|
||||
//usage
|
||||
baseCostInCents: true,
|
||||
costInCents: true,
|
||||
usageDurationMs: true,
|
||||
//env
|
||||
runtimeEnvironment: {
|
||||
select: { id: true, slug: true, type: true },
|
||||
},
|
||||
payload: true,
|
||||
payloadType: true,
|
||||
maxAttempts: true,
|
||||
//finished attempt
|
||||
attempts: {
|
||||
select: {
|
||||
output: true,
|
||||
outputType: true,
|
||||
error: true,
|
||||
},
|
||||
where: {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
},
|
||||
project: {
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
lockedBy: {
|
||||
select: {
|
||||
filePath: true,
|
||||
exportName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
spanId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
return;
|
||||
}
|
||||
|
||||
const finishedAttempt = run.attempts.at(0);
|
||||
const output =
|
||||
finishedAttempt === undefined
|
||||
? undefined
|
||||
: finishedAttempt.outputType === "application/store"
|
||||
? `/resources/packets/${run.runtimeEnvironment.id}/${finishedAttempt.output}`
|
||||
: typeof finishedAttempt.output !== "undefined" && finishedAttempt.output !== null
|
||||
? await prettyPrintPacket(finishedAttempt.output, finishedAttempt.outputType ?? undefined)
|
||||
: undefined;
|
||||
|
||||
const payload =
|
||||
run.payloadType === "application/store"
|
||||
? `/resources/packets/${run.runtimeEnvironment.id}/${run.payload}`
|
||||
: typeof run.payload !== "undefined" && run.payload !== null
|
||||
? await prettyPrintPacket(run.payload, run.payloadType ?? undefined)
|
||||
: undefined;
|
||||
|
||||
const span = await eventRepository.getSpan(spanId, run.traceId);
|
||||
|
||||
const context = {
|
||||
task: {
|
||||
id: run.taskIdentifier,
|
||||
filePath: run.lockedBy?.filePath,
|
||||
exportName: run.lockedBy?.exportName,
|
||||
},
|
||||
run: {
|
||||
id: run.friendlyId,
|
||||
createdAt: run.createdAt,
|
||||
tags: run.tags.map((tag) => tag.name),
|
||||
isTest: run.isTest,
|
||||
idempotencyKey: run.idempotencyKey ?? undefined,
|
||||
startedAt: run.startedAt ?? run.createdAt,
|
||||
durationMs: run.usageDurationMs,
|
||||
costInCents: run.costInCents,
|
||||
baseCostInCents: run.baseCostInCents,
|
||||
maxAttempts: run.maxAttempts ?? undefined,
|
||||
version: run.lockedToVersion?.version,
|
||||
},
|
||||
queue: {
|
||||
name: run.queue,
|
||||
},
|
||||
environment: {
|
||||
id: run.runtimeEnvironment.id,
|
||||
slug: run.runtimeEnvironment.slug,
|
||||
type: run.runtimeEnvironment.type,
|
||||
},
|
||||
organization: {
|
||||
id: run.project.organization.id,
|
||||
slug: run.project.organization.slug,
|
||||
name: run.project.organization.title,
|
||||
},
|
||||
project: {
|
||||
id: run.project.id,
|
||||
ref: run.project.externalRef,
|
||||
slug: run.project.slug,
|
||||
name: run.project.name,
|
||||
},
|
||||
machine: run.machinePreset
|
||||
? machinePresetFromName(run.machinePreset as MachinePresetName)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
friendlyId: run.friendlyId,
|
||||
status: run.status,
|
||||
createdAt: run.createdAt,
|
||||
startedAt: run.startedAt,
|
||||
updatedAt: run.updatedAt,
|
||||
delayUntil: run.delayUntil,
|
||||
expiredAt: run.expiredAt,
|
||||
ttl: run.ttl,
|
||||
taskIdentifier: run.taskIdentifier,
|
||||
version: run.lockedToVersion?.version,
|
||||
sdkVersion: run.lockedToVersion?.sdkVersion,
|
||||
isTest: run.isTest,
|
||||
environmentId: run.runtimeEnvironment.id,
|
||||
schedule: run.schedule
|
||||
? {
|
||||
friendlyId: run.schedule.friendlyId,
|
||||
generatorExpression: run.schedule.generatorExpression,
|
||||
description: run.schedule.generatorDescription,
|
||||
timezone: run.schedule.timezone,
|
||||
}
|
||||
: undefined,
|
||||
queue: {
|
||||
name: run.queue,
|
||||
isCustomQueue: !run.queue.startsWith("task/"),
|
||||
concurrencyKey: run.concurrencyKey,
|
||||
},
|
||||
tags: run.tags.map((tag) => tag.name),
|
||||
baseCostInCents: run.baseCostInCents,
|
||||
costInCents: run.costInCents,
|
||||
totalCostInCents: run.costInCents + run.baseCostInCents,
|
||||
usageDurationMs: run.usageDurationMs,
|
||||
isFinished: FINISHED_STATUSES.includes(run.status),
|
||||
isRunning: RUNNING_STATUSES.includes(run.status),
|
||||
payload,
|
||||
payloadType: run.payloadType,
|
||||
output,
|
||||
outputType: finishedAttempt?.outputType ?? "application/json",
|
||||
links: span?.links,
|
||||
events: span?.events,
|
||||
context: JSON.stringify(context, null, 2),
|
||||
};
|
||||
}
|
||||
|
||||
async getSpan(runFriendlyId: string, spanId: string) {
|
||||
const run = await this._prisma.taskRun.findFirst({
|
||||
select: {
|
||||
traceId: true,
|
||||
@@ -49,31 +269,11 @@ export class SpanPresenter extends BasePresenter {
|
||||
return;
|
||||
}
|
||||
|
||||
const output =
|
||||
span.outputType === "application/store"
|
||||
? `/resources/packets/${span.environmentId}/${span.output}`
|
||||
: typeof span.output !== "undefined"
|
||||
? await prettyPrintPacket(span.output, span.outputType ?? undefined)
|
||||
: undefined;
|
||||
|
||||
const payload =
|
||||
span.payloadType === "application/store"
|
||||
? `/resources/packets/${span.environmentId}/${span.payload}`
|
||||
: typeof span.payload !== "undefined" && span.payload !== null
|
||||
? await prettyPrintPacket(span.payload, span.payloadType ?? undefined)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
event: {
|
||||
...span,
|
||||
events: span.events,
|
||||
output,
|
||||
outputType: span.outputType ?? "application/json",
|
||||
payload,
|
||||
payloadType: span.payloadType ?? "application/json",
|
||||
properties: span.properties ? JSON.stringify(span.properties, null, 2) : undefined,
|
||||
showActionBar: span.show?.actions === true,
|
||||
},
|
||||
...span,
|
||||
events: span.events,
|
||||
properties: span.properties ? JSON.stringify(span.properties, null, 2) : undefined,
|
||||
showActionBar: span.show?.actions === true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+13
-14
@@ -20,7 +20,7 @@ import { Header1, Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import {
|
||||
@@ -129,21 +129,20 @@ export default function Page() {
|
||||
<PageTitle title="Tasks" />
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property.Table>
|
||||
{tasks.map((task) => (
|
||||
<Property label={task.exportName} key={task.slug}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{task.environments
|
||||
.map((e) =>
|
||||
e.userName ? `${e.userName}/${e.id}` : `${e.type.slice(0, 3)}/${e.id}`
|
||||
)
|
||||
.join(", ")}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property.Item key={task.slug}>
|
||||
<Property.Label>{task.exportName}</Property.Label>
|
||||
<Property.Value>
|
||||
{task.environments
|
||||
.map((e) =>
|
||||
e.userName ? `${e.userName}/${e.id}` : `${e.type.slice(0, 3)}/${e.id}`
|
||||
)
|
||||
.join(", ")}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
))}
|
||||
</PropertyTable>
|
||||
</Property.Table>
|
||||
</AdminDebugTooltip>
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
|
||||
+7
-8
@@ -11,7 +11,7 @@ import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -61,15 +61,14 @@ export default function Page() {
|
||||
<PageTitle title="API keys" />
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property.Table>
|
||||
{environments.map((environment) => (
|
||||
<Property label={environment.slug} key={environment.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{environment.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property.Item key={environment.id}>
|
||||
<Property.Label>{environment.slug}</Property.Label>
|
||||
<Property.Value>{environment.id}</Property.Value>
|
||||
</Property.Item>
|
||||
))}
|
||||
</PropertyTable>
|
||||
</Property.Table>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<LinkButton
|
||||
|
||||
+94
-79
@@ -11,7 +11,7 @@ import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { DateTimeAccurate } from "~/components/primitives/DateTime";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -71,44 +71,39 @@ export default function Page() {
|
||||
<Header2 className={cn("whitespace-nowrap")}>Deploy: {deployment.shortCode}</Header2>
|
||||
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{deployment.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Project ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{deployment.projectId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{deployment.organizationId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>ID</Property.Label>
|
||||
<Property.Value>{deployment.id}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Project ID</Property.Label>
|
||||
<Property.Value>{deployment.projectId}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Org ID</Property.Label>
|
||||
<Property.Value>{deployment.organizationId}</Property.Value>
|
||||
</Property.Item>
|
||||
{deployment.imageReference && (
|
||||
<Property label="Image">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{deployment.imageReference}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property.Item>
|
||||
<Property.Label>Image</Property.Label>
|
||||
<Property.Value>{deployment.imageReference}</Property.Value>
|
||||
</Property.Item>
|
||||
)}
|
||||
{deployment.externalBuildData && (
|
||||
<Property label="Build Server">
|
||||
<div className="flex items-center gap-2">
|
||||
<Property.Item>
|
||||
<Property.Label>Build Server</Property.Label>
|
||||
<Property.Value>
|
||||
<Link
|
||||
to={`/resources/${deployment.projectId}/deployments/${deployment.id}/logs`}
|
||||
className="extra-small/bright/mono underline"
|
||||
>
|
||||
{deployment.externalBuildData.buildId}
|
||||
</Link>
|
||||
</div>
|
||||
</Property>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
)}
|
||||
</PropertyTable>
|
||||
</Property.Table>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<LinkButton
|
||||
@@ -120,35 +115,51 @@ export default function Page() {
|
||||
</div>
|
||||
<div className="overflow-y-auto px-3 pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="flex flex-col gap-4">
|
||||
<PropertyTable>
|
||||
<Property label="Deploy">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="small/bright">{deployment.shortCode}</Paragraph>
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>Deploy</Property.Label>
|
||||
<Property.Value className="flex items-center gap-2">
|
||||
<span>{deployment.shortCode}</span>
|
||||
{deployment.label && <Badge variant="outline-rounded">{deployment.label}</Badge>}
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Environment">
|
||||
<EnvironmentLabel environment={deployment.environment} userName={usernameForEnv} />
|
||||
</Property>
|
||||
<Property label="Version">{deployment.version}</Property>
|
||||
<Property label="Status">
|
||||
<DeploymentStatus
|
||||
status={deployment.status}
|
||||
isBuilt={deployment.isBuilt}
|
||||
className="text-sm"
|
||||
/>
|
||||
</Property>
|
||||
<Property label="Tasks">{deployment.tasks ? deployment.tasks.length : "–"}</Property>
|
||||
<Property label="SDK Version">
|
||||
{deployment.sdkVersion ? deployment.sdkVersion : "–"}
|
||||
</Property>
|
||||
<Property label="Started at">
|
||||
<Paragraph variant="small/bright">
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Environment</Property.Label>
|
||||
<Property.Value>
|
||||
<EnvironmentLabel environment={deployment.environment} userName={usernameForEnv} />
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Version</Property.Label>
|
||||
<Property.Value>{deployment.version}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Status</Property.Label>
|
||||
<Property.Value>
|
||||
<DeploymentStatus
|
||||
status={deployment.status}
|
||||
isBuilt={deployment.isBuilt}
|
||||
className="text-sm"
|
||||
/>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Tasks</Property.Label>
|
||||
<Property.Value>{deployment.tasks ? deployment.tasks.length : "–"}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>SDK Version</Property.Label>
|
||||
<Property.Value>{deployment.sdkVersion ? deployment.sdkVersion : "–"}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Started at</Property.Label>
|
||||
<Property.Value>
|
||||
<DateTimeAccurate date={deployment.createdAt} /> UTC
|
||||
</Paragraph>
|
||||
</Property>
|
||||
<Property label="Built at">
|
||||
<Paragraph variant="small/bright">
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Built at</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.builtAt ? (
|
||||
<>
|
||||
<DateTimeAccurate date={deployment.builtAt} /> UTC
|
||||
@@ -156,10 +167,11 @@ export default function Page() {
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
</Property>
|
||||
<Property label="Deployed at">
|
||||
<Paragraph variant="small/bright">
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Deployed at</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.deployedAt ? (
|
||||
<>
|
||||
<DateTimeAccurate date={deployment.deployedAt} /> UTC
|
||||
@@ -167,25 +179,28 @@ export default function Page() {
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
</Property>
|
||||
<Property label="Deployed by">
|
||||
{deployment.deployedBy ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<UserAvatar
|
||||
avatarUrl={deployment.deployedBy.avatarUrl}
|
||||
name={deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Paragraph variant="small">
|
||||
{deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Property>
|
||||
</PropertyTable>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Deployed by</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.deployedBy ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<UserAvatar
|
||||
avatarUrl={deployment.deployedBy.avatarUrl}
|
||||
name={deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Paragraph variant="small">
|
||||
{deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
</Property.Table>
|
||||
|
||||
{deployment.tasks ? (
|
||||
<div className="divide-y divide-charcoal-800 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
|
||||
+23
-4
@@ -248,6 +248,7 @@ export default function Page() {
|
||||
<EditEnvironmentVariablePanel
|
||||
environments={environments}
|
||||
variable={variable}
|
||||
revealAll={revealAll}
|
||||
/>
|
||||
<DeleteEnvironmentVariableButton variable={variable} />
|
||||
</TableCellMenu>
|
||||
@@ -293,15 +294,21 @@ export default function Page() {
|
||||
function EditEnvironmentVariablePanel({
|
||||
variable,
|
||||
environments,
|
||||
revealAll,
|
||||
}: {
|
||||
variable: EnvironmentVariableWithSetValues;
|
||||
environments: Pick<RuntimeEnvironment, "id" | "type">[];
|
||||
revealAll: boolean;
|
||||
}) {
|
||||
const [reveal, setReveal] = useState(revealAll);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const lastSubmission = useActionData();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const hiddenValues = Object.values(variable.values).filter((value) => !environments.map(e => e.id).includes(value.environment.id));
|
||||
const hiddenValues = Object.values(variable.values).filter(
|
||||
(value) => !environments.map((e) => e.id).includes(value.environment.id)
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
navigation.state !== "idle" &&
|
||||
@@ -340,7 +347,11 @@ function EditEnvironmentVariablePanel({
|
||||
<input type="hidden" name="key" value={variable.key} />
|
||||
{hiddenValues.map((value, index) => (
|
||||
<Fragment key={index}>
|
||||
<input type="hidden" name={`values[${index}].environmentId`} value={value.environment.id} />
|
||||
<input
|
||||
type="hidden"
|
||||
name={`values[${index}].environmentId`}
|
||||
value={value.environment.id}
|
||||
/>
|
||||
<input type="hidden" name={`values[${index}].value`} value={value.value} />
|
||||
</Fragment>
|
||||
))}
|
||||
@@ -355,7 +366,15 @@ function EditEnvironmentVariablePanel({
|
||||
</Fieldset>
|
||||
<Fieldset>
|
||||
<InputGroup fullWidth>
|
||||
<Label>Values</Label>
|
||||
<div className="flex justify-between gap-1">
|
||||
<Label>Values</Label>
|
||||
<Switch
|
||||
variant="small"
|
||||
label="Reveal"
|
||||
checked={reveal}
|
||||
onCheckedChange={(e) => setReveal(e.valueOf())}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-2">
|
||||
{environments.map((environment, index) => {
|
||||
const value = variable.values[environment.id]?.value;
|
||||
@@ -377,7 +396,7 @@ function EditEnvironmentVariablePanel({
|
||||
name={`values[${index}].value`}
|
||||
placeholder="Not set"
|
||||
defaultValue={value}
|
||||
type="password"
|
||||
type={reveal ? "text" : "password"}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
+106
-69
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
ArrowUturnLeftIcon,
|
||||
BoltSlashIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
MagnifyingGlassMinusIcon,
|
||||
MagnifyingGlassPlusIcon,
|
||||
StopCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import type { Location } from "@remix-run/react";
|
||||
import { useLoaderData, useParams, useRevalidator } from "@remix-run/react";
|
||||
@@ -26,14 +29,14 @@ import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody } from "~/components/layout/AppLayout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Popover, PopoverArrowTrigger, PopoverContent } from "~/components/primitives/Popover";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
@@ -71,6 +74,9 @@ import {
|
||||
import { SpanView } from "../resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { env } from "~/env.server";
|
||||
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog";
|
||||
import { ReplayRunDialog } from "~/components/runs/v3/ReplayRunDialog";
|
||||
|
||||
type TraceEvent = NonNullable<SerializeFrom<typeof loader>["trace"]>["events"][0];
|
||||
|
||||
@@ -122,41 +128,38 @@ export default function Page() {
|
||||
to: v3RunsPath(organization, project),
|
||||
text: "Runs",
|
||||
}}
|
||||
title={`Run #${run.number}`}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<span>Run #{run.number}</span>
|
||||
<EnvironmentLabel
|
||||
size="large"
|
||||
environment={run.environment}
|
||||
userName={usernameForEnv}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Trace ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.traceId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Env ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.environment.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{run.environment.organizationId}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
</PropertyTable>
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>ID</Property.Label>
|
||||
<Property.Value>{run.id}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Trace ID</Property.Label>
|
||||
<Property.Value>{run.traceId}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Env ID</Property.Label>
|
||||
<Property.Value>{run.environment.id}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Org ID</Property.Label>
|
||||
<Property.Value>{run.environment.organizationId}</Property.Value>
|
||||
</Property.Item>
|
||||
</Property.Table>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<EnvironmentLabel
|
||||
size="large"
|
||||
environment={run.environment}
|
||||
userName={usernameForEnv}
|
||||
/>
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
<PageBody>
|
||||
@@ -200,37 +203,76 @@ export default function Page() {
|
||||
to: v3RunsPath(organization, project),
|
||||
text: "Runs",
|
||||
}}
|
||||
title={`Run #${run.number}`}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<span>Run #{run.number}</span>
|
||||
<EnvironmentLabel
|
||||
size="large"
|
||||
environment={run.environment}
|
||||
userName={usernameForEnv}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Trace ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.traceId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Env ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.environment.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{run.environment.organizationId}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
</PropertyTable>
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>ID</Property.Label>
|
||||
<Property.Value>{run.id}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Trace ID</Property.Label>
|
||||
<Property.Value>{run.traceId}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Env ID</Property.Label>
|
||||
<Property.Value>{run.environment.id}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Org ID</Property.Label>
|
||||
<Property.Value>{run.environment.organizationId}</Property.Value>
|
||||
</Property.Item>
|
||||
</Property.Table>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<EnvironmentLabel size="large" environment={run.environment} userName={usernameForEnv} />
|
||||
<Dialog key={`replay-${run.friendlyId}`}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={ArrowUturnLeftIcon}
|
||||
shortcut={{ key: "R" }}
|
||||
>
|
||||
Replay run
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<ReplayRunDialog
|
||||
runFriendlyId={run.friendlyId}
|
||||
failedRedirect={v3RunSpanPath(
|
||||
organization,
|
||||
project,
|
||||
{ friendlyId: run.friendlyId },
|
||||
{ spanId: run.spanId }
|
||||
)}
|
||||
/>
|
||||
</Dialog>
|
||||
{run.isFinished ? null : (
|
||||
<Dialog key={`cancel-${run.friendlyId}`}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="danger/small" LeadingIcon={StopCircleIcon}>
|
||||
Cancel run
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<CancelRunDialog
|
||||
runFriendlyId={run.friendlyId}
|
||||
redirectPath={v3RunSpanPath(
|
||||
organization,
|
||||
project,
|
||||
{ friendlyId: run.friendlyId },
|
||||
{ spanId: run.spanId }
|
||||
)}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
<PageBody scrollable={false}>
|
||||
@@ -244,7 +286,7 @@ export default function Page() {
|
||||
setResizableRunSettings(document, layout);
|
||||
}}
|
||||
>
|
||||
<ResizablePanel order={1} minSize={30} defaultSize={resizeSettings.layout?.[0]}>
|
||||
<ResizablePanel order={1} minSize={30} defaultSize={resizeSettings.layout?.[0] ?? 70}>
|
||||
<TasksTreeView
|
||||
selectedId={selectedSpanId}
|
||||
key={events[0]?.id ?? "-"}
|
||||
@@ -269,7 +311,7 @@ export default function Page() {
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
{selectedSpanId && (
|
||||
<ResizablePanel order={2} minSize={30} defaultSize={resizeSettings.layout?.[1]}>
|
||||
<ResizablePanel order={2} minSize={25} defaultSize={resizeSettings.layout?.[1] ?? 30}>
|
||||
<SpanView
|
||||
runParam={run.friendlyId}
|
||||
spanId={selectedSpanId}
|
||||
@@ -311,7 +353,7 @@ function TasksTreeView({
|
||||
}: TasksTreeViewProps) {
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [errorsOnly, setErrorsOnly] = useState(false);
|
||||
const [showDurations, setShowDurations] = useState(false);
|
||||
const [showDurations, setShowDurations] = useState(true);
|
||||
const [scale, setScale] = useState(0);
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const treeScrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -1028,16 +1070,11 @@ function KeyboardShortcuts({
|
||||
title="Expand all"
|
||||
/>
|
||||
<ShortcutWithAction
|
||||
shortcut={{ key: "c" }}
|
||||
shortcut={{ key: "w" }}
|
||||
action={() => collapseAllBelowDepth(1)}
|
||||
title="Collapse all"
|
||||
/>
|
||||
<NumberShortcuts toggleLevel={(number) => toggleExpandLevel(number)} />
|
||||
<ShortcutWithAction
|
||||
shortcut={{ key: "d" }}
|
||||
action={() => setShowDurations((d) => !d)}
|
||||
title="Toggle durations"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+56
-30
@@ -15,7 +15,6 @@ import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout, variantClasses } from "~/components/primitives/Callout";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -27,7 +26,7 @@ import {
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
@@ -225,36 +224,63 @@ export default function Page() {
|
||||
<div className="overflow-y-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="p-3">
|
||||
<div className="space-y-3">
|
||||
<PropertyTable>
|
||||
<Property label="Schedule ID">{schedule.friendlyId}</Property>
|
||||
<Property label="Task ID">{schedule.taskIdentifier}</Property>
|
||||
<Property label="Type">
|
||||
<ScheduleTypeCombo type={schedule.type} className="text-sm" />
|
||||
</Property>
|
||||
<Property label="CRON (UTC)" labelClassName="self-start">
|
||||
<div className="space-y-2">
|
||||
<InlineCode variant="extra-small">{schedule.cron}</InlineCode>
|
||||
<Paragraph variant="small">{schedule.cronDescription}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Timezone">{schedule.timezone}</Property>
|
||||
<Property label="Environments">
|
||||
<EnvironmentLabels size="small" environments={schedule.environments} />
|
||||
</Property>
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>Schedule ID</Property.Label>
|
||||
<Property.Value>{schedule.friendlyId}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Task ID</Property.Label>
|
||||
<Property.Value>{schedule.taskIdentifier}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Type</Property.Label>
|
||||
<Property.Value>
|
||||
<ScheduleTypeCombo type={schedule.type} className="text-sm" />
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>CRON</Property.Label>
|
||||
<Property.Value>
|
||||
<div className="space-y-2">
|
||||
<InlineCode variant="extra-small">{schedule.cron}</InlineCode>
|
||||
<Paragraph variant="small">{schedule.cronDescription}</Paragraph>
|
||||
</div>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Timezone</Property.Label>
|
||||
<Property.Value>{schedule.timezone}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Environments</Property.Label>
|
||||
<Property.Value>
|
||||
<EnvironmentLabels size="small" environments={schedule.environments} />
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
{isImperative && (
|
||||
<>
|
||||
<Property label="External ID">
|
||||
{schedule.externalId ? schedule.externalId : "–"}
|
||||
</Property>
|
||||
<Property label="Deduplication key">
|
||||
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
||||
</Property>
|
||||
<Property label="Status">
|
||||
<EnabledStatus enabled={schedule.active} />
|
||||
</Property>
|
||||
<Property.Item>
|
||||
<Property.Label>External ID</Property.Label>
|
||||
<Property.Value>
|
||||
{schedule.externalId ? schedule.externalId : "–"}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Deduplication key</Property.Label>
|
||||
<Property.Value>
|
||||
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Status</Property.Label>
|
||||
<Property.Value>
|
||||
<EnabledStatus enabled={schedule.active} />
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
</>
|
||||
)}
|
||||
</PropertyTable>
|
||||
</Property.Table>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Header3>Last 5 runs</Header3>
|
||||
<TaskRunsTable
|
||||
@@ -297,12 +323,12 @@ export default function Page() {
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableBlankRow colSpan={1}>
|
||||
<TableBlankRow colSpan={isUtc ? 1 : 2}>
|
||||
<PlaceholderText title="You found a bug" />
|
||||
</TableBlankRow>
|
||||
)
|
||||
) : (
|
||||
<TableBlankRow colSpan={1}>
|
||||
<TableBlankRow colSpan={isUtc ? 1 : 2}>
|
||||
<PlaceholderText title="Schedule disabled" />
|
||||
</TableBlankRow>
|
||||
)}
|
||||
|
||||
+7
-8
@@ -28,7 +28,7 @@ import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { PaginationControls } from "~/components/primitives/Pagination";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
@@ -122,15 +122,14 @@ export default function Page() {
|
||||
<PageTitle title="Schedules" />
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property.Table>
|
||||
{schedules.map((schedule) => (
|
||||
<Property label={schedule.friendlyId} key={schedule.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{schedule.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property.Item key={schedule.id}>
|
||||
<Property.Label>{schedule.friendlyId}</Property.Label>
|
||||
<Property.Value>{schedule.id}</Property.Value>
|
||||
</Property.Item>
|
||||
))}
|
||||
</PropertyTable>
|
||||
</Property.Table>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
{limits.used >= limits.limit ? (
|
||||
|
||||
+11
-10
@@ -17,7 +17,7 @@ import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
@@ -116,18 +116,19 @@ export default function Page() {
|
||||
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="ID">
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>ID</Property.Label>
|
||||
<Property.Value>{project.id}</Property.Value>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{project.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{project.organizationId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
</PropertyTable>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Org ID</Property.Label>
|
||||
<Property.Value>{project.organizationId}</Property.Value>
|
||||
</Property.Item>
|
||||
</Property.Table>
|
||||
</AdminDebugTooltip>
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
|
||||
@@ -27,7 +27,7 @@ import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { $replica } from "~/db.server";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
@@ -128,23 +128,25 @@ export default function Page() {
|
||||
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{organization.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>Org ID</Property.Label>
|
||||
<Property.Value>{organization.id}</Property.Value>
|
||||
</Property.Item>
|
||||
|
||||
{members.map((member) => (
|
||||
<Property label={member.user.name} key={member.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{member.user.email} - {member.user.id}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property.Item key={member.id}>
|
||||
<Property.Label>{member.user.name}</Property.Label>
|
||||
<Property.Value>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{member.user.email} - {member.user.id}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
))}
|
||||
</PropertyTable>
|
||||
</Property.Table>
|
||||
</AdminDebugTooltip>
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { AddTagsRequestBody } from "@trigger.dev/core/v3";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { createTag, getTagsForRunId } from "~/models/taskRunTag.server";
|
||||
import { createTag, getTagsForRunId, MAX_TAGS_PER_RUN } from "~/models/taskRunTag.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
|
||||
|
||||
@@ -51,12 +51,12 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
return !existingTags.map((t) => t.name).includes(tag);
|
||||
});
|
||||
|
||||
if (existingTags.length + newTags.length > 3) {
|
||||
if (existingTags.length + newTags.length > MAX_TAGS_PER_RUN) {
|
||||
return json(
|
||||
{
|
||||
error: `Runs can only have 3 tags, you're trying to set ${
|
||||
error: `Runs can only have ${MAX_TAGS_PER_RUN} tags, you're trying to set ${
|
||||
existingTags.length + newTags.length
|
||||
}.`,
|
||||
}. These tags have not been set: ${newTags.map((t) => `'${t}'`).join(", ")}.`,
|
||||
},
|
||||
{ status: 422 }
|
||||
);
|
||||
|
||||
+730
-267
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,93 @@
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { ActionFunction, json } from "@remix-run/node";
|
||||
import { ActionFunction, json, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { prettyPrintPacket } from "@trigger.dev/core/v3";
|
||||
import { typedjson } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { $replica, prisma } from "~/db.server";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { sortEnvironments } from "~/utils/environmentSort";
|
||||
import { v3RunSpanPath } from "~/utils/pathBuilder";
|
||||
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
|
||||
|
||||
const FormSchema = z.object({
|
||||
failedRedirect: z.string(),
|
||||
});
|
||||
|
||||
const ParamSchema = z.object({
|
||||
runParam: z.string(),
|
||||
});
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { runParam } = ParamSchema.parse(params);
|
||||
|
||||
const run = await $replica.taskRun.findFirst({
|
||||
select: {
|
||||
payload: true,
|
||||
payloadType: true,
|
||||
runtimeEnvironmentId: true,
|
||||
project: {
|
||||
select: {
|
||||
environments: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
slug: true,
|
||||
orgMember: {
|
||||
select: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
type: {
|
||||
in: ["PREVIEW", "STAGING", "PRODUCTION"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "DEVELOPMENT",
|
||||
orgMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: { friendlyId: runParam, project: { organization: { members: { some: { userId } } } } },
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
throw new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
const environment = run.project.environments.find((env) => env.id === run.runtimeEnvironmentId);
|
||||
if (!environment) {
|
||||
throw new Response("Environment not found", { status: 404 });
|
||||
}
|
||||
|
||||
return typedjson({
|
||||
payload: await prettyPrintPacket(run.payload, run.payloadType),
|
||||
payloadType: run.payloadType,
|
||||
environment: displayableEnvironment(environment, userId),
|
||||
environments: sortEnvironments(
|
||||
run.project.environments.map((environment) => displayableEnvironment(environment, userId))
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const FormSchema = z.object({
|
||||
environment: z.string().optional(),
|
||||
payload: z.string().optional(),
|
||||
failedRedirect: z.string(),
|
||||
});
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const { runParam } = ParamSchema.parse(params);
|
||||
|
||||
const formData = await request.formData();
|
||||
@@ -44,7 +116,10 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
}
|
||||
|
||||
const replayRunService = new ReplayTaskRunService();
|
||||
const newRun = await replayRunService.call(taskRun);
|
||||
const newRun = await replayRunService.call(taskRun, {
|
||||
environmentId: submission.value.environment,
|
||||
payload: submission.value.payload,
|
||||
});
|
||||
|
||||
if (!newRun) {
|
||||
return redirectWithErrorMessage(
|
||||
|
||||
@@ -24,7 +24,7 @@ export function machinePresetFromConfig(config: unknown): MachinePreset {
|
||||
return machinePresetFromName("small-1x");
|
||||
}
|
||||
|
||||
function machinePresetFromName(name: MachinePresetName): MachinePreset {
|
||||
export function machinePresetFromName(name: MachinePresetName): MachinePreset {
|
||||
return {
|
||||
name,
|
||||
...machines[name],
|
||||
|
||||
@@ -190,6 +190,7 @@ export class CreateTaskRunAttemptService extends BaseService {
|
||||
costInCents: taskRun.costInCents,
|
||||
baseCostInCents: taskRun.baseCostInCents,
|
||||
maxAttempts: taskRun.maxAttempts ?? undefined,
|
||||
version: taskRun.lockedBy.worker.version,
|
||||
},
|
||||
queue: {
|
||||
id: queue.friendlyId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { marqs } from "~/v3/marqs/index.server";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { eventRepository } from "../eventRepository.server";
|
||||
|
||||
export class ExpireEnqueuedRunService extends BaseService {
|
||||
public async call(runId: string) {
|
||||
@@ -48,6 +49,24 @@ export class ExpireEnqueuedRunService extends BaseService {
|
||||
},
|
||||
});
|
||||
|
||||
await eventRepository.completeEvent(run.spanId, {
|
||||
endTime: new Date(),
|
||||
attributes: {
|
||||
isError: true,
|
||||
},
|
||||
events: [
|
||||
{
|
||||
name: "exception",
|
||||
time: new Date(),
|
||||
properties: {
|
||||
exception: {
|
||||
message: `Run expired because the TTL (${run.ttl}) was reached`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await marqs?.acknowledgeMessage(run.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { conditionallyImportPacket, parsePacket, RunTags } from "@trigger.dev/core/v3";
|
||||
import {
|
||||
conditionallyImportPacket,
|
||||
IOPacket,
|
||||
parsePacket,
|
||||
RunTags,
|
||||
stringifyIO,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { replaceSuperJsonPayload } from "@trigger.dev/core/v3/utils/ioSerialization";
|
||||
import { TaskRun } from "@trigger.dev/database";
|
||||
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
|
||||
import { getTagsForRunId } from "~/models/taskRunTag.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { OutOfEntitlementError, TriggerTaskService } from "./triggerTask.server";
|
||||
import { getTagsForRunId } from "~/models/taskRunTag.server";
|
||||
|
||||
type OverrideOptions = {
|
||||
environmentId?: string;
|
||||
payload?: string;
|
||||
};
|
||||
|
||||
export class ReplayTaskRunService extends BaseService {
|
||||
public async call(existingTaskRun: TaskRun) {
|
||||
public async call(existingTaskRun: TaskRun, overrideOptions?: OverrideOptions) {
|
||||
const authenticatedEnvironment = await findEnvironmentById(
|
||||
existingTaskRun.runtimeEnvironmentId
|
||||
overrideOptions?.environmentId ?? existingTaskRun.runtimeEnvironmentId
|
||||
);
|
||||
if (!authenticatedEnvironment) {
|
||||
return;
|
||||
@@ -20,10 +32,27 @@ export class ReplayTaskRunService extends BaseService {
|
||||
taskRunFriendlyId: existingTaskRun.friendlyId,
|
||||
});
|
||||
|
||||
const payloadPacket = await conditionallyImportPacket({
|
||||
data: existingTaskRun.payload,
|
||||
dataType: existingTaskRun.payloadType,
|
||||
});
|
||||
let payloadPacket: IOPacket;
|
||||
|
||||
if (overrideOptions?.payload) {
|
||||
if (existingTaskRun.payloadType === "application/super+json") {
|
||||
const newPayload = await replaceSuperJsonPayload(
|
||||
existingTaskRun.payload,
|
||||
overrideOptions.payload
|
||||
);
|
||||
payloadPacket = await stringifyIO(newPayload);
|
||||
} else {
|
||||
payloadPacket = await conditionallyImportPacket({
|
||||
data: overrideOptions.payload,
|
||||
dataType: existingTaskRun.payloadType,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
payloadPacket = await conditionallyImportPacket({
|
||||
data: existingTaskRun.payload,
|
||||
dataType: existingTaskRun.payloadType,
|
||||
});
|
||||
}
|
||||
|
||||
const parsedPayload =
|
||||
payloadPacket.dataType === "application/json"
|
||||
@@ -38,7 +67,7 @@ export class ReplayTaskRunService extends BaseService {
|
||||
|
||||
try {
|
||||
const tags = await getTagsForRunId({
|
||||
friendlyId: existingTaskRun.id,
|
||||
friendlyId: existingTaskRun.friendlyId,
|
||||
environmentId: authenticatedEnvironment.id,
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { getEntitlement } from "~/services/platform.v3.server";
|
||||
import { BaseService, ServiceValidationError } from "./baseService.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { isFinalAttemptStatus, isFinalRunStatus } from "../taskStatus";
|
||||
import { createTag } from "~/models/taskRunTag.server";
|
||||
import { createTag, MAX_TAGS_PER_RUN } from "~/models/taskRunTag.server";
|
||||
|
||||
export type TriggerTaskServiceOptions = {
|
||||
idempotencyKey?: string;
|
||||
@@ -78,6 +78,16 @@ export class TriggerTaskService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
body.options?.tags &&
|
||||
typeof body.options.tags !== "string" &&
|
||||
body.options.tags.length > MAX_TAGS_PER_RUN
|
||||
) {
|
||||
throw new ServiceValidationError(
|
||||
`Runs can only have ${MAX_TAGS_PER_RUN} tags, you're trying to set ${body.options.tags.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
const runFriendlyId = generateFriendlyId("run");
|
||||
|
||||
const payloadPacket = await this.#handlePayloadPacket(
|
||||
|
||||
@@ -57,10 +57,7 @@ export type CreateBackgroundWorkerResponse = z.infer<typeof CreateBackgroundWork
|
||||
|
||||
//an array of 1, 2, or 3 strings
|
||||
const RunTag = z.string().max(64, "Tags must be less than 64 characters");
|
||||
export const RunTags = z.union([
|
||||
RunTag,
|
||||
RunTag.array().max(3, "You can only set a maximum of 3 tags on a run."),
|
||||
]);
|
||||
export const RunTags = z.union([RunTag, RunTag.array()]);
|
||||
|
||||
export type RunTags = z.infer<typeof RunTags>;
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ export const TaskRun = z.object({
|
||||
durationMs: z.number().default(0),
|
||||
costInCents: z.number().default(0),
|
||||
baseCostInCents: z.number().default(0),
|
||||
version: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TaskRun = z.infer<typeof TaskRun>;
|
||||
|
||||
@@ -351,3 +351,16 @@ function safeJsonParse(value: string): any {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function replaceSuperJsonPayload(original: string, newPayload: string) {
|
||||
const superjson = await loadSuperJSON();
|
||||
const originalObject = superjson.parse(original);
|
||||
const { meta } = superjson.serialize(originalObject);
|
||||
|
||||
const newSuperJson = {
|
||||
json: JSON.parse(newPayload) as any,
|
||||
meta,
|
||||
};
|
||||
|
||||
return superjson.deserialize(newSuperJson);
|
||||
}
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TaskRun_spanId_idx" ON "TaskRun"("spanId");
|
||||
@@ -1691,6 +1691,8 @@ model TaskRun {
|
||||
@@index([projectId, taskIdentifier, status])
|
||||
//Schedules
|
||||
@@index([scheduleId])
|
||||
// Run page inspector
|
||||
@@index([spanId])
|
||||
}
|
||||
|
||||
enum TaskRunStatus {
|
||||
|
||||
@@ -31,6 +31,10 @@ export const simpleChildTask = task({
|
||||
await tags.add("product:1");
|
||||
|
||||
await wait.for({ seconds: 10 });
|
||||
|
||||
return {
|
||||
foo: "bar",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ type Payload = {
|
||||
export const triggerRunsWithTags = task({
|
||||
id: "trigger-runs-with-tags",
|
||||
run: async (payload: Payload, { ctx }) => {
|
||||
logger.info(`${ctx.run.version}`);
|
||||
|
||||
const { id } = await simpleChildTask.trigger(
|
||||
{ message: "trigger from triggerRunsWithTags" },
|
||||
{ tags: payload.tags }
|
||||
|
||||
Reference in New Issue
Block a user