Run tags (#1232)
* TaskRunTag migration. Removed unused TaskTag
* Show tags for a run in the run list
* API endpoint for searching tags
* Let’s not expose an API endpoint for tags at the moment
* Filter by tags working from the URL
* Tag async filter working
* We don’t need a “None” option because it’s multi-select
* When triggering a run you can add tags
* SDK runs.retrieve and runs.list with tag support
* Tidied imports
* Changed the run list query so we show all the tags even if a run only matches one of them
* Fix for dealing with weird characters in tags
* Run tags changeset
* Creating a project has a proper loading state (and blocks multiple)
* Improved the error message for tag length
* Added a tags icon for display on the run screen
* Convenient functions for creating and getting run tags
* tags.set() from inside the run function
* Replay a run passes tags through
* Less ridiculous tags for the catalog example
* Allow passing just a string for the tags
* Order by id because there’s an index on the primary key already
* Trim the tags earlier so we don’t accidentally error if a blank string is passed
* Renamed some thing from setTags to addTags
* Use findFirst for the tags project lookup
* Added an index for "TaskRunTag"("name", "id")
It massively improves the performance of run filtering based on tags
* Use `array_agg` for the run list tags so pagination works and we get a single result for each run
* Tidied imports
* More comprehensive test of tags with all triggering functions
* Support tags with tasks.trigger, tasks.batchTrigger and tasks poll variants
* Added tasks.batchTrigger tags
* Sort the tags in the UI so they’re always in the same order
* Added tooltips in the run table for delay, ttl and tags
* Added costInCents, baseCostInCents and durationMs to runs.retrieve and runs.list
* Added support for displaying a split tag if you use key_value or key:value format
* Fix code comment
* Tweaked the JSDoc to make the prefixing clearer
* Added tasks.triggerAndWait to the tags test task
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@trigger.dev/sdk": patch
|
||||||
|
"@trigger.dev/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
You can now add tags to runs and list runs using them
|
||||||
@@ -1,32 +1,37 @@
|
|||||||
import { cn } from "~/utils/cn";
|
import { cn } from "~/utils/cn";
|
||||||
|
|
||||||
|
type CustomColor = {
|
||||||
|
background: string;
|
||||||
|
foreground: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function Spinner({
|
export function Spinner({
|
||||||
className,
|
className,
|
||||||
color = "blue",
|
color = "blue",
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
color?: "blue" | "white" | "muted" | "dark";
|
color?: "blue" | "white" | "muted" | "dark" | CustomColor;
|
||||||
}) {
|
}) {
|
||||||
const colors = {
|
const colors = {
|
||||||
blue: {
|
blue: {
|
||||||
light: "rgba(59, 130, 246, 0.4)",
|
background: "rgba(59, 130, 246, 0.4)",
|
||||||
dark: "rgba(59, 130, 246)",
|
foreground: "rgba(59, 130, 246)",
|
||||||
},
|
},
|
||||||
white: {
|
white: {
|
||||||
light: "rgba(255, 255, 255, 0.4)",
|
background: "rgba(255, 255, 255, 0.4)",
|
||||||
dark: "rgba(255, 255, 255)",
|
foreground: "rgba(255, 255, 255)",
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
light: "#1C2433",
|
background: "#1C2433",
|
||||||
dark: "#3C4B62",
|
foreground: "#3C4B62",
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
light: "#15171A",
|
background: "#15171A",
|
||||||
dark: "#272A2E",
|
foreground: "#272A2E",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentColor = colors[color];
|
const currentColor = typeof color === "string" ? colors[color] : color;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -37,13 +42,33 @@ export function Spinner({
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className={cn("animate-spin motion-reduce:hidden", className)}
|
className={cn("animate-spin motion-reduce:hidden", className)}
|
||||||
>
|
>
|
||||||
<rect x="2" y="2" width="16" height="16" rx="8" stroke={currentColor.light} strokeWidth="3" />
|
<rect
|
||||||
|
x="2"
|
||||||
|
y="2"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
rx="8"
|
||||||
|
stroke={currentColor.background}
|
||||||
|
strokeWidth="3"
|
||||||
|
/>
|
||||||
<path
|
<path
|
||||||
d="M10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2"
|
d="M10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2"
|
||||||
stroke={currentColor.dark}
|
stroke={currentColor.foreground}
|
||||||
strokeWidth="3"
|
strokeWidth="3"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ButtonSpinner() {
|
||||||
|
return (
|
||||||
|
<Spinner
|
||||||
|
className="size-3"
|
||||||
|
color={{
|
||||||
|
foreground: "rgba(0, 0, 0, 1)",
|
||||||
|
background: "rgba(0, 0, 0, 0.25)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import {
|
|||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
CpuChipIcon,
|
CpuChipIcon,
|
||||||
InboxStackIcon,
|
InboxStackIcon,
|
||||||
|
TagIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import { Form } from "@remix-run/react";
|
import { Form, useFetcher } from "@remix-run/react";
|
||||||
import type {
|
import type {
|
||||||
RuntimeEnvironment,
|
RuntimeEnvironment,
|
||||||
TaskTriggerSource,
|
TaskTriggerSource,
|
||||||
@@ -15,7 +16,7 @@ import type {
|
|||||||
} from "@trigger.dev/database";
|
} from "@trigger.dev/database";
|
||||||
import { ListFilterIcon } from "lucide-react";
|
import { ListFilterIcon } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { startTransition, useCallback, useMemo, useState } from "react";
|
import { startTransition, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||||
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
|
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||||
@@ -51,6 +52,10 @@ import {
|
|||||||
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
|
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
|
||||||
import { DateTime } from "~/components/primitives/DateTime";
|
import { DateTime } from "~/components/primitives/DateTime";
|
||||||
import { BulkActionStatusCombo } from "./BulkAction";
|
import { BulkActionStatusCombo } from "./BulkAction";
|
||||||
|
import { type loader } from "~/routes/resources.projects.$projectParam.runs.tags";
|
||||||
|
import { useProject } from "~/hooks/useProject";
|
||||||
|
import { Spinner } from "~/components/primitives/Spinner";
|
||||||
|
import { matchSorter } from "match-sorter";
|
||||||
|
|
||||||
export const TaskAttemptStatus = z.enum(allTaskRunStatuses);
|
export const TaskAttemptStatus = z.enum(allTaskRunStatuses);
|
||||||
|
|
||||||
@@ -73,6 +78,10 @@ export const TaskRunListSearchFilters = z.object({
|
|||||||
(value) => (typeof value === "string" ? [value] : value),
|
(value) => (typeof value === "string" ? [value] : value),
|
||||||
TaskAttemptStatus.array().optional()
|
TaskAttemptStatus.array().optional()
|
||||||
),
|
),
|
||||||
|
tags: z.preprocess(
|
||||||
|
(value) => (typeof value === "string" ? [value] : value),
|
||||||
|
z.string().array().optional()
|
||||||
|
),
|
||||||
period: z.preprocess((value) => (value === "all" ? undefined : value), z.string().optional()),
|
period: z.preprocess((value) => (value === "all" ? undefined : value), z.string().optional()),
|
||||||
bulkId: z.string().optional(),
|
bulkId: z.string().optional(),
|
||||||
from: z.coerce.number().optional(),
|
from: z.coerce.number().optional(),
|
||||||
@@ -104,7 +113,8 @@ export function RunsFilters(props: RunFiltersProps) {
|
|||||||
searchParams.has("environments") ||
|
searchParams.has("environments") ||
|
||||||
searchParams.has("tasks") ||
|
searchParams.has("tasks") ||
|
||||||
searchParams.has("period") ||
|
searchParams.has("period") ||
|
||||||
searchParams.has("bulkId");
|
searchParams.has("bulkId") ||
|
||||||
|
searchParams.has("tags");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||||
@@ -133,6 +143,7 @@ const filterTypes = [
|
|||||||
},
|
},
|
||||||
{ name: "environments", title: "Environment", icon: <CpuChipIcon className="size-4" /> },
|
{ name: "environments", title: "Environment", icon: <CpuChipIcon className="size-4" /> },
|
||||||
{ name: "tasks", title: "Tasks", icon: <TaskIcon className="size-4" /> },
|
{ name: "tasks", title: "Tasks", icon: <TaskIcon className="size-4" /> },
|
||||||
|
{ name: "tags", title: "Tags", icon: <TagIcon className="size-4" /> },
|
||||||
{ name: "created", title: "Created", icon: <CalendarIcon className="size-4" /> },
|
{ name: "created", title: "Created", icon: <CalendarIcon className="size-4" /> },
|
||||||
{ name: "bulk", title: "Bulk action", icon: <InboxStackIcon className="size-4" /> },
|
{ name: "bulk", title: "Bulk action", icon: <InboxStackIcon className="size-4" /> },
|
||||||
] as const;
|
] as const;
|
||||||
@@ -209,6 +220,7 @@ function AppliedFilters({ possibleEnvironments, possibleTasks, bulkActions }: Ru
|
|||||||
<AppliedStatusFilter />
|
<AppliedStatusFilter />
|
||||||
<AppliedEnvironmentFilter possibleEnvironments={possibleEnvironments} />
|
<AppliedEnvironmentFilter possibleEnvironments={possibleEnvironments} />
|
||||||
<AppliedTaskFilter possibleTasks={possibleTasks} />
|
<AppliedTaskFilter possibleTasks={possibleTasks} />
|
||||||
|
<AppliedTagsFilter />
|
||||||
<AppliedPeriodFilter />
|
<AppliedPeriodFilter />
|
||||||
<AppliedBulkActionsFilter bulkActions={bulkActions} />
|
<AppliedBulkActionsFilter bulkActions={bulkActions} />
|
||||||
</>
|
</>
|
||||||
@@ -237,6 +249,8 @@ function Menu(props: MenuProps) {
|
|||||||
return <CreatedDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
return <CreatedDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||||
case "bulk":
|
case "bulk":
|
||||||
return <BulkActionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
return <BulkActionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||||
|
case "tags":
|
||||||
|
return <TagsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,6 +662,124 @@ function AppliedBulkActionsFilter({ bulkActions }: Pick<RunFiltersProps, "bulkAc
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TagsDropdown({
|
||||||
|
trigger,
|
||||||
|
clearSearchValue,
|
||||||
|
searchValue,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
trigger: ReactNode;
|
||||||
|
clearSearchValue: () => void;
|
||||||
|
searchValue: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
}) {
|
||||||
|
const project = useProject();
|
||||||
|
const { values, replace } = useSearchParams();
|
||||||
|
|
||||||
|
const handleChange = (values: string[]) => {
|
||||||
|
clearSearchValue();
|
||||||
|
replace({
|
||||||
|
tags: values,
|
||||||
|
cursor: undefined,
|
||||||
|
direction: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetcher = useFetcher<typeof loader>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (searchValue) {
|
||||||
|
searchParams.set("name", encodeURIComponent(searchValue));
|
||||||
|
}
|
||||||
|
fetcher.load(`/resources/projects/${project.slug}/runs/tags?${searchParams}`);
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
let items: string[] = [];
|
||||||
|
if (searchValue === "") {
|
||||||
|
items = values("tags");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetcher.data === undefined) {
|
||||||
|
return matchSorter(items, searchValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(...fetcher.data.tags.map((t) => t.name));
|
||||||
|
|
||||||
|
return matchSorter(Array.from(new Set(items)), searchValue);
|
||||||
|
}, [searchValue, fetcher.data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectProvider value={values("tags")} setValue={handleChange} virtualFocus={true}>
|
||||||
|
{trigger}
|
||||||
|
<SelectPopover
|
||||||
|
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
|
||||||
|
hideOnEscape={() => {
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ComboBox
|
||||||
|
value={searchValue}
|
||||||
|
render={(props) => (
|
||||||
|
<div className="flex items-center justify-stretch">
|
||||||
|
<input {...props} placeholder={"Filter by tags..."} />
|
||||||
|
{fetcher.state === "loading" && <Spinner color="muted" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<SelectList>
|
||||||
|
{filtered.length > 0
|
||||||
|
? filtered.map((tag, index) => (
|
||||||
|
<SelectItem key={tag} value={tag}>
|
||||||
|
{tag}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
{filtered.length === 0 && fetcher.state !== "loading" && (
|
||||||
|
<SelectItem disabled>No tags found</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectList>
|
||||||
|
</SelectPopover>
|
||||||
|
</SelectProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppliedTagsFilter() {
|
||||||
|
const { values, del } = useSearchParams();
|
||||||
|
|
||||||
|
const tags = values("tags");
|
||||||
|
|
||||||
|
if (tags.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterMenuProvider>
|
||||||
|
{(search, setSearch) => (
|
||||||
|
<TagsDropdown
|
||||||
|
trigger={
|
||||||
|
<Ariakit.Select render={<div className="group cursor-pointer" />}>
|
||||||
|
<AppliedFilter
|
||||||
|
label="Tags"
|
||||||
|
value={appliedSummary(values("tags"))}
|
||||||
|
onRemove={() => del(["tags", "cursor", "direction"])}
|
||||||
|
/>
|
||||||
|
</Ariakit.Select>
|
||||||
|
}
|
||||||
|
searchValue={search}
|
||||||
|
clearSearchValue={() => setSearch("")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FilterMenuProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const timePeriods = [
|
const timePeriods = [
|
||||||
{
|
{
|
||||||
label: "All periods",
|
label: "All periods",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
HandRaisedIcon,
|
HandRaisedIcon,
|
||||||
InformationCircleIcon,
|
InformationCircleIcon,
|
||||||
Squares2X2Icon,
|
Squares2X2Icon,
|
||||||
|
TagIcon,
|
||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import { AttemptIcon } from "~/assets/icons/AttemptIcon";
|
import { AttemptIcon } from "~/assets/icons/AttemptIcon";
|
||||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||||
@@ -48,6 +49,8 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
|
|||||||
return <ClockIcon className={cn(className, "text-teal-500")} />;
|
return <ClockIcon className={cn(className, "text-teal-500")} />;
|
||||||
case "trace":
|
case "trace":
|
||||||
return <Squares2X2Icon className={cn(className, "text-text-dimmed")} />;
|
return <Squares2X2Icon className={cn(className, "text-text-dimmed")} />;
|
||||||
|
case "tag":
|
||||||
|
return <TagIcon className={cn(className, "text-text-dimmed")} />;
|
||||||
//log levels
|
//log levels
|
||||||
case "debug":
|
case "debug":
|
||||||
case "log":
|
case "log":
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import tagLeftPath from "./tag-left.svg";
|
||||||
|
|
||||||
|
type Tag = string | { key: string; value: string };
|
||||||
|
|
||||||
|
export function RunTag({ tag }: { tag: string }) {
|
||||||
|
const tagResult = useMemo(() => splitTag(tag), [tag]);
|
||||||
|
|
||||||
|
if (typeof tagResult === "string") {
|
||||||
|
return (
|
||||||
|
<span className="flex h-6 items-stretch">
|
||||||
|
<img src={tagLeftPath} alt="" className="block h-full w-[0.5625rem]" />
|
||||||
|
<span className="flex items-center rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-800 pr-1.5 text-text-dimmed">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span className="flex h-6 items-stretch">
|
||||||
|
<img src={tagLeftPath} alt="" className="block h-full w-[0.5625rem]" />
|
||||||
|
<span className="flex items-center border-y border-r border-charcoal-700 bg-charcoal-800 pr-1.5 text-text-dimmed">
|
||||||
|
{tagResult.key}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-750 px-1.5 text-text-dimmed">
|
||||||
|
{tagResult.value}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Takes a string and turns it into a tag
|
||||||
|
*
|
||||||
|
* If the string has 12 or fewer alpha characters followed by an underscore or colon then we return an object with a key and value
|
||||||
|
* Otherwise we return the original string
|
||||||
|
*/
|
||||||
|
function splitTag(tag: string): Tag {
|
||||||
|
if (tag.match(/^[a-zA-Z]{1,12}[_:]/)) {
|
||||||
|
const components = tag.split(/[_:]/);
|
||||||
|
if (components.length !== 2) {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
return { key: components[0], value: components[1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
@@ -5,16 +5,22 @@ import {
|
|||||||
RectangleStackIcon,
|
RectangleStackIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import { StopIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
|
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||||
import { useLocation } from "@remix-run/react";
|
import { useLocation } from "@remix-run/react";
|
||||||
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
||||||
|
import { useCallback, useRef } from "react";
|
||||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||||
|
import { Checkbox } from "~/components/primitives/Checkbox";
|
||||||
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||||
|
import { Header3 } from "~/components/primitives/Headers";
|
||||||
|
import { useSelectedItems } from "~/components/primitives/SelectedItemsProvider";
|
||||||
import { useEnvironments } from "~/hooks/useEnvironments";
|
import { useEnvironments } from "~/hooks/useEnvironments";
|
||||||
|
import { useFeatures } from "~/hooks/useFeatures";
|
||||||
import { useOrganization } from "~/hooks/useOrganizations";
|
import { useOrganization } from "~/hooks/useOrganizations";
|
||||||
import { useProject } from "~/hooks/useProject";
|
import { useProject } from "~/hooks/useProject";
|
||||||
|
import { useUser } from "~/hooks/useUser";
|
||||||
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
|
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
|
||||||
|
import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter";
|
||||||
import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder";
|
import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder";
|
||||||
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
|
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
|
||||||
import { DateTime } from "../../primitives/DateTime";
|
import { DateTime } from "../../primitives/DateTime";
|
||||||
@@ -31,18 +37,10 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "../../primitives/Table";
|
} from "../../primitives/Table";
|
||||||
import { CancelRunDialog } from "./CancelRunDialog";
|
import { CancelRunDialog } from "./CancelRunDialog";
|
||||||
|
import { LiveTimer } from "./LiveTimer";
|
||||||
import { ReplayRunDialog } from "./ReplayRunDialog";
|
import { ReplayRunDialog } from "./ReplayRunDialog";
|
||||||
import { TaskRunStatusCombo } from "./TaskRunStatus";
|
import { TaskRunStatusCombo } from "./TaskRunStatus";
|
||||||
import { LiveTimer } from "./LiveTimer";
|
import { RunTag } from "./RunTag";
|
||||||
import { useSelectedItems } from "~/components/primitives/SelectedItemsProvider";
|
|
||||||
import { Checkbox } from "~/components/primitives/Checkbox";
|
|
||||||
import { useCallback, useRef } from "react";
|
|
||||||
import { run } from "@remix-run/dev/dist/cli/run";
|
|
||||||
import { formatCurrency, formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter";
|
|
||||||
import { useFeatures } from "~/hooks/useFeatures";
|
|
||||||
import { useUser } from "~/hooks/useUser";
|
|
||||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
|
||||||
import { Header3 } from "~/components/primitives/Headers";
|
|
||||||
|
|
||||||
type RunsTableProps = {
|
type RunsTableProps = {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -173,8 +171,73 @@ export function TaskRunsTable({
|
|||||||
)}
|
)}
|
||||||
<TableHeaderCell>Test</TableHeaderCell>
|
<TableHeaderCell>Test</TableHeaderCell>
|
||||||
<TableHeaderCell>Created at</TableHeaderCell>
|
<TableHeaderCell>Created at</TableHeaderCell>
|
||||||
<TableHeaderCell>Delayed until</TableHeaderCell>
|
<TableHeaderCell
|
||||||
<TableHeaderCell>TTL</TableHeaderCell>
|
tooltip={
|
||||||
|
<div className="max-w-xs p-1">
|
||||||
|
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
|
||||||
|
When you want to trigger a task now, but have it run at a later time, you can use
|
||||||
|
the delay option.
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
|
||||||
|
Runs that are delayed and have not been enqueued yet will display in the dashboard
|
||||||
|
with a “Delayed” status.
|
||||||
|
</Paragraph>
|
||||||
|
<LinkButton
|
||||||
|
to={docsPath("v3/triggering")}
|
||||||
|
variant="tertiary/small"
|
||||||
|
LeadingIcon={BookOpenIcon}
|
||||||
|
>
|
||||||
|
Read docs
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delayed until
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell
|
||||||
|
tooltip={
|
||||||
|
<div className="max-w-xs p-1">
|
||||||
|
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
|
||||||
|
You can set a TTL (time to live) when triggering a task, which will automatically
|
||||||
|
expire the run if it hasn’t started within the specified time.
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
|
||||||
|
All runs in development have a default ttl of 10 minutes. You can disable this by
|
||||||
|
setting the ttl option.
|
||||||
|
</Paragraph>
|
||||||
|
<LinkButton
|
||||||
|
to={docsPath("v3/triggering")}
|
||||||
|
variant="tertiary/small"
|
||||||
|
LeadingIcon={BookOpenIcon}
|
||||||
|
>
|
||||||
|
Read docs
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
TTL
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell
|
||||||
|
tooltip={
|
||||||
|
<div className="max-w-xs p-1">
|
||||||
|
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
|
||||||
|
You can add tags to a run and then filter runs using them.
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
|
||||||
|
You can add tags when triggering a run or inside the run function.
|
||||||
|
</Paragraph>
|
||||||
|
<LinkButton
|
||||||
|
to={docsPath("v3/tags")}
|
||||||
|
variant="tertiary/small"
|
||||||
|
LeadingIcon={BookOpenIcon}
|
||||||
|
>
|
||||||
|
Read docs
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Tags
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>
|
<TableHeaderCell>
|
||||||
<span className="sr-only">Go to page</span>
|
<span className="sr-only">Go to page</span>
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
@@ -278,6 +341,11 @@ export function TaskRunsTable({
|
|||||||
{run.delayUntil ? <DateTime date={run.delayUntil} /> : "–"}
|
{run.delayUntil ? <DateTime date={run.delayUntil} /> : "–"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell to={path}>{run.ttl ?? "–"}</TableCell>
|
<TableCell to={path}>{run.ttl ?? "–"}</TableCell>
|
||||||
|
<TableCell to={path}>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{run.tags.map((tag) => <RunTag key={tag} tag={tag} />) || "–"}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
<RunActionsCell run={run} path={path} />
|
<RunActionsCell run={run} path={path} />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
<svg width="9" height="25" viewBox="0 0 9 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_13512_47994" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="9" height="25">
|
||||||
|
<rect width="9" height="25" fill="#D9D9D9"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_13512_47994)">
|
||||||
|
<path d="M8.51694 0.5H10.5V24.5H8.51694C7.17088 24.5 5.94409 23.7281 5.36161 22.5146L1.69703 14.88C0.974863 13.3755 0.974863 11.6245 1.69703 10.12L5.3616 2.48544C5.94409 1.27194 7.17088 0.5 8.51694 0.5Z" fill="#1A1B1F" stroke="#272A2E" vectorEffect="non-scaling-stroke"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 598 B |
@@ -18,13 +18,13 @@ export function useSearchParams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
search.set(param, value);
|
search.set(param, encodeURIComponent(value));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
search.delete(param);
|
search.delete(param);
|
||||||
for (const v of value) {
|
for (const v of value) {
|
||||||
search.append(param, v);
|
search.append(param, encodeURIComponent(v));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -54,14 +54,20 @@ export function useSearchParams() {
|
|||||||
|
|
||||||
const value = useCallback(
|
const value = useCallback(
|
||||||
(param: string) => {
|
(param: string) => {
|
||||||
return search.get(param) ?? undefined;
|
const val = search.get(param) ?? undefined;
|
||||||
|
if (val === undefined) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeURIComponent(val);
|
||||||
},
|
},
|
||||||
[location, search]
|
[location, search]
|
||||||
);
|
);
|
||||||
|
|
||||||
const values = useCallback(
|
const values = useCallback(
|
||||||
(param: string) => {
|
(param: string) => {
|
||||||
return search.getAll(param);
|
const all = search.getAll(param);
|
||||||
|
return all.map((v) => decodeURIComponent(v));
|
||||||
},
|
},
|
||||||
[location, search]
|
[location, search]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { prisma } from "~/db.server";
|
||||||
|
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
|
||||||
|
|
||||||
|
export async function createTag({ tag, projectId }: { tag: string; projectId: string }) {
|
||||||
|
if (tag.trim().length === 0) return;
|
||||||
|
return prisma.taskRunTag.upsert({
|
||||||
|
where: {
|
||||||
|
projectId_name: {
|
||||||
|
projectId: projectId,
|
||||||
|
name: tag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: tag,
|
||||||
|
friendlyId: generateFriendlyId("runtag"),
|
||||||
|
projectId: projectId,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTagsForRunId({
|
||||||
|
friendlyId,
|
||||||
|
environmentId,
|
||||||
|
}: {
|
||||||
|
friendlyId: string;
|
||||||
|
environmentId: string;
|
||||||
|
}) {
|
||||||
|
const run = await prisma.taskRun.findFirst({
|
||||||
|
where: {
|
||||||
|
friendlyId,
|
||||||
|
runtimeEnvironmentId: environmentId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
tags: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return run?.tags ?? undefined;
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
|||||||
},
|
},
|
||||||
lockedToVersion: true,
|
lockedToVersion: true,
|
||||||
schedule: true,
|
schedule: true,
|
||||||
|
tags: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,6 +120,10 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
|||||||
isTest: taskRun.isTest,
|
isTest: taskRun.isTest,
|
||||||
ttl: taskRun.ttl ?? undefined,
|
ttl: taskRun.ttl ?? undefined,
|
||||||
expiredAt: taskRun.expiredAt ?? undefined,
|
expiredAt: taskRun.expiredAt ?? undefined,
|
||||||
|
tags: taskRun.tags.map((t) => t.name).sort((a, b) => a.localeCompare(b)),
|
||||||
|
costInCents: taskRun.costInCents,
|
||||||
|
baseCostInCents: taskRun.baseCostInCents,
|
||||||
|
durationMs: taskRun.usageDurationMs,
|
||||||
schedule: taskRun.schedule
|
schedule: taskRun.schedule
|
||||||
? {
|
? {
|
||||||
id: taskRun.schedule.friendlyId,
|
id: taskRun.schedule.friendlyId,
|
||||||
@@ -127,7 +132,7 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
|||||||
? taskRun.schedule.deduplicationKey
|
? taskRun.schedule.deduplicationKey
|
||||||
: undefined,
|
: undefined,
|
||||||
generator: {
|
generator: {
|
||||||
type: "CRON",
|
type: "CRON" as const,
|
||||||
expression: taskRun.schedule.generatorExpression,
|
expression: taskRun.schedule.generatorExpression,
|
||||||
description: taskRun.schedule.generatorDescription,
|
description: taskRun.schedule.generatorDescription,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ const SearchParamsSchema = z.object({
|
|||||||
.transform((value) => {
|
.transform((value) => {
|
||||||
return value ? value.split(",") : undefined;
|
return value ? value.split(",") : undefined;
|
||||||
}),
|
}),
|
||||||
|
"filter[tag]": z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => {
|
||||||
|
return value ? value.split(",") : undefined;
|
||||||
|
}),
|
||||||
"filter[bulkAction]": z.string().optional(),
|
"filter[bulkAction]": z.string().optional(),
|
||||||
"filter[schedule]": z.string().optional(),
|
"filter[schedule]": z.string().optional(),
|
||||||
"filter[isTest]": z
|
"filter[isTest]": z
|
||||||
@@ -168,6 +174,10 @@ export class ApiRunListPresenter extends BasePresenter {
|
|||||||
options.versions = $searchParams.data["filter[version]"];
|
options.versions = $searchParams.data["filter[version]"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["filter[tag]"]) {
|
||||||
|
options.tags = $searchParams.data["filter[tag]"];
|
||||||
|
}
|
||||||
|
|
||||||
if ($searchParams.data["filter[bulkAction]"]) {
|
if ($searchParams.data["filter[bulkAction]"]) {
|
||||||
options.bulkId = $searchParams.data["filter[bulkAction]"];
|
options.bulkId = $searchParams.data["filter[bulkAction]"];
|
||||||
}
|
}
|
||||||
@@ -218,6 +228,10 @@ export class ApiRunListPresenter extends BasePresenter {
|
|||||||
name: run.environment.slug,
|
name: run.environment.slug,
|
||||||
user: run.environment.userName,
|
user: run.environment.userName,
|
||||||
},
|
},
|
||||||
|
tags: run.tags,
|
||||||
|
costInCents: run.costInCents,
|
||||||
|
baseCostInCents: run.baseCostInCents,
|
||||||
|
durationMs: run.usageDurationMs,
|
||||||
...ApiRetrieveRunPresenter.apiBooleanHelpersFromRunStatus(
|
...ApiRetrieveRunPresenter.apiBooleanHelpersFromRunStatus(
|
||||||
ApiRetrieveRunPresenter.apiStatusFromRunStatus(run.status)
|
ApiRetrieveRunPresenter.apiStatusFromRunStatus(run.status)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Prisma, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database";
|
import { Prisma, type TaskRunStatus, type TaskTriggerSource } from "@trigger.dev/database";
|
||||||
import parse from "parse-duration";
|
import parse from "parse-duration";
|
||||||
import { Direction } from "~/components/runs/RunStatuses";
|
import { type Direction } from "~/components/runs/RunStatuses";
|
||||||
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||||
import { sqlDatabaseSchema } from "~/db.server";
|
import { sqlDatabaseSchema } from "~/db.server";
|
||||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||||
import { BasePresenter } from "./basePresenter.server";
|
|
||||||
import { isCancellableRunStatus } from "~/v3/taskStatus";
|
import { isCancellableRunStatus } from "~/v3/taskStatus";
|
||||||
|
import { BasePresenter } from "./basePresenter.server";
|
||||||
|
|
||||||
export type RunListOptions = {
|
export type RunListOptions = {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
@@ -15,6 +15,7 @@ export type RunListOptions = {
|
|||||||
versions?: string[];
|
versions?: string[];
|
||||||
statuses?: TaskRunStatus[];
|
statuses?: TaskRunStatus[];
|
||||||
environments?: string[];
|
environments?: string[];
|
||||||
|
tags?: string[];
|
||||||
scheduleId?: string;
|
scheduleId?: string;
|
||||||
period?: string;
|
period?: string;
|
||||||
bulkId?: string;
|
bulkId?: string;
|
||||||
@@ -41,6 +42,7 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
versions,
|
versions,
|
||||||
statuses,
|
statuses,
|
||||||
environments,
|
environments,
|
||||||
|
tags,
|
||||||
scheduleId,
|
scheduleId,
|
||||||
period,
|
period,
|
||||||
bulkId,
|
bulkId,
|
||||||
@@ -63,6 +65,7 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
from !== undefined ||
|
from !== undefined ||
|
||||||
to !== undefined ||
|
to !== undefined ||
|
||||||
(scheduleId !== undefined && scheduleId !== "") ||
|
(scheduleId !== undefined && scheduleId !== "") ||
|
||||||
|
(tags !== undefined && tags.length > 0) ||
|
||||||
typeof isTest === "boolean";
|
typeof isTest === "boolean";
|
||||||
|
|
||||||
// Find the project scoped to the organization
|
// Find the project scoped to the organization
|
||||||
@@ -95,10 +98,10 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
|
|
||||||
//get all possible tasks
|
//get all possible tasks
|
||||||
const possibleTasksAsync = this._replica.$queryRaw<
|
const possibleTasksAsync = this._replica.$queryRaw<
|
||||||
{
|
{
|
||||||
slug: string;
|
slug: string;
|
||||||
triggerSource: TaskTriggerSource
|
triggerSource: TaskTriggerSource;
|
||||||
}[]
|
}[]
|
||||||
>`
|
>`
|
||||||
SELECT DISTINCT(slug), "triggerSource"
|
SELECT DISTINCT(slug), "triggerSource"
|
||||||
FROM ${sqlDatabaseSchema}."BackgroundWorkerTask"
|
FROM ${sqlDatabaseSchema}."BackgroundWorkerTask"
|
||||||
@@ -150,7 +153,7 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
const periodMs = period ? parse(period) : undefined;
|
const periodMs = period ? parse(period) : undefined;
|
||||||
|
|
||||||
//get the runs
|
//get the runs
|
||||||
let runs = await this._replica.$queryRaw<
|
const runs = await this._replica.$queryRaw<
|
||||||
{
|
{
|
||||||
id: string;
|
id: string;
|
||||||
number: BigInt;
|
number: BigInt;
|
||||||
@@ -170,7 +173,9 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
ttl: string | null;
|
ttl: string | null;
|
||||||
expiredAt: Date | null;
|
expiredAt: Date | null;
|
||||||
costInCents: number;
|
costInCents: number;
|
||||||
|
baseCostInCents: number;
|
||||||
usageDurationMs: BigInt;
|
usageDurationMs: BigInt;
|
||||||
|
tags: string[];
|
||||||
}[]
|
}[]
|
||||||
>`
|
>`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -191,66 +196,88 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
tr."idempotencyKey" AS "idempotencyKey",
|
tr."idempotencyKey" AS "idempotencyKey",
|
||||||
tr."ttl" AS "ttl",
|
tr."ttl" AS "ttl",
|
||||||
tr."expiredAt" AS "expiredAt",
|
tr."expiredAt" AS "expiredAt",
|
||||||
|
tr."baseCostInCents" AS "baseCostInCents",
|
||||||
tr."costInCents" AS "costInCents",
|
tr."costInCents" AS "costInCents",
|
||||||
tr."usageDurationMs" AS "usageDurationMs"
|
tr."usageDurationMs" AS "usageDurationMs",
|
||||||
FROM
|
array_remove(array_agg(tag.name), NULL) AS "tags"
|
||||||
|
FROM
|
||||||
${sqlDatabaseSchema}."TaskRun" tr
|
${sqlDatabaseSchema}."TaskRun" tr
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
${sqlDatabaseSchema}."BackgroundWorker" bw ON tr."lockedToVersionId" = bw.id
|
${sqlDatabaseSchema}."BackgroundWorker" bw ON tr."lockedToVersionId" = bw.id
|
||||||
WHERE
|
LEFT JOIN
|
||||||
-- project
|
${sqlDatabaseSchema}."_TaskRunToTaskRunTag" trtg ON tr.id = trtg."A"
|
||||||
tr."projectId" = ${project.id}
|
LEFT JOIN
|
||||||
-- cursor
|
${sqlDatabaseSchema}."TaskRunTag" tag ON trtg."B" = tag.id
|
||||||
${
|
WHERE
|
||||||
cursor
|
-- project
|
||||||
? direction === "forward"
|
tr."projectId" = ${project.id}
|
||||||
? Prisma.sql`AND tr.id < ${cursor}`
|
-- cursor
|
||||||
: Prisma.sql`AND tr.id > ${cursor}`
|
${
|
||||||
: Prisma.empty
|
cursor
|
||||||
}
|
? direction === "forward"
|
||||||
-- filters
|
? Prisma.sql`AND tr.id < ${cursor}`
|
||||||
${
|
: Prisma.sql`AND tr.id > ${cursor}`
|
||||||
restrictToRunIds
|
: Prisma.empty
|
||||||
? restrictToRunIds.length === 0
|
}
|
||||||
? Prisma.sql`AND tr.id = ''`
|
-- filters
|
||||||
: Prisma.sql`AND tr.id IN (${Prisma.join(restrictToRunIds)})`
|
${
|
||||||
: Prisma.empty
|
restrictToRunIds
|
||||||
}
|
? restrictToRunIds.length === 0
|
||||||
${
|
? Prisma.sql`AND tr.id = ''`
|
||||||
tasks && tasks.length > 0
|
: Prisma.sql`AND tr.id IN (${Prisma.join(restrictToRunIds)})`
|
||||||
? Prisma.sql`AND tr."taskIdentifier" IN (${Prisma.join(tasks)})`
|
: Prisma.empty
|
||||||
: Prisma.empty
|
}
|
||||||
}
|
${
|
||||||
${
|
tasks && tasks.length > 0
|
||||||
statuses && statuses.length > 0
|
? Prisma.sql`AND tr."taskIdentifier" IN (${Prisma.join(tasks)})`
|
||||||
? Prisma.sql`AND tr.status = ANY(ARRAY[${Prisma.join(statuses)}]::"TaskRunStatus"[])`
|
: Prisma.empty
|
||||||
: Prisma.empty
|
}
|
||||||
}
|
${
|
||||||
${
|
statuses && statuses.length > 0
|
||||||
environments && environments.length > 0
|
? Prisma.sql`AND tr.status = ANY(ARRAY[${Prisma.join(statuses)}]::"TaskRunStatus"[])`
|
||||||
? Prisma.sql`AND tr."runtimeEnvironmentId" IN (${Prisma.join(environments)})`
|
: Prisma.empty
|
||||||
: Prisma.empty
|
}
|
||||||
}
|
${
|
||||||
${scheduleId ? Prisma.sql`AND tr."scheduleId" = ${scheduleId}` : Prisma.empty}
|
environments && environments.length > 0
|
||||||
${typeof isTest === "boolean" ? Prisma.sql`AND tr."isTest" = ${isTest}` : Prisma.empty}
|
? Prisma.sql`AND tr."runtimeEnvironmentId" IN (${Prisma.join(environments)})`
|
||||||
${
|
: Prisma.empty
|
||||||
periodMs
|
}
|
||||||
? Prisma.sql`AND tr."createdAt" >= NOW() - INTERVAL '1 millisecond' * ${periodMs}`
|
${scheduleId ? Prisma.sql`AND tr."scheduleId" = ${scheduleId}` : Prisma.empty}
|
||||||
: Prisma.empty
|
${typeof isTest === "boolean" ? Prisma.sql`AND tr."isTest" = ${isTest}` : Prisma.empty}
|
||||||
}
|
${
|
||||||
${
|
periodMs
|
||||||
from
|
? Prisma.sql`AND tr."createdAt" >= NOW() - INTERVAL '1 millisecond' * ${periodMs}`
|
||||||
? Prisma.sql`AND tr."createdAt" >= ${new Date(from).toISOString()}::timestamp`
|
: Prisma.empty
|
||||||
: Prisma.empty
|
}
|
||||||
}
|
${
|
||||||
${
|
from
|
||||||
to
|
? Prisma.sql`AND tr."createdAt" >= ${new Date(from).toISOString()}::timestamp`
|
||||||
? Prisma.sql`AND tr."createdAt" <= ${new Date(to).toISOString()}::timestamp`
|
: Prisma.empty
|
||||||
: Prisma.empty
|
}
|
||||||
}
|
${
|
||||||
ORDER BY
|
to ? Prisma.sql`AND tr."createdAt" <= ${new Date(to).toISOString()}::timestamp` : Prisma.empty
|
||||||
${direction === "forward" ? Prisma.sql`tr.id DESC` : Prisma.sql`tr.id ASC`}
|
}
|
||||||
LIMIT ${pageSize + 1}`;
|
${
|
||||||
|
tags && tags.length > 0
|
||||||
|
? Prisma.sql`AND (
|
||||||
|
tr.id IN (
|
||||||
|
SELECT
|
||||||
|
trtg."A"
|
||||||
|
FROM
|
||||||
|
${sqlDatabaseSchema}."_TaskRunToTaskRunTag" trtg
|
||||||
|
JOIN
|
||||||
|
${sqlDatabaseSchema}."TaskRunTag" tag ON trtg."B" = tag.id
|
||||||
|
WHERE
|
||||||
|
tag.name IN (${Prisma.join(tags)})
|
||||||
|
)
|
||||||
|
)`
|
||||||
|
: Prisma.empty
|
||||||
|
}
|
||||||
|
GROUP BY
|
||||||
|
tr.id, bw.version
|
||||||
|
ORDER BY
|
||||||
|
${direction === "forward" ? Prisma.sql`tr.id DESC` : Prisma.sql`tr.id ASC`}
|
||||||
|
LIMIT ${pageSize + 1}`;
|
||||||
|
|
||||||
const hasMore = runs.length > pageSize;
|
const hasMore = runs.length > pageSize;
|
||||||
|
|
||||||
@@ -312,7 +339,9 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
ttl: run.ttl ? run.ttl : undefined,
|
ttl: run.ttl ? run.ttl : undefined,
|
||||||
expiredAt: run.expiredAt ? run.expiredAt.toISOString() : undefined,
|
expiredAt: run.expiredAt ? run.expiredAt.toISOString() : undefined,
|
||||||
costInCents: run.costInCents,
|
costInCents: run.costInCents,
|
||||||
|
baseCostInCents: run.baseCostInCents,
|
||||||
usageDurationMs: Number(run.usageDurationMs),
|
usageDurationMs: Number(run.usageDurationMs),
|
||||||
|
tags: run.tags.sort((a, b) => a.localeCompare(b)),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { logger } from "~/services/logger.server";
|
||||||
|
import { BasePresenter } from "./basePresenter.server";
|
||||||
|
|
||||||
|
export type TagListOptions = {
|
||||||
|
userId?: string;
|
||||||
|
projectId: string;
|
||||||
|
//filters
|
||||||
|
names?: string[];
|
||||||
|
environments?: string[];
|
||||||
|
//pagination
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_PAGE_SIZE = 25;
|
||||||
|
|
||||||
|
export type TagList = Awaited<ReturnType<RunTagListPresenter["call"]>>;
|
||||||
|
export type TagListItem = TagList["tags"][number];
|
||||||
|
|
||||||
|
export class RunTagListPresenter extends BasePresenter {
|
||||||
|
public async call({
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
names,
|
||||||
|
environments,
|
||||||
|
page = 1,
|
||||||
|
pageSize = DEFAULT_PAGE_SIZE,
|
||||||
|
}: TagListOptions) {
|
||||||
|
const hasFilters =
|
||||||
|
(names !== undefined && names.length > 0) ||
|
||||||
|
(environments !== undefined && environments.length > 0);
|
||||||
|
|
||||||
|
const tags = await this._replica.taskRunTag.findMany({
|
||||||
|
where: {
|
||||||
|
projectId,
|
||||||
|
OR:
|
||||||
|
names && names.length > 0
|
||||||
|
? names.map((name) => ({ name: { contains: name, mode: "insensitive" } }))
|
||||||
|
: undefined,
|
||||||
|
project: environments
|
||||||
|
? {
|
||||||
|
environments: {
|
||||||
|
some: {
|
||||||
|
id: {
|
||||||
|
in: environments,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
id: "desc",
|
||||||
|
},
|
||||||
|
take: pageSize + 1,
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log("tags", {
|
||||||
|
tags,
|
||||||
|
projectId,
|
||||||
|
names,
|
||||||
|
environments,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags: tags
|
||||||
|
.map((tag) => ({
|
||||||
|
id: tag.friendlyId,
|
||||||
|
name: tag.name,
|
||||||
|
}))
|
||||||
|
.slice(0, pageSize),
|
||||||
|
currentPage: page,
|
||||||
|
hasMore: tags.length > pageSize,
|
||||||
|
hasFilters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-2
@@ -56,9 +56,21 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
|||||||
tasks: url.searchParams.getAll("tasks"),
|
tasks: url.searchParams.getAll("tasks"),
|
||||||
period: url.searchParams.get("period") ?? undefined,
|
period: url.searchParams.get("period") ?? undefined,
|
||||||
bulkId: url.searchParams.get("bulkId") ?? undefined,
|
bulkId: url.searchParams.get("bulkId") ?? undefined,
|
||||||
|
tags: url.searchParams.getAll("tags").map((t) => decodeURIComponent(t)),
|
||||||
};
|
};
|
||||||
const { tasks, versions, statuses, environments, period, bulkId, from, to, cursor, direction } =
|
const {
|
||||||
TaskRunListSearchFilters.parse(s);
|
tasks,
|
||||||
|
versions,
|
||||||
|
statuses,
|
||||||
|
environments,
|
||||||
|
tags,
|
||||||
|
period,
|
||||||
|
bulkId,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
cursor,
|
||||||
|
direction,
|
||||||
|
} = TaskRunListSearchFilters.parse(s);
|
||||||
|
|
||||||
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
|
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
|
||||||
|
|
||||||
@@ -74,6 +86,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
|||||||
versions,
|
versions,
|
||||||
statuses,
|
statuses,
|
||||||
environments,
|
environments,
|
||||||
|
tags,
|
||||||
period,
|
period,
|
||||||
bulkId,
|
bulkId,
|
||||||
from,
|
from,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { conform, useForm } from "@conform-to/react";
|
|||||||
import { parse } from "@conform-to/zod";
|
import { parse } from "@conform-to/zod";
|
||||||
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node";
|
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node";
|
||||||
import { json } from "@remix-run/node";
|
import { json } from "@remix-run/node";
|
||||||
import { Form, useActionData } from "@remix-run/react";
|
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -17,6 +17,7 @@ import { Input } from "~/components/primitives/Input";
|
|||||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||||
import { Label } from "~/components/primitives/Label";
|
import { Label } from "~/components/primitives/Label";
|
||||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||||
|
import { ButtonSpinner } from "~/components/primitives/Spinner";
|
||||||
import { prisma } from "~/db.server";
|
import { prisma } from "~/db.server";
|
||||||
import { featuresForRequest } from "~/features.server";
|
import { featuresForRequest } from "~/features.server";
|
||||||
import { useFeatures } from "~/hooks/useFeatures";
|
import { useFeatures } from "~/hooks/useFeatures";
|
||||||
@@ -135,6 +136,9 @@ export default function Page() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const isLoading = navigation.state === "submitting" || navigation.state === "loading";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainCenteredContainer>
|
<MainCenteredContainer>
|
||||||
<div>
|
<div>
|
||||||
@@ -190,8 +194,13 @@ export default function Page() {
|
|||||||
)}
|
)}
|
||||||
<FormButtons
|
<FormButtons
|
||||||
confirmButton={
|
confirmButton={
|
||||||
<Button type="submit" variant={"primary/small"}>
|
<Button
|
||||||
Create
|
type="submit"
|
||||||
|
variant={"primary/small"}
|
||||||
|
disabled={isLoading}
|
||||||
|
TrailingIcon={isLoading ? ButtonSpinner : undefined}
|
||||||
|
>
|
||||||
|
{isLoading ? "Creating…" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
cancelButton={
|
cancelButton={
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
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 { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||||
|
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
|
||||||
|
|
||||||
|
const ParamsSchema = z.object({
|
||||||
|
runId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function action({ request, params }: ActionFunctionArgs) {
|
||||||
|
// Ensure this is a POST request
|
||||||
|
if (request.method.toUpperCase() !== "POST") {
|
||||||
|
return { status: 405, body: "Method Not Allowed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate the request
|
||||||
|
const authenticationResult = await authenticateApiRequest(request);
|
||||||
|
if (!authenticationResult) {
|
||||||
|
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedParams = ParamsSchema.safeParse(params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return json(
|
||||||
|
{ error: "Invalid request parameters", issues: parsedParams.error.issues },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const anyBody = await request.json();
|
||||||
|
|
||||||
|
const body = AddTagsRequestBody.safeParse(anyBody);
|
||||||
|
if (!body.success) {
|
||||||
|
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingTags =
|
||||||
|
(await getTagsForRunId({
|
||||||
|
friendlyId: parsedParams.data.runId,
|
||||||
|
environmentId: authenticationResult.environment.id,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
//remove duplicate tags from the new tags
|
||||||
|
const bodyTags = typeof body.data.tags === "string" ? [body.data.tags] : body.data.tags;
|
||||||
|
const newTags = bodyTags.filter((tag) => {
|
||||||
|
if (tag.trim().length === 0) return false;
|
||||||
|
return !existingTags.map((t) => t.name).includes(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingTags.length + newTags.length > 3) {
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
error: `Runs can only have 3 tags, you're trying to set ${
|
||||||
|
existingTags.length + newTags.length
|
||||||
|
}.`,
|
||||||
|
},
|
||||||
|
{ status: 422 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTags.length === 0) {
|
||||||
|
return json({ message: "No new tags to add" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
//create tags
|
||||||
|
let tagIds: string[] = existingTags.map((t) => t.id);
|
||||||
|
if (newTags.length > 0) {
|
||||||
|
for (const tag of newTags) {
|
||||||
|
const tagRecord = await createTag({
|
||||||
|
tag,
|
||||||
|
projectId: authenticationResult.environment.projectId,
|
||||||
|
});
|
||||||
|
if (tagRecord) {
|
||||||
|
tagIds.push(tagRecord.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskRun = await prisma.taskRun.update({
|
||||||
|
where: {
|
||||||
|
friendlyId: parsedParams.data.runId,
|
||||||
|
runtimeEnvironmentId: authenticationResult.environment.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
tags: {
|
||||||
|
connect: tagIds.map((id) => ({ id })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return json({ message: `Successfully set ${newTags.length} new tags.` }, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
return json(
|
||||||
|
{ error: error instanceof Error ? error.message : "Internal Server Error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { $replica } from "~/db.server";
|
||||||
|
import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server";
|
||||||
|
import { requireUserId } from "~/services/session.server";
|
||||||
|
|
||||||
|
const Params = z.object({
|
||||||
|
projectParam: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||||
|
const userId = await requireUserId(request);
|
||||||
|
const { projectParam } = Params.parse(params);
|
||||||
|
|
||||||
|
const project = await $replica.project.findFirst({
|
||||||
|
where: { slug: projectParam, deletedAt: null, organization: { members: { some: { userId } } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new Response("Not Found", { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = new URL(request.url).searchParams;
|
||||||
|
const name = search.get("name");
|
||||||
|
|
||||||
|
const presenter = new RunTagListPresenter();
|
||||||
|
const result = await presenter.call({
|
||||||
|
projectId: project.id,
|
||||||
|
names: name ? [decodeURIComponent(name)] : undefined,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { conditionallyImportPacket, parsePacket } from "@trigger.dev/core/v3";
|
import { conditionallyImportPacket, parsePacket, RunTags } from "@trigger.dev/core/v3";
|
||||||
import { TaskRun } from "@trigger.dev/database";
|
import { TaskRun } from "@trigger.dev/database";
|
||||||
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
|
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
|
||||||
import { logger } from "~/services/logger.server";
|
import { logger } from "~/services/logger.server";
|
||||||
import { BaseService } from "./baseService.server";
|
import { BaseService } from "./baseService.server";
|
||||||
import { OutOfEntitlementError, TriggerTaskService } from "./triggerTask.server";
|
import { OutOfEntitlementError, TriggerTaskService } from "./triggerTask.server";
|
||||||
|
import { getTagsForRunId } from "~/models/taskRunTag.server";
|
||||||
|
|
||||||
export class ReplayTaskRunService extends BaseService {
|
export class ReplayTaskRunService extends BaseService {
|
||||||
public async call(existingTaskRun: TaskRun) {
|
public async call(existingTaskRun: TaskRun) {
|
||||||
@@ -36,6 +37,11 @@ export class ReplayTaskRunService extends BaseService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const tags = await getTagsForRunId({
|
||||||
|
friendlyId: existingTaskRun.id,
|
||||||
|
environmentId: authenticatedEnvironment.id,
|
||||||
|
});
|
||||||
|
|
||||||
const triggerTaskService = new TriggerTaskService();
|
const triggerTaskService = new TriggerTaskService();
|
||||||
return await triggerTaskService.call(
|
return await triggerTaskService.call(
|
||||||
existingTaskRun.taskIdentifier,
|
existingTaskRun.taskIdentifier,
|
||||||
@@ -49,6 +55,7 @@ export class ReplayTaskRunService extends BaseService {
|
|||||||
concurrencyKey: existingTaskRun.concurrencyKey ?? undefined,
|
concurrencyKey: existingTaskRun.concurrencyKey ?? undefined,
|
||||||
test: existingTaskRun.isTest,
|
test: existingTaskRun.isTest,
|
||||||
payloadType: payloadPacket.dataType,
|
payloadType: payloadPacket.dataType,
|
||||||
|
tags: tags?.map((t) => t.name) as RunTags,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { getEntitlement } from "~/services/platform.v3.server";
|
|||||||
import { BaseService, ServiceValidationError } from "./baseService.server";
|
import { BaseService, ServiceValidationError } from "./baseService.server";
|
||||||
import { logger } from "~/services/logger.server";
|
import { logger } from "~/services/logger.server";
|
||||||
import { isFinalAttemptStatus, isFinalRunStatus } from "../taskStatus";
|
import { isFinalAttemptStatus, isFinalRunStatus } from "../taskStatus";
|
||||||
|
import { createTag } from "~/models/taskRunTag.server";
|
||||||
|
|
||||||
export type TriggerTaskServiceOptions = {
|
export type TriggerTaskServiceOptions = {
|
||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
@@ -210,6 +211,22 @@ export class TriggerTaskService extends BaseService {
|
|||||||
event.setAttribute("queueName", queueName);
|
event.setAttribute("queueName", queueName);
|
||||||
span.setAttribute("queueName", queueName);
|
span.setAttribute("queueName", queueName);
|
||||||
|
|
||||||
|
//upsert tags
|
||||||
|
let tagIds: string[] = [];
|
||||||
|
const bodyTags =
|
||||||
|
typeof body.options?.tags === "string" ? [body.options.tags] : body.options?.tags;
|
||||||
|
if (bodyTags && bodyTags.length > 0) {
|
||||||
|
for (const tag of bodyTags) {
|
||||||
|
const tagRecord = await createTag({
|
||||||
|
tag,
|
||||||
|
projectId: environment.projectId,
|
||||||
|
});
|
||||||
|
if (tagRecord) {
|
||||||
|
tagIds.push(tagRecord.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const taskRun = await tx.taskRun.create({
|
const taskRun = await tx.taskRun.create({
|
||||||
data: {
|
data: {
|
||||||
status: delayUntil ? "DELAYED" : "PENDING",
|
status: delayUntil ? "DELAYED" : "PENDING",
|
||||||
@@ -233,6 +250,12 @@ export class TriggerTaskService extends BaseService {
|
|||||||
queuedAt: delayUntil ? undefined : new Date(),
|
queuedAt: delayUntil ? undefined : new Date(),
|
||||||
maxAttempts: body.options?.maxAttempts,
|
maxAttempts: body.options?.maxAttempts,
|
||||||
ttl,
|
ttl,
|
||||||
|
tags:
|
||||||
|
tagIds.length === 0
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
connect: tagIds.map((id) => ({ id })),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { context, propagation } from "@opentelemetry/api";
|
import { context, propagation } from "@opentelemetry/api";
|
||||||
|
import { z } from "zod";
|
||||||
import { version } from "../../../package.json";
|
import { version } from "../../../package.json";
|
||||||
import {
|
import {
|
||||||
|
AddTagsRequestBody,
|
||||||
BatchTaskRunExecutionResult,
|
BatchTaskRunExecutionResult,
|
||||||
BatchTriggerTaskRequestBody,
|
BatchTriggerTaskRequestBody,
|
||||||
BatchTriggerTaskResponse,
|
BatchTriggerTaskResponse,
|
||||||
@@ -63,8 +65,8 @@ const DEFAULT_ZOD_FETCH_OPTIONS: ZodFetchOptions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { ApiRequestOptions };
|
|
||||||
export { isRequestOptions };
|
export { isRequestOptions };
|
||||||
|
export type { ApiRequestOptions };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger.dev v3 API client
|
* Trigger.dev v3 API client
|
||||||
@@ -289,6 +291,19 @@ export class ApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addTags(runId: string, body: AddTagsRequestBody, requestOptions?: ZodFetchOptions) {
|
||||||
|
return zodfetch(
|
||||||
|
z.object({ message: z.string() }),
|
||||||
|
`${this.baseUrl}/api/v1/runs/${runId}/tags`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: this.#getHeaders(false),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
},
|
||||||
|
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
createSchedule(options: CreateScheduleOptions, requestOptions?: ZodFetchOptions) {
|
createSchedule(options: CreateScheduleOptions, requestOptions?: ZodFetchOptions) {
|
||||||
return zodfetch(
|
return zodfetch(
|
||||||
ScheduleObject,
|
ScheduleObject,
|
||||||
@@ -534,6 +549,13 @@ function createSearchQueryForListRuns(query?: ListRunsQueryParams): URLSearchPar
|
|||||||
searchParams.append("filter[bulkAction]", query.bulkAction);
|
searchParams.append("filter[bulkAction]", query.bulkAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.tag) {
|
||||||
|
searchParams.append(
|
||||||
|
"filter[tag]",
|
||||||
|
Array.isArray(query.tag) ? query.tag.join(",") : query.tag
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (query.schedule) {
|
if (query.schedule) {
|
||||||
searchParams.append("filter[schedule]", query.schedule);
|
searchParams.append("filter[schedule]", query.schedule);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface ListRunsQueryParams extends CursorPageParams {
|
|||||||
to?: Date | number;
|
to?: Date | number;
|
||||||
period?: string;
|
period?: string;
|
||||||
bulkAction?: string;
|
bulkAction?: string;
|
||||||
|
tag?: Array<string> | string;
|
||||||
schedule?: string;
|
schedule?: string;
|
||||||
isTest?: boolean;
|
isTest?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,17 @@ export const CreateBackgroundWorkerResponse = z.object({
|
|||||||
|
|
||||||
export type CreateBackgroundWorkerResponse = z.infer<typeof CreateBackgroundWorkerResponse>;
|
export type CreateBackgroundWorkerResponse = z.infer<typeof CreateBackgroundWorkerResponse>;
|
||||||
|
|
||||||
|
//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,
|
||||||
|
z.tuple([RunTag]),
|
||||||
|
z.tuple([RunTag, RunTag]),
|
||||||
|
z.tuple([RunTag, RunTag, RunTag]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type RunTags = z.infer<typeof RunTags>;
|
||||||
|
|
||||||
export const TriggerTaskRequestBody = z.object({
|
export const TriggerTaskRequestBody = z.object({
|
||||||
payload: z.any(),
|
payload: z.any(),
|
||||||
context: z.any(),
|
context: z.any(),
|
||||||
@@ -70,6 +81,7 @@ export const TriggerTaskRequestBody = z.object({
|
|||||||
payloadType: z.string().optional(),
|
payloadType: z.string().optional(),
|
||||||
delay: z.string().or(z.coerce.date()).optional(),
|
delay: z.string().or(z.coerce.date()).optional(),
|
||||||
ttl: z.string().or(z.number().nonnegative().int()).optional(),
|
ttl: z.string().or(z.number().nonnegative().int()).optional(),
|
||||||
|
tags: RunTags.optional(),
|
||||||
maxAttempts: z.number().int().optional(),
|
maxAttempts: z.number().int().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -110,6 +122,12 @@ export const GetBatchResponseBody = z.object({
|
|||||||
|
|
||||||
export type GetBatchResponseBody = z.infer<typeof GetBatchResponseBody>;
|
export type GetBatchResponseBody = z.infer<typeof GetBatchResponseBody>;
|
||||||
|
|
||||||
|
export const AddTagsRequestBody = z.object({
|
||||||
|
tags: RunTags,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AddTagsRequestBody = z.infer<typeof AddTagsRequestBody>;
|
||||||
|
|
||||||
export const RescheduleRunRequestBody = z.object({
|
export const RescheduleRunRequestBody = z.object({
|
||||||
delay: z.string().or(z.coerce.date()),
|
delay: z.string().or(z.coerce.date()),
|
||||||
});
|
});
|
||||||
@@ -452,6 +470,10 @@ const CommonRunFields = {
|
|||||||
delayedUntil: z.coerce.date().optional(),
|
delayedUntil: z.coerce.date().optional(),
|
||||||
ttl: z.string().optional(),
|
ttl: z.string().optional(),
|
||||||
expiredAt: z.coerce.date().optional(),
|
expiredAt: z.coerce.date().optional(),
|
||||||
|
tags: z.string().array(),
|
||||||
|
costInCents: z.number(),
|
||||||
|
baseCostInCents: z.number(),
|
||||||
|
durationMs: z.number(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RetrieveRunResponse = z.object({
|
export const RetrieveRunResponse = z.object({
|
||||||
|
|||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `TaskTag` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `_TaskRunToTaskTag` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "TaskTag" DROP CONSTRAINT "TaskTag_projectId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_TaskRunToTaskTag" DROP CONSTRAINT "_TaskRunToTaskTag_A_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_TaskRunToTaskTag" DROP CONSTRAINT "_TaskRunToTaskTag_B_fkey";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "TaskTag";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "_TaskRunToTaskTag";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TaskRunTag" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"friendlyId" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "TaskRunTag_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_TaskRunToTaskRunTag" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TaskRunTag_friendlyId_key" ON "TaskRunTag"("friendlyId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TaskRunTag_projectId_name_key" ON "TaskRunTag"("projectId", "name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_TaskRunToTaskRunTag_AB_unique" ON "_TaskRunToTaskRunTag"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_TaskRunToTaskRunTag_B_index" ON "_TaskRunToTaskRunTag"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TaskRunTag" ADD CONSTRAINT "TaskRunTag_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_TaskRunToTaskRunTag" ADD CONSTRAINT "_TaskRunToTaskRunTag_A_fkey" FOREIGN KEY ("A") REFERENCES "TaskRun"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_TaskRunToTaskRunTag" ADD CONSTRAINT "_TaskRunToTaskRunTag_B_fkey" FOREIGN KEY ("B") REFERENCES "TaskRunTag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TaskRunTag_name_id_idx" ON "TaskRunTag"("name", "id");
|
||||||
@@ -453,7 +453,7 @@ model Project {
|
|||||||
backgroundWorkers BackgroundWorker[]
|
backgroundWorkers BackgroundWorker[]
|
||||||
backgroundWorkerTasks BackgroundWorkerTask[]
|
backgroundWorkerTasks BackgroundWorkerTask[]
|
||||||
taskRuns TaskRun[]
|
taskRuns TaskRun[]
|
||||||
taskTags TaskTag[]
|
runTags TaskRunTag[]
|
||||||
taskQueues TaskQueue[]
|
taskQueues TaskQueue[]
|
||||||
environmentVariables EnvironmentVariable[]
|
environmentVariables EnvironmentVariable[]
|
||||||
checkpoints Checkpoint[]
|
checkpoints Checkpoint[]
|
||||||
@@ -1641,8 +1641,9 @@ model TaskRun {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
attempts TaskRunAttempt[] @relation("attempts")
|
attempts TaskRunAttempt[] @relation("attempts")
|
||||||
tags TaskTag[]
|
tags TaskRunTag[]
|
||||||
|
|
||||||
checkpoints Checkpoint[]
|
checkpoints Checkpoint[]
|
||||||
|
|
||||||
startedAt DateTime?
|
startedAt DateTime?
|
||||||
@@ -1735,6 +1736,24 @@ enum TaskRunStatus {
|
|||||||
EXPIRED
|
EXPIRED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model TaskRunTag {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
|
||||||
|
friendlyId String @unique
|
||||||
|
|
||||||
|
runs TaskRun[]
|
||||||
|
|
||||||
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
projectId String
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([projectId, name])
|
||||||
|
//Makes run filtering by tag faster
|
||||||
|
@@index([name, id])
|
||||||
|
}
|
||||||
|
|
||||||
model TaskRunDependency {
|
model TaskRunDependency {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|
||||||
@@ -1775,22 +1794,6 @@ model TaskRunNumberCounter {
|
|||||||
@@unique([taskIdentifier, environmentId])
|
@@unique([taskIdentifier, environmentId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model TaskTag {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
name String
|
|
||||||
|
|
||||||
friendlyId String @unique
|
|
||||||
|
|
||||||
runs TaskRun[]
|
|
||||||
|
|
||||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
|
||||||
projectId String
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
|
|
||||||
@@unique([projectId, name])
|
|
||||||
}
|
|
||||||
|
|
||||||
model TaskRunAttempt {
|
model TaskRunAttempt {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
number Int @default(0)
|
number Int @default(0)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export * from "./tasks";
|
|||||||
export * from "./wait";
|
export * from "./wait";
|
||||||
export * from "./usage";
|
export * from "./usage";
|
||||||
export * from "./idempotencyKeys";
|
export * from "./idempotencyKeys";
|
||||||
|
export * from "./tags";
|
||||||
export type { Context };
|
export type { Context };
|
||||||
|
|
||||||
import type { Context } from "./shared";
|
import type { Context } from "./shared";
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
QueueOptions,
|
QueueOptions,
|
||||||
RetryOptions,
|
RetryOptions,
|
||||||
RunFnParams,
|
RunFnParams,
|
||||||
|
RunTags,
|
||||||
SemanticInternalAttributes,
|
SemanticInternalAttributes,
|
||||||
StartFnParams,
|
StartFnParams,
|
||||||
SuccessFnParams,
|
SuccessFnParams,
|
||||||
@@ -440,6 +441,21 @@ export type TaskRunOptions = {
|
|||||||
* **Note:** Runs in development have a default `ttl` of 10 minutes. You can override this by setting the `ttl` option.
|
* **Note:** Runs in development have a default `ttl` of 10 minutes. You can override this by setting the `ttl` option.
|
||||||
*/
|
*/
|
||||||
ttl?: string | number;
|
ttl?: string | number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tags to attach to the run. Tags can be used to filter runs in the dashboard and using the SDK.
|
||||||
|
*
|
||||||
|
* You can set up to 3 tags per run, they must be less than 64 characters each.
|
||||||
|
*
|
||||||
|
* We recommend prefixing tags with a namespace using an underscore or colon, like `user_1234567` or `org:9876543`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* await myTask.trigger({ foo: "bar" }, { tags: ["user:1234567", "org:9876543"] });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
tags?: RunTags;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TaskRunConcurrencyOptions = Queue;
|
type TaskRunConcurrencyOptions = Queue;
|
||||||
@@ -485,6 +501,7 @@ export function createTask<
|
|||||||
idempotencyKey: await makeKey(options?.idempotencyKey),
|
idempotencyKey: await makeKey(options?.idempotencyKey),
|
||||||
delay: options?.delay,
|
delay: options?.delay,
|
||||||
ttl: options?.ttl,
|
ttl: options?.ttl,
|
||||||
|
tags: options?.tags,
|
||||||
maxAttempts: options?.maxAttempts,
|
maxAttempts: options?.maxAttempts,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -547,6 +564,7 @@ export function createTask<
|
|||||||
idempotencyKey: await makeKey(item.options?.idempotencyKey),
|
idempotencyKey: await makeKey(item.options?.idempotencyKey),
|
||||||
delay: item.options?.delay,
|
delay: item.options?.delay,
|
||||||
ttl: item.options?.ttl,
|
ttl: item.options?.ttl,
|
||||||
|
tags: item.options?.tags,
|
||||||
maxAttempts: item.options?.maxAttempts,
|
maxAttempts: item.options?.maxAttempts,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -616,6 +634,7 @@ export function createTask<
|
|||||||
idempotencyKey: await makeKey(options?.idempotencyKey),
|
idempotencyKey: await makeKey(options?.idempotencyKey),
|
||||||
delay: options?.delay,
|
delay: options?.delay,
|
||||||
ttl: options?.ttl,
|
ttl: options?.ttl,
|
||||||
|
tags: options?.tags,
|
||||||
maxAttempts: options?.maxAttempts,
|
maxAttempts: options?.maxAttempts,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -701,6 +720,7 @@ export function createTask<
|
|||||||
idempotencyKey: await makeKey(item.options?.idempotencyKey),
|
idempotencyKey: await makeKey(item.options?.idempotencyKey),
|
||||||
delay: item.options?.delay,
|
delay: item.options?.delay,
|
||||||
ttl: item.options?.ttl,
|
ttl: item.options?.ttl,
|
||||||
|
tags: item.options?.tags,
|
||||||
maxAttempts: item.options?.maxAttempts,
|
maxAttempts: item.options?.maxAttempts,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -866,6 +886,7 @@ export async function trigger<TTask extends AnyTask>(
|
|||||||
idempotencyKey: await makeKey(options?.idempotencyKey),
|
idempotencyKey: await makeKey(options?.idempotencyKey),
|
||||||
delay: options?.delay,
|
delay: options?.delay,
|
||||||
ttl: options?.ttl,
|
ttl: options?.ttl,
|
||||||
|
tags: options?.tags,
|
||||||
maxAttempts: options?.maxAttempts,
|
maxAttempts: options?.maxAttempts,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -942,6 +963,7 @@ export async function triggerAndWait<TTask extends AnyTask>(
|
|||||||
idempotencyKey: await makeKey(options?.idempotencyKey),
|
idempotencyKey: await makeKey(options?.idempotencyKey),
|
||||||
delay: options?.delay,
|
delay: options?.delay,
|
||||||
ttl: options?.ttl,
|
ttl: options?.ttl,
|
||||||
|
tags: options?.tags,
|
||||||
maxAttempts: options?.maxAttempts,
|
maxAttempts: options?.maxAttempts,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1051,6 +1073,7 @@ export async function batchTrigger<TTask extends AnyTask>(
|
|||||||
idempotencyKey: await makeKey(item.options?.idempotencyKey),
|
idempotencyKey: await makeKey(item.options?.idempotencyKey),
|
||||||
delay: item.options?.delay,
|
delay: item.options?.delay,
|
||||||
ttl: item.options?.ttl,
|
ttl: item.options?.ttl,
|
||||||
|
tags: item.options?.tags,
|
||||||
maxAttempts: item.options?.maxAttempts,
|
maxAttempts: item.options?.maxAttempts,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { ApiRequestOptions, RunTags } from "@trigger.dev/core/v3";
|
||||||
|
import {
|
||||||
|
UnprocessableEntityError,
|
||||||
|
accessoryAttributes,
|
||||||
|
apiClientManager,
|
||||||
|
logger,
|
||||||
|
mergeRequestOptions,
|
||||||
|
taskContext,
|
||||||
|
} from "@trigger.dev/core/v3";
|
||||||
|
import { apiClientMissingError } from "./shared";
|
||||||
|
import { tracer } from "./tracer";
|
||||||
|
|
||||||
|
export const tags = {
|
||||||
|
add: addTags,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function addTags(tags: RunTags, requestOptions?: ApiRequestOptions) {
|
||||||
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
|
if (!apiClient) {
|
||||||
|
throw apiClientMissingError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = taskContext.ctx?.run;
|
||||||
|
if (!run) {
|
||||||
|
throw new Error(
|
||||||
|
"Can't set tags outside of a run. You can trigger a task and set tags in the options."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const $requestOptions = mergeRequestOptions(
|
||||||
|
{
|
||||||
|
tracer,
|
||||||
|
name: "tags.set()",
|
||||||
|
icon: "tag",
|
||||||
|
attributes: {
|
||||||
|
...accessoryAttributes({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: typeof tags === "string" ? tags : tags.join(", "),
|
||||||
|
variant: "normal",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
style: "codepath",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.addTags(run.id, { tags }, $requestOptions);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UnprocessableEntityError) {
|
||||||
|
logger.error(error.message, {
|
||||||
|
existingTags: run.tags,
|
||||||
|
newTags: tags,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Failed to set tags", { error });
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { logger, task, wait, tasks } from "@trigger.dev/sdk/v3";
|
import { logger, task, wait, tasks, tags } from "@trigger.dev/sdk/v3";
|
||||||
import { taskWithRetries } from "./retries";
|
import { taskWithRetries } from "./retries";
|
||||||
|
|
||||||
export const simpleParentTask = task({
|
export const simpleParentTask = task({
|
||||||
@@ -27,6 +27,9 @@ export const simpleChildTask = task({
|
|||||||
run: async (payload: { message: string }, { ctx }) => {
|
run: async (payload: { message: string }, { ctx }) => {
|
||||||
logger.log("Simple child task payload", { payload, ctx });
|
logger.log("Simple child task payload", { payload, ctx });
|
||||||
|
|
||||||
|
logger.log("Context tags", { tags: ctx.run.tags });
|
||||||
|
await tags.add("product:1");
|
||||||
|
|
||||||
await wait.for({ seconds: 10 });
|
await wait.for({ seconds: 10 });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { RunTags } from "@trigger.dev/core/v3";
|
||||||
|
import { logger, runs, tags, task, tasks } from "@trigger.dev/sdk/v3";
|
||||||
|
import { simpleChildTask } from "./subtasks";
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
tags: RunTags;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const triggerRunsWithTags = task({
|
||||||
|
id: "trigger-runs-with-tags",
|
||||||
|
run: async (payload: Payload, { ctx }) => {
|
||||||
|
const { id } = await simpleChildTask.trigger(
|
||||||
|
{ message: "trigger from triggerRunsWithTags" },
|
||||||
|
{ tags: payload.tags }
|
||||||
|
);
|
||||||
|
|
||||||
|
await simpleChildTask.triggerAndWait(
|
||||||
|
{ message: "triggerAndWait from triggerRunsWithTags" },
|
||||||
|
{ tags: payload.tags }
|
||||||
|
);
|
||||||
|
|
||||||
|
await simpleChildTask.batchTrigger([
|
||||||
|
{
|
||||||
|
payload: { message: "batchTrigger 1 from triggerRunsWithTags" },
|
||||||
|
options: { tags: payload.tags },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
payload: { message: "batchTrigger 2 from triggerRunsWithTags" },
|
||||||
|
options: { tags: payload.tags },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const results = await simpleChildTask.batchTriggerAndWait([
|
||||||
|
{
|
||||||
|
payload: { message: "batchTriggerAndWait 1 from triggerRunsWithTags" },
|
||||||
|
options: { tags: payload.tags },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
payload: { message: "batchTriggerAndWait 2 from triggerRunsWithTags" },
|
||||||
|
options: { tags: payload.tags },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await tasks.trigger<typeof simpleChildTask>(
|
||||||
|
"simple-child-task",
|
||||||
|
{ message: "tasks.trigger from triggerRunsWithTags" },
|
||||||
|
{ tags: payload.tags }
|
||||||
|
);
|
||||||
|
await tasks.triggerAndWait<typeof simpleChildTask>(
|
||||||
|
"simple-child-task",
|
||||||
|
{ message: "tasks.triggerAndWait from triggerRunsWithTags" },
|
||||||
|
{ tags: payload.tags }
|
||||||
|
);
|
||||||
|
await tasks.batchTrigger<typeof simpleChildTask>("simple-child-task", [
|
||||||
|
{
|
||||||
|
payload: { message: "tasks.batchTrigger 1 from triggerRunsWithTags" },
|
||||||
|
options: { tags: payload.tags },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
payload: { message: "tasks.batchTrigger 2 from triggerRunsWithTags" },
|
||||||
|
options: { tags: payload.tags },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const run = await runs.retrieve(id);
|
||||||
|
logger.log("run", run);
|
||||||
|
logger.log("run usage", {
|
||||||
|
costInCents: run.costInCents,
|
||||||
|
baseCostInCents: run.baseCostInCents,
|
||||||
|
durationMs: run.durationMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result2 = await runs.list({ tag: payload.tags });
|
||||||
|
logger.log("trigger runs ", { length: result2.data.length, data: result2.data });
|
||||||
|
logger.log("run usage", {
|
||||||
|
costInCents: result2.data[0].costInCents,
|
||||||
|
baseCostInCents: result2.data[0].baseCostInCents,
|
||||||
|
durationMs: result2.data[0].durationMs,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user