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";
|
||||
|
||||
type CustomColor = {
|
||||
background: string;
|
||||
foreground: string;
|
||||
};
|
||||
|
||||
export function Spinner({
|
||||
className,
|
||||
color = "blue",
|
||||
}: {
|
||||
className?: string;
|
||||
color?: "blue" | "white" | "muted" | "dark";
|
||||
color?: "blue" | "white" | "muted" | "dark" | CustomColor;
|
||||
}) {
|
||||
const colors = {
|
||||
blue: {
|
||||
light: "rgba(59, 130, 246, 0.4)",
|
||||
dark: "rgba(59, 130, 246)",
|
||||
background: "rgba(59, 130, 246, 0.4)",
|
||||
foreground: "rgba(59, 130, 246)",
|
||||
},
|
||||
white: {
|
||||
light: "rgba(255, 255, 255, 0.4)",
|
||||
dark: "rgba(255, 255, 255)",
|
||||
background: "rgba(255, 255, 255, 0.4)",
|
||||
foreground: "rgba(255, 255, 255)",
|
||||
},
|
||||
muted: {
|
||||
light: "#1C2433",
|
||||
dark: "#3C4B62",
|
||||
background: "#1C2433",
|
||||
foreground: "#3C4B62",
|
||||
},
|
||||
dark: {
|
||||
light: "#15171A",
|
||||
dark: "#272A2E",
|
||||
background: "#15171A",
|
||||
foreground: "#272A2E",
|
||||
},
|
||||
};
|
||||
|
||||
const currentColor = colors[color];
|
||||
const currentColor = typeof color === "string" ? colors[color] : color;
|
||||
|
||||
return (
|
||||
<svg
|
||||
@@ -37,13 +42,33 @@ export function Spinner({
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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
|
||||
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"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</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,
|
||||
CpuChipIcon,
|
||||
InboxStackIcon,
|
||||
TagIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Form } from "@remix-run/react";
|
||||
import { Form, useFetcher } from "@remix-run/react";
|
||||
import type {
|
||||
RuntimeEnvironment,
|
||||
TaskTriggerSource,
|
||||
@@ -15,7 +16,7 @@ import type {
|
||||
} from "@trigger.dev/database";
|
||||
import { ListFilterIcon } from "lucide-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 { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||
@@ -51,6 +52,10 @@ import {
|
||||
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
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);
|
||||
|
||||
@@ -73,6 +78,10 @@ export const TaskRunListSearchFilters = z.object({
|
||||
(value) => (typeof value === "string" ? [value] : value),
|
||||
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()),
|
||||
bulkId: z.string().optional(),
|
||||
from: z.coerce.number().optional(),
|
||||
@@ -104,7 +113,8 @@ export function RunsFilters(props: RunFiltersProps) {
|
||||
searchParams.has("environments") ||
|
||||
searchParams.has("tasks") ||
|
||||
searchParams.has("period") ||
|
||||
searchParams.has("bulkId");
|
||||
searchParams.has("bulkId") ||
|
||||
searchParams.has("tags");
|
||||
|
||||
return (
|
||||
<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: "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: "bulk", title: "Bulk action", icon: <InboxStackIcon className="size-4" /> },
|
||||
] as const;
|
||||
@@ -209,6 +220,7 @@ function AppliedFilters({ possibleEnvironments, possibleTasks, bulkActions }: Ru
|
||||
<AppliedStatusFilter />
|
||||
<AppliedEnvironmentFilter possibleEnvironments={possibleEnvironments} />
|
||||
<AppliedTaskFilter possibleTasks={possibleTasks} />
|
||||
<AppliedTagsFilter />
|
||||
<AppliedPeriodFilter />
|
||||
<AppliedBulkActionsFilter bulkActions={bulkActions} />
|
||||
</>
|
||||
@@ -237,6 +249,8 @@ function Menu(props: MenuProps) {
|
||||
return <CreatedDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "bulk":
|
||||
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 = [
|
||||
{
|
||||
label: "All periods",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
HandRaisedIcon,
|
||||
InformationCircleIcon,
|
||||
Squares2X2Icon,
|
||||
TagIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { AttemptIcon } from "~/assets/icons/AttemptIcon";
|
||||
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")} />;
|
||||
case "trace":
|
||||
return <Squares2X2Icon className={cn(className, "text-text-dimmed")} />;
|
||||
case "tag":
|
||||
return <TagIcon className={cn(className, "text-text-dimmed")} />;
|
||||
//log levels
|
||||
case "debug":
|
||||
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,
|
||||
StopCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { StopIcon } from "@heroicons/react/24/outline";
|
||||
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Checkbox } from "~/components/primitives/Checkbox";
|
||||
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 { useFeatures } from "~/hooks/useFeatures";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
|
||||
import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter";
|
||||
import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder";
|
||||
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
|
||||
import { DateTime } from "../../primitives/DateTime";
|
||||
@@ -31,18 +37,10 @@ import {
|
||||
TableRow,
|
||||
} from "../../primitives/Table";
|
||||
import { CancelRunDialog } from "./CancelRunDialog";
|
||||
import { LiveTimer } from "./LiveTimer";
|
||||
import { ReplayRunDialog } from "./ReplayRunDialog";
|
||||
import { TaskRunStatusCombo } from "./TaskRunStatus";
|
||||
import { LiveTimer } from "./LiveTimer";
|
||||
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";
|
||||
import { RunTag } from "./RunTag";
|
||||
|
||||
type RunsTableProps = {
|
||||
total: number;
|
||||
@@ -173,8 +171,73 @@ export function TaskRunsTable({
|
||||
)}
|
||||
<TableHeaderCell>Test</TableHeaderCell>
|
||||
<TableHeaderCell>Created at</TableHeaderCell>
|
||||
<TableHeaderCell>Delayed until</TableHeaderCell>
|
||||
<TableHeaderCell>TTL</TableHeaderCell>
|
||||
<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>
|
||||
<span className="sr-only">Go to page</span>
|
||||
</TableHeaderCell>
|
||||
@@ -278,6 +341,11 @@ export function TaskRunsTable({
|
||||
{run.delayUntil ? <DateTime date={run.delayUntil} /> : "–"}
|
||||
</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} />
|
||||
</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") {
|
||||
search.set(param, value);
|
||||
search.set(param, encodeURIComponent(value));
|
||||
continue;
|
||||
}
|
||||
|
||||
search.delete(param);
|
||||
for (const v of value) {
|
||||
search.append(param, v);
|
||||
search.append(param, encodeURIComponent(v));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -54,14 +54,20 @@ export function useSearchParams() {
|
||||
|
||||
const value = useCallback(
|
||||
(param: string) => {
|
||||
return search.get(param) ?? undefined;
|
||||
const val = search.get(param) ?? undefined;
|
||||
if (val === undefined) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return decodeURIComponent(val);
|
||||
},
|
||||
[location, search]
|
||||
);
|
||||
|
||||
const values = useCallback(
|
||||
(param: string) => {
|
||||
return search.getAll(param);
|
||||
const all = search.getAll(param);
|
||||
return all.map((v) => decodeURIComponent(v));
|
||||
},
|
||||
[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,
|
||||
schedule: true,
|
||||
tags: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -119,6 +120,10 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
||||
isTest: taskRun.isTest,
|
||||
ttl: taskRun.ttl ?? 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
|
||||
? {
|
||||
id: taskRun.schedule.friendlyId,
|
||||
@@ -127,7 +132,7 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
||||
? taskRun.schedule.deduplicationKey
|
||||
: undefined,
|
||||
generator: {
|
||||
type: "CRON",
|
||||
type: "CRON" as const,
|
||||
expression: taskRun.schedule.generatorExpression,
|
||||
description: taskRun.schedule.generatorDescription,
|
||||
},
|
||||
|
||||
@@ -64,6 +64,12 @@ const SearchParamsSchema = z.object({
|
||||
.transform((value) => {
|
||||
return value ? value.split(",") : undefined;
|
||||
}),
|
||||
"filter[tag]": z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => {
|
||||
return value ? value.split(",") : undefined;
|
||||
}),
|
||||
"filter[bulkAction]": z.string().optional(),
|
||||
"filter[schedule]": z.string().optional(),
|
||||
"filter[isTest]": z
|
||||
@@ -168,6 +174,10 @@ export class ApiRunListPresenter extends BasePresenter {
|
||||
options.versions = $searchParams.data["filter[version]"];
|
||||
}
|
||||
|
||||
if ($searchParams.data["filter[tag]"]) {
|
||||
options.tags = $searchParams.data["filter[tag]"];
|
||||
}
|
||||
|
||||
if ($searchParams.data["filter[bulkAction]"]) {
|
||||
options.bulkId = $searchParams.data["filter[bulkAction]"];
|
||||
}
|
||||
@@ -218,6 +228,10 @@ export class ApiRunListPresenter extends BasePresenter {
|
||||
name: run.environment.slug,
|
||||
user: run.environment.userName,
|
||||
},
|
||||
tags: run.tags,
|
||||
costInCents: run.costInCents,
|
||||
baseCostInCents: run.baseCostInCents,
|
||||
durationMs: run.usageDurationMs,
|
||||
...ApiRetrieveRunPresenter.apiBooleanHelpersFromRunStatus(
|
||||
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 { Direction } from "~/components/runs/RunStatuses";
|
||||
import { type Direction } from "~/components/runs/RunStatuses";
|
||||
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { sqlDatabaseSchema } from "~/db.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { isCancellableRunStatus } from "~/v3/taskStatus";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
|
||||
export type RunListOptions = {
|
||||
userId?: string;
|
||||
@@ -15,6 +15,7 @@ export type RunListOptions = {
|
||||
versions?: string[];
|
||||
statuses?: TaskRunStatus[];
|
||||
environments?: string[];
|
||||
tags?: string[];
|
||||
scheduleId?: string;
|
||||
period?: string;
|
||||
bulkId?: string;
|
||||
@@ -41,6 +42,7 @@ export class RunListPresenter extends BasePresenter {
|
||||
versions,
|
||||
statuses,
|
||||
environments,
|
||||
tags,
|
||||
scheduleId,
|
||||
period,
|
||||
bulkId,
|
||||
@@ -63,6 +65,7 @@ export class RunListPresenter extends BasePresenter {
|
||||
from !== undefined ||
|
||||
to !== undefined ||
|
||||
(scheduleId !== undefined && scheduleId !== "") ||
|
||||
(tags !== undefined && tags.length > 0) ||
|
||||
typeof isTest === "boolean";
|
||||
|
||||
// Find the project scoped to the organization
|
||||
@@ -95,10 +98,10 @@ export class RunListPresenter extends BasePresenter {
|
||||
|
||||
//get all possible tasks
|
||||
const possibleTasksAsync = this._replica.$queryRaw<
|
||||
{
|
||||
slug: string;
|
||||
triggerSource: TaskTriggerSource
|
||||
}[]
|
||||
{
|
||||
slug: string;
|
||||
triggerSource: TaskTriggerSource;
|
||||
}[]
|
||||
>`
|
||||
SELECT DISTINCT(slug), "triggerSource"
|
||||
FROM ${sqlDatabaseSchema}."BackgroundWorkerTask"
|
||||
@@ -150,7 +153,7 @@ export class RunListPresenter extends BasePresenter {
|
||||
const periodMs = period ? parse(period) : undefined;
|
||||
|
||||
//get the runs
|
||||
let runs = await this._replica.$queryRaw<
|
||||
const runs = await this._replica.$queryRaw<
|
||||
{
|
||||
id: string;
|
||||
number: BigInt;
|
||||
@@ -170,7 +173,9 @@ export class RunListPresenter extends BasePresenter {
|
||||
ttl: string | null;
|
||||
expiredAt: Date | null;
|
||||
costInCents: number;
|
||||
baseCostInCents: number;
|
||||
usageDurationMs: BigInt;
|
||||
tags: string[];
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
@@ -191,66 +196,88 @@ export class RunListPresenter extends BasePresenter {
|
||||
tr."idempotencyKey" AS "idempotencyKey",
|
||||
tr."ttl" AS "ttl",
|
||||
tr."expiredAt" AS "expiredAt",
|
||||
tr."baseCostInCents" AS "baseCostInCents",
|
||||
tr."costInCents" AS "costInCents",
|
||||
tr."usageDurationMs" AS "usageDurationMs"
|
||||
FROM
|
||||
tr."usageDurationMs" AS "usageDurationMs",
|
||||
array_remove(array_agg(tag.name), NULL) AS "tags"
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" tr
|
||||
LEFT JOIN
|
||||
LEFT JOIN
|
||||
${sqlDatabaseSchema}."BackgroundWorker" bw ON tr."lockedToVersionId" = bw.id
|
||||
WHERE
|
||||
-- project
|
||||
tr."projectId" = ${project.id}
|
||||
-- cursor
|
||||
${
|
||||
cursor
|
||||
? direction === "forward"
|
||||
? Prisma.sql`AND tr.id < ${cursor}`
|
||||
: Prisma.sql`AND tr.id > ${cursor}`
|
||||
: Prisma.empty
|
||||
}
|
||||
-- filters
|
||||
${
|
||||
restrictToRunIds
|
||||
? restrictToRunIds.length === 0
|
||||
? Prisma.sql`AND tr.id = ''`
|
||||
: Prisma.sql`AND tr.id IN (${Prisma.join(restrictToRunIds)})`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
tasks && tasks.length > 0
|
||||
? Prisma.sql`AND tr."taskIdentifier" IN (${Prisma.join(tasks)})`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
statuses && statuses.length > 0
|
||||
? Prisma.sql`AND tr.status = ANY(ARRAY[${Prisma.join(statuses)}]::"TaskRunStatus"[])`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
environments && environments.length > 0
|
||||
? Prisma.sql`AND tr."runtimeEnvironmentId" IN (${Prisma.join(environments)})`
|
||||
: Prisma.empty
|
||||
}
|
||||
${scheduleId ? Prisma.sql`AND tr."scheduleId" = ${scheduleId}` : Prisma.empty}
|
||||
${typeof isTest === "boolean" ? Prisma.sql`AND tr."isTest" = ${isTest}` : Prisma.empty}
|
||||
${
|
||||
periodMs
|
||||
? Prisma.sql`AND tr."createdAt" >= NOW() - INTERVAL '1 millisecond' * ${periodMs}`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
from
|
||||
? Prisma.sql`AND tr."createdAt" >= ${new Date(from).toISOString()}::timestamp`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
to
|
||||
? Prisma.sql`AND tr."createdAt" <= ${new Date(to).toISOString()}::timestamp`
|
||||
: Prisma.empty
|
||||
}
|
||||
ORDER BY
|
||||
${direction === "forward" ? Prisma.sql`tr.id DESC` : Prisma.sql`tr.id ASC`}
|
||||
LIMIT ${pageSize + 1}`;
|
||||
LEFT JOIN
|
||||
${sqlDatabaseSchema}."_TaskRunToTaskRunTag" trtg ON tr.id = trtg."A"
|
||||
LEFT JOIN
|
||||
${sqlDatabaseSchema}."TaskRunTag" tag ON trtg."B" = tag.id
|
||||
WHERE
|
||||
-- project
|
||||
tr."projectId" = ${project.id}
|
||||
-- cursor
|
||||
${
|
||||
cursor
|
||||
? direction === "forward"
|
||||
? Prisma.sql`AND tr.id < ${cursor}`
|
||||
: Prisma.sql`AND tr.id > ${cursor}`
|
||||
: Prisma.empty
|
||||
}
|
||||
-- filters
|
||||
${
|
||||
restrictToRunIds
|
||||
? restrictToRunIds.length === 0
|
||||
? Prisma.sql`AND tr.id = ''`
|
||||
: Prisma.sql`AND tr.id IN (${Prisma.join(restrictToRunIds)})`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
tasks && tasks.length > 0
|
||||
? Prisma.sql`AND tr."taskIdentifier" IN (${Prisma.join(tasks)})`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
statuses && statuses.length > 0
|
||||
? Prisma.sql`AND tr.status = ANY(ARRAY[${Prisma.join(statuses)}]::"TaskRunStatus"[])`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
environments && environments.length > 0
|
||||
? Prisma.sql`AND tr."runtimeEnvironmentId" IN (${Prisma.join(environments)})`
|
||||
: Prisma.empty
|
||||
}
|
||||
${scheduleId ? Prisma.sql`AND tr."scheduleId" = ${scheduleId}` : Prisma.empty}
|
||||
${typeof isTest === "boolean" ? Prisma.sql`AND tr."isTest" = ${isTest}` : Prisma.empty}
|
||||
${
|
||||
periodMs
|
||||
? Prisma.sql`AND tr."createdAt" >= NOW() - INTERVAL '1 millisecond' * ${periodMs}`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
from
|
||||
? Prisma.sql`AND tr."createdAt" >= ${new Date(from).toISOString()}::timestamp`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
to ? Prisma.sql`AND tr."createdAt" <= ${new Date(to).toISOString()}::timestamp` : Prisma.empty
|
||||
}
|
||||
${
|
||||
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;
|
||||
|
||||
@@ -312,7 +339,9 @@ export class RunListPresenter extends BasePresenter {
|
||||
ttl: run.ttl ? run.ttl : undefined,
|
||||
expiredAt: run.expiredAt ? run.expiredAt.toISOString() : undefined,
|
||||
costInCents: run.costInCents,
|
||||
baseCostInCents: run.baseCostInCents,
|
||||
usageDurationMs: Number(run.usageDurationMs),
|
||||
tags: run.tags.sort((a, b) => a.localeCompare(b)),
|
||||
};
|
||||
}),
|
||||
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"),
|
||||
period: url.searchParams.get("period") ?? 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 } =
|
||||
TaskRunListSearchFilters.parse(s);
|
||||
const {
|
||||
tasks,
|
||||
versions,
|
||||
statuses,
|
||||
environments,
|
||||
tags,
|
||||
period,
|
||||
bulkId,
|
||||
from,
|
||||
to,
|
||||
cursor,
|
||||
direction,
|
||||
} = TaskRunListSearchFilters.parse(s);
|
||||
|
||||
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
|
||||
|
||||
@@ -74,6 +86,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
versions,
|
||||
statuses,
|
||||
environments,
|
||||
tags,
|
||||
period,
|
||||
bulkId,
|
||||
from,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import type { ActionFunction, LoaderFunctionArgs } 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 invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
@@ -17,6 +17,7 @@ import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||
import { ButtonSpinner } from "~/components/primitives/Spinner";
|
||||
import { prisma } from "~/db.server";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
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 (
|
||||
<MainCenteredContainer>
|
||||
<div>
|
||||
@@ -190,8 +194,13 @@ export default function Page() {
|
||||
)}
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Button type="submit" variant={"primary/small"}>
|
||||
Create
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"primary/small"}
|
||||
disabled={isLoading}
|
||||
TrailingIcon={isLoading ? ButtonSpinner : undefined}
|
||||
>
|
||||
{isLoading ? "Creating…" : "Create"}
|
||||
</Button>
|
||||
}
|
||||
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 { findEnvironmentById } from "~/models/runtimeEnvironment.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { OutOfEntitlementError, TriggerTaskService } from "./triggerTask.server";
|
||||
import { getTagsForRunId } from "~/models/taskRunTag.server";
|
||||
|
||||
export class ReplayTaskRunService extends BaseService {
|
||||
public async call(existingTaskRun: TaskRun) {
|
||||
@@ -36,6 +37,11 @@ export class ReplayTaskRunService extends BaseService {
|
||||
});
|
||||
|
||||
try {
|
||||
const tags = await getTagsForRunId({
|
||||
friendlyId: existingTaskRun.id,
|
||||
environmentId: authenticatedEnvironment.id,
|
||||
});
|
||||
|
||||
const triggerTaskService = new TriggerTaskService();
|
||||
return await triggerTaskService.call(
|
||||
existingTaskRun.taskIdentifier,
|
||||
@@ -49,6 +55,7 @@ export class ReplayTaskRunService extends BaseService {
|
||||
concurrencyKey: existingTaskRun.concurrencyKey ?? undefined,
|
||||
test: existingTaskRun.isTest,
|
||||
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 { logger } from "~/services/logger.server";
|
||||
import { isFinalAttemptStatus, isFinalRunStatus } from "../taskStatus";
|
||||
import { createTag } from "~/models/taskRunTag.server";
|
||||
|
||||
export type TriggerTaskServiceOptions = {
|
||||
idempotencyKey?: string;
|
||||
@@ -210,6 +211,22 @@ export class TriggerTaskService extends BaseService {
|
||||
event.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({
|
||||
data: {
|
||||
status: delayUntil ? "DELAYED" : "PENDING",
|
||||
@@ -233,6 +250,12 @@ export class TriggerTaskService extends BaseService {
|
||||
queuedAt: delayUntil ? undefined : new Date(),
|
||||
maxAttempts: body.options?.maxAttempts,
|
||||
ttl,
|
||||
tags:
|
||||
tagIds.length === 0
|
||||
? undefined
|
||||
: {
|
||||
connect: tagIds.map((id) => ({ id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { context, propagation } from "@opentelemetry/api";
|
||||
import { z } from "zod";
|
||||
import { version } from "../../../package.json";
|
||||
import {
|
||||
AddTagsRequestBody,
|
||||
BatchTaskRunExecutionResult,
|
||||
BatchTriggerTaskRequestBody,
|
||||
BatchTriggerTaskResponse,
|
||||
@@ -63,8 +65,8 @@ const DEFAULT_ZOD_FETCH_OPTIONS: ZodFetchOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
export type { ApiRequestOptions };
|
||||
export { isRequestOptions };
|
||||
export type { ApiRequestOptions };
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return zodfetch(
|
||||
ScheduleObject,
|
||||
@@ -534,6 +549,13 @@ function createSearchQueryForListRuns(query?: ListRunsQueryParams): URLSearchPar
|
||||
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) {
|
||||
searchParams.append("filter[schedule]", query.schedule);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ListRunsQueryParams extends CursorPageParams {
|
||||
to?: Date | number;
|
||||
period?: string;
|
||||
bulkAction?: string;
|
||||
tag?: Array<string> | string;
|
||||
schedule?: string;
|
||||
isTest?: boolean;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,17 @@ export const CreateBackgroundWorkerResponse = z.object({
|
||||
|
||||
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({
|
||||
payload: z.any(),
|
||||
context: z.any(),
|
||||
@@ -70,6 +81,7 @@ export const TriggerTaskRequestBody = z.object({
|
||||
payloadType: z.string().optional(),
|
||||
delay: z.string().or(z.coerce.date()).optional(),
|
||||
ttl: z.string().or(z.number().nonnegative().int()).optional(),
|
||||
tags: RunTags.optional(),
|
||||
maxAttempts: z.number().int().optional(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -110,6 +122,12 @@ export const GetBatchResponseBody = z.object({
|
||||
|
||||
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({
|
||||
delay: z.string().or(z.coerce.date()),
|
||||
});
|
||||
@@ -452,6 +470,10 @@ const CommonRunFields = {
|
||||
delayedUntil: z.coerce.date().optional(),
|
||||
ttl: z.string().optional(),
|
||||
expiredAt: z.coerce.date().optional(),
|
||||
tags: z.string().array(),
|
||||
costInCents: z.number(),
|
||||
baseCostInCents: z.number(),
|
||||
durationMs: z.number(),
|
||||
};
|
||||
|
||||
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[]
|
||||
backgroundWorkerTasks BackgroundWorkerTask[]
|
||||
taskRuns TaskRun[]
|
||||
taskTags TaskTag[]
|
||||
runTags TaskRunTag[]
|
||||
taskQueues TaskQueue[]
|
||||
environmentVariables EnvironmentVariable[]
|
||||
checkpoints Checkpoint[]
|
||||
@@ -1641,8 +1641,9 @@ model TaskRun {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
attempts TaskRunAttempt[] @relation("attempts")
|
||||
tags TaskTag[]
|
||||
attempts TaskRunAttempt[] @relation("attempts")
|
||||
tags TaskRunTag[]
|
||||
|
||||
checkpoints Checkpoint[]
|
||||
|
||||
startedAt DateTime?
|
||||
@@ -1735,6 +1736,24 @@ enum TaskRunStatus {
|
||||
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 {
|
||||
id String @id @default(cuid())
|
||||
|
||||
@@ -1775,22 +1794,6 @@ model TaskRunNumberCounter {
|
||||
@@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 {
|
||||
id String @id @default(cuid())
|
||||
number Int @default(0)
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from "./tasks";
|
||||
export * from "./wait";
|
||||
export * from "./usage";
|
||||
export * from "./idempotencyKeys";
|
||||
export * from "./tags";
|
||||
export type { Context };
|
||||
|
||||
import type { Context } from "./shared";
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
QueueOptions,
|
||||
RetryOptions,
|
||||
RunFnParams,
|
||||
RunTags,
|
||||
SemanticInternalAttributes,
|
||||
StartFnParams,
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
@@ -485,6 +501,7 @@ export function createTask<
|
||||
idempotencyKey: await makeKey(options?.idempotencyKey),
|
||||
delay: options?.delay,
|
||||
ttl: options?.ttl,
|
||||
tags: options?.tags,
|
||||
maxAttempts: options?.maxAttempts,
|
||||
},
|
||||
},
|
||||
@@ -547,6 +564,7 @@ export function createTask<
|
||||
idempotencyKey: await makeKey(item.options?.idempotencyKey),
|
||||
delay: item.options?.delay,
|
||||
ttl: item.options?.ttl,
|
||||
tags: item.options?.tags,
|
||||
maxAttempts: item.options?.maxAttempts,
|
||||
},
|
||||
};
|
||||
@@ -616,6 +634,7 @@ export function createTask<
|
||||
idempotencyKey: await makeKey(options?.idempotencyKey),
|
||||
delay: options?.delay,
|
||||
ttl: options?.ttl,
|
||||
tags: options?.tags,
|
||||
maxAttempts: options?.maxAttempts,
|
||||
},
|
||||
});
|
||||
@@ -701,6 +720,7 @@ export function createTask<
|
||||
idempotencyKey: await makeKey(item.options?.idempotencyKey),
|
||||
delay: item.options?.delay,
|
||||
ttl: item.options?.ttl,
|
||||
tags: item.options?.tags,
|
||||
maxAttempts: item.options?.maxAttempts,
|
||||
},
|
||||
};
|
||||
@@ -866,6 +886,7 @@ export async function trigger<TTask extends AnyTask>(
|
||||
idempotencyKey: await makeKey(options?.idempotencyKey),
|
||||
delay: options?.delay,
|
||||
ttl: options?.ttl,
|
||||
tags: options?.tags,
|
||||
maxAttempts: options?.maxAttempts,
|
||||
},
|
||||
},
|
||||
@@ -942,6 +963,7 @@ export async function triggerAndWait<TTask extends AnyTask>(
|
||||
idempotencyKey: await makeKey(options?.idempotencyKey),
|
||||
delay: options?.delay,
|
||||
ttl: options?.ttl,
|
||||
tags: options?.tags,
|
||||
maxAttempts: options?.maxAttempts,
|
||||
},
|
||||
},
|
||||
@@ -1051,6 +1073,7 @@ export async function batchTrigger<TTask extends AnyTask>(
|
||||
idempotencyKey: await makeKey(item.options?.idempotencyKey),
|
||||
delay: item.options?.delay,
|
||||
ttl: item.options?.ttl,
|
||||
tags: item.options?.tags,
|
||||
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";
|
||||
|
||||
export const simpleParentTask = task({
|
||||
@@ -27,6 +27,9 @@ export const simpleChildTask = task({
|
||||
run: async (payload: { message: string }, { 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 });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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