Compare commits

...

36 Commits

Author SHA1 Message Date
nicktrn cfe4b5409a Fix lockfile 2024-05-08 17:27:34 +01:00
github-actions[bot] 223768a025 chore: Update version for release (beta) (#1091)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-08 17:25:22 +01:00
Ramadan Omar 1fd26ff98b Improvement: Added more context to Shopify webhook registering errors (#1090)
* Improvement: Added more context to Shopify webhook registering errors

Co-authored-by: Ahmed Ramadan <ahmedramadan1337@gmail.com>

* revert: shopify reference jobs

* fix: added error handling to some shopify client edge cases

* small changes to crud error handling

---------

Co-authored-by: Ahmed Ramadan <ahmedramadan1337@gmail.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
2024-05-08 17:22:59 +01:00
Eric Allam 8d34c63c2d Fix issue when concurrency limit is set to 0 on a queue 2024-05-08 10:19:06 +01:00
Zafer Cesur ff04c986a1 Add notice on sideEffects in deployment guide (#680)
Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-05-07 22:58:27 +01:00
Matt Aitken 7d6430dc46 New filtered dropdown and run filters (#1089)
* Extract out useFilterTasks

* WIP on Combobox

* Revert "WIP on Combobox"

This reverts commit f0a9b2e57c67f24d5e0876630e17a54252ceb86b.

* Added ariakit

* Started adding a Listbox component

* Listbox WIP

* More WIP on the list box

* More work on the Listbox

* WIP on filterable Listbox using headless… but it’s going to be a nightmare to make it good. Wrong approach

* WIP using Ariakit

* Multiple selection working

* WIP in reworking it so search is built in and it’s easier to use

* Nicer API for listbox

* Allow sections to be rendered with filtering

* More progress on the styling of the menu

* The empty state

* Removed focus outline

* Allow filtering based on the section title

* Render the shortcut correctly

* Allow passing static children to Select and render a separator if a section has no title

* Static example and added subtle focus

* Allow static data with no items array passed in

* Allow shortcuts on select items

* Only how heading if true

* Tooltip for keyboard shortcut

* useShortcutKeys fixes

- Can be passed an undefined definition (so we render the same number of hooks always).
- Fix for enabledOnInputElements not being used…

* Shortcut keys working with lists

* Don’t render shortcut numbers higher than 0

* More improvements

* WIP on a Link row

* Nicer API for the children function

* Much nicer shortcut key support

* Separated SelectTrigger out

* Heading separated out

* More styled elements created

* Removed unused element

* Reordered statuses

* Added search context

* Fixes for default values

* Removed tab elements

* Combo box

* Simplified it more

* Started work on Filter menu

* Experiments with filter menu

* Filter menu proof of concept working

* Nice hook for using a search param

* The escape key goes back to the previous menu

* Added filter page to storybook

* Added other variants

* Renamed Select file to OldSelect, implemented Feedback form

* Added placeholder support

* Added an optional dropdown icon (defaults to a chevron down)

* Feedback new Select

* Feedback just use static items

* New project page

* Renamed storybook pages

* Lots of progress on the new run filters

* Added new tasks filters

* Show the applied filters

* Added period filtering

* Fix for escape resetting the period value to the previous value when escape is pressed

* Show the Clear button sooner

* Removed unused code

* Reworked useSearchParams so multiple values can be set at once…

* Renamed file: OldSelect -> SImpleSelect

* Fix for new Select not working in forms because props weren’t being passed through

* New Select on the schedules page

* Better layout when there are lots of run filters

* Many style improvements to run filters

* Status filter allows you to select as well

* Rolled out the menus for the other files

* Minor improvements to schedule page and page padding

* Latest lockfile
2024-05-07 22:55:31 +01:00
Dan b289aa31da Readme updates - removed old roadmap and added dev preview EA link (#1078)
* Removed outdated roadmap

* Added link to the dev preview article
2024-05-07 22:55:00 +01:00
Eric Allam 0508de6258 Release 3.0.0-beta.28 2024-05-06 12:35:27 +01:00
github-actions[bot] 56a063e586 chore: Update version for release (beta) (#1087)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-06 12:34:43 +01:00
Eric Allam d490bc5cbf Add the "log" level back in as an alias to "info" 2024-05-06 12:28:17 +01:00
Eric Allam 6d9dfbc75d Add configure function to be able to configure the SDK manually
And move the ApiClient configuration away from AsyncLocalStorage and use our globals system instead
2024-05-06 12:12:30 +01:00
Eric Allam 6406924b02 Ensure @trigger.dev/sdk and @trigger.dev/core are always in the list of deployed dependencies 2024-05-06 11:01:49 +01:00
Eric Allam 2156e15266 Adding some additional telemetry during deploy to help debug issues 2024-05-06 10:34:54 +01:00
Eric Allam 49b4d47726 Release 3.0.0-beta.27 packages 2024-05-05 08:20:07 +01:00
github-actions[bot] 2e4f5d893c chore: Update version for release (beta) (#1086)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-05 08:19:13 +01:00
Eric Allam 203e002087 Add runs.retrieve management API method to get info about a run by run ID 2024-05-04 22:24:42 +01:00
Eric Allam 1670c4c41d Remove "log" Log Level, unify log and info messages under the "info" log level 2024-05-03 14:55:24 +01:00
Eric Allam 9d6c424236 Add deploy admin debug tooltips 2024-05-03 11:22:51 +01:00
nicktrn cffc88aeac Increase indexing specs 2024-05-03 09:45:18 +01:00
Eric Allam ae13757703 Release 3.0.0-beta.26 (fix server-only imports) 2024-05-03 09:40:32 +01:00
github-actions[bot] 0ee39e6101 chore: Update version for release (beta) (#1085)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-03 09:39:34 +01:00
nicktrn bce398b892 increase indexing specs 2024-05-03 09:36:16 +01:00
Eric Allam e667028d4a Strip out server-only package from worker builds 2024-05-02 21:28:41 +01:00
Eric Allam ae99807047 Allow admin tooltips while impersonating 2024-05-02 16:39:01 +01:00
Eric Allam bf4e505c70 Release 3.0.0-beta.25 2024-05-02 16:00:43 +01:00
github-actions[bot] c8538c236c chore: Update version for release (beta) (#1080)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-02 15:58:42 +01:00
Eric Allam ff6e2461d5 Add admin tooltips to make support/debugging easier for admin users 2024-05-02 14:12:16 +01:00
Eric Allam 3ed016bc2b Fixed SharedQueueConsumer internal otel logging 2024-05-02 12:31:55 +01:00
Eric Allam 68dd3804a7 Improve the fair queue selection strategy by paginating through all the queues in the parent queue 2024-05-02 11:56:35 +01:00
Eric Allam 95f0c7b780 increase queue selection count to 36 2024-05-01 22:41:50 +01:00
Eric Allam 339c39d739 Fix type issues and unit tests 2024-05-01 22:23:38 +01:00
Eric Allam bc0ca01319 Fix fair queue priority strategy off-by one error 2024-05-01 22:13:52 +01:00
Eric Allam c37c822314 Use locked package versions when resolving dependencies in deployed workers 2024-05-01 15:51:39 +01:00
Eric Allam 2dfe191c55 Fixed some docs that were missing imports 2024-05-01 14:47:50 +01:00
Eric Allam e337b21650 Add a postInstall option to allow running scripts after dependencies have been installed in deployed images 2024-05-01 14:16:52 +01:00
Eric Allam 9e5382951b Improve the display of non-object return types in the run trace viewer 2024-05-01 13:52:05 +01:00
177 changed files with 5671 additions and 1160 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/shopify": patch
---
improved error messages when a shopify webhook fails to register
+6
View File
@@ -0,0 +1,6 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---
Remove "log" Log Level, unify log and info messages under the "info" log level
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Strip out server-only package from worker builds
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
Add runs.retrieve management API method to get info about a run by run ID
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Adding some additional telemetry during deploy to help debug issues
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Add the "log" level back in as an alias to "info"
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
Add configure function to be able to configure the SDK manually
+11
View File
@@ -47,18 +47,22 @@
"angry-eagles-trade",
"beige-pens-dance",
"big-tomatoes-deliver",
"blue-pumas-whisper",
"breezy-gorillas-mate",
"chilled-hornets-move",
"clean-pianos-listen",
"clever-apes-collect",
"clever-carrots-travel",
"clever-donkeys-hunt",
"cool-glasses-bake",
"cuddly-feet-approve",
"dry-walls-check",
"dull-mangos-press",
"eight-pumas-float",
"eleven-paws-join",
"few-students-share",
"funny-swans-destroy",
"gorgeous-gorillas-compete",
"green-bags-wink",
"khaki-apricots-design",
"khaki-poems-lay",
@@ -68,7 +72,9 @@
"light-bulldogs-press",
"light-dragons-complain",
"little-crabs-cross",
"long-fireants-search",
"loud-actors-remember",
"lovely-drinks-flash",
"many-ligers-pump",
"mighty-camels-joke",
"mighty-flowers-train",
@@ -76,10 +82,12 @@
"new-rivers-tell",
"ninety-pets-travel",
"odd-poets-own",
"pink-pumas-rhyme",
"polite-ducks-switch",
"polite-rockets-matter",
"poor-flowers-cross",
"purple-garlics-shop",
"rare-lamps-promise",
"rare-roses-float",
"real-planets-stare",
"rich-kangaroos-unite",
@@ -91,6 +99,7 @@
"sharp-zebras-serve",
"shiny-coats-cry",
"silly-suits-switch",
"six-ligers-exist",
"slow-buses-own",
"smart-needles-move",
"smart-olives-eat",
@@ -102,7 +111,9 @@
"swift-dragons-peel",
"tall-bees-wave",
"tame-guests-know",
"tender-moose-tell",
"tender-oranges-rhyme",
"thin-parents-heal",
"tidy-balloons-suffer",
"tidy-dryers-sleep",
"tiny-doors-type",
+6
View File
@@ -0,0 +1,6 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---
Add a postInstall option to allow running scripts after dependencies have been installed in deployed images
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Improve the display of non-object return types in the run trace viewer
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Use locked package versions when resolving dependencies in deployed workers
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Ensure @trigger.dev/sdk and @trigger.dev/core are always in the list of deployed dependencies
+4 -26
View File
@@ -5,7 +5,7 @@
<img alt="Trigger.dev logo" src="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/a45d1fa2-0ae8-4a39-4409-f4f934bfae00/public">
</picture>
### The open source background jobs framework
### The open source background jobs platform
[Discord](https://trigger.dev/discord) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
@@ -14,7 +14,9 @@
</div>
# About Trigger.dev
> The Trigger.dev v3 developer preview is now open. For more information and to get early access, check out our [developer preview launch post](https://trigger.dev/blog/v3-developer-preview-launch/).
## About Trigger.dev
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
@@ -48,30 +50,6 @@ View every Task in every Run so you can tell exactly what happened.
Easily integrate with hundreds of third-party APIs including your own. Use API keys (which never leave your server) or let us handle OAuth for you. Install our integration packages and easily subscribe to webhooks and perform common tasks, or you can easily use your existing favorite Node.JS SDKs and get resumability and idempotency through our `runTask` function.
## Our progress
Were building the most comprehensive and easy-to-use background jobs framework for developers.
Click the links to join the discussions about our upcoming features.
| Feature | What it does | Status |
| ------------------------------------------------------------------------------------ | --------------------------------------------------- | ------ |
| Integration kit | Official Trigger.dev integrations or build your own | ✅ |
| Self-hosting | Host the platform yourself | ✅ |
| Cloud | Just write code, no deployment required | ✅ |
| Dashboard | View every Task in every Run | ✅ |
| Serverless | Long-running Jobs on your serverless backend | ✅ |
| React hooks | Easily update your UI with Job progress | ✅ |
| React frameworks | Support for Remix, Astro, RedwoodJS & more | ✅ |
| [Background tasks](https://github.com/triggerdotdev/trigger.dev/discussions/400) | Offload long or intense Tasks to our infrastructure | 🛠️ |
| [Long-running servers](https://github.com/triggerdotdev/trigger.dev/discussions/430) | Run Jobs on your long-running backend | 🛠️ |
| Polling Triggers | Subscribe to changes without webhooks | 🕝 |
| Vercel integration | Easy deploy and preview environment support | 🕝 |
| Streaming | Receive data from your Jobs in realtime | 🕝 |
| 100+ integrations | Comprehensive support for popular APIs | 🕝 |
| [Trigger.dev Connect](https://github.com/triggerdotdev/trigger.dev/discussions/441) | Use integrations signed in as your users | 🕝 |
| File IO | Create Tasks that have file outputs | 🕝 |
# Getting started
Visit our docs [here](https://trigger.dev/docs).
+2 -2
View File
@@ -80,8 +80,8 @@ class KubernetesTaskOperations implements TaskOperations {
],
resources: {
limits: {
cpu: "250m",
memory: "0.5G",
cpu: "1",
memory: "1G",
"ephemeral-storage": "2Gi",
},
},
+19 -26
View File
@@ -1,10 +1,14 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { BookOpenIcon } from "@heroicons/react/20/solid";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
import { DiscordIcon, GitHubLightIcon } from "@trigger.dev/companyicons";
import { ActivityIcon } from "lucide-react";
import { ReactNode, useState } from "react";
import { FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
import { cn } from "~/utils/cn";
import { docsPath } from "~/utils/pathBuilder";
import { Button, LinkButton } from "./primitives/Buttons";
import { Fieldset } from "./primitives/Fieldset";
import { FormButtons } from "./primitives/FormButtons";
@@ -13,20 +17,9 @@ import { Header1, Header2 } from "./primitives/Headers";
import { InputGroup } from "./primitives/InputGroup";
import { Label } from "./primitives/Label";
import { Paragraph } from "./primitives/Paragraph";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "./primitives/Select";
import { Select, SelectItem } from "./primitives/Select";
import { Sheet, SheetBody, SheetContent, SheetTrigger } from "./primitives/Sheet";
import { TextArea } from "./primitives/TextArea";
import { cn } from "~/utils/cn";
import { BookOpenIcon } from "@heroicons/react/20/solid";
import { ActivityIcon, HeartPulseIcon } from "lucide-react";
import { docsPath } from "~/utils/pathBuilder";
type FeedbackProps = {
button: ReactNode;
@@ -78,20 +71,20 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
<Fieldset className="max-w-full gap-y-3">
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
<InputGroup className="max-w-full">
<SelectGroup>
<Select {...conform.input(feedbackType)} defaultValue={defaultValue}>
<SelectTrigger size="medium" width="full">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
{Object.entries(feedbackTypeLabel).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
<Select
{...conform.select(feedbackType)}
variant="tertiary/medium"
defaultValue={defaultValue}
placeholder="Select type"
text={(value) => feedbackTypeLabel[value]}
dropdownIcon
>
{Object.entries(feedbackTypeLabel).map(([name, title]) => (
<SelectItem key={name} value={name}>
{title}
</SelectItem>
))}
</Select>
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
</InputGroup>
<InputGroup className="max-w-full">
@@ -0,0 +1,29 @@
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/primitives/Tooltip";
import { useIsImpersonating } from "~/hooks/useOrganizations";
import { useHasAdminAccess } from "~/hooks/useUser";
export function AdminDebugTooltip({ children }: { children: React.ReactNode }) {
const hasAdminAccess = useHasAdminAccess();
const isImpersonating = useIsImpersonating();
if (!hasAdminAccess && !isImpersonating) {
return null;
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<ShieldCheckIcon className="h-5 w-5" />
</TooltipTrigger>
<TooltipContent className="flex items-center gap-1">{children}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
@@ -9,7 +9,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "../primitives/Select";
} from "../primitives/SimpleSelect";
import { EventListSearchSchema } from "./EventStatuses";
import { environmentKeys, FilterableEnvironment } from "~/components/runs/RunStatuses";
import { TimeFrameFilter } from "../runs/TimeFrameFilter";
@@ -41,7 +41,7 @@ export function PageBody({
<div
className={cn(
scrollable
? "overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
? "overflow-y-auto p-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
: "overflow-hidden",
className
)}
@@ -0,0 +1,56 @@
import { XMarkIcon } from "@heroicons/react/20/solid";
import { ReactNode } from "react";
import { cn } from "~/utils/cn";
const variants = {
"tertiary/small": {
box: "h-6 bg-tertiary rounded pl-1.5 gap-1.5 text-xs divide-x divide-black/15 group-hover:bg-charcoal-600",
clear: "size-6 text-text-dimmed hover:text-text-bright transition-colors",
},
"minimal/small": {
box: "h-6 hover:bg-tertiary rounded pl-1.5 gap-1.5 text-xs",
clear: "size-6 text-text-dimmed hover:text-text-bright transition-colors",
},
};
type Variant = keyof typeof variants;
type AppliedFilterProps = {
label: ReactNode;
value: ReactNode;
removable?: boolean;
onRemove?: () => void;
variant?: Variant;
className?: string;
};
export function AppliedFilter({
label,
value,
removable = true,
onRemove,
variant = "tertiary/small",
className,
}: AppliedFilterProps) {
const variantClassName = variants[variant];
return (
<div className={cn("flex items-center transition", variantClassName.box, className)}>
<div className="flex items-center gap-0.5">
<div className="text-text-dimmed">
<span>{label}</span>:
</div>
<div className="text-text-bright">
<div>{value}</div>
</div>
</div>
{removable && (
<button
className={cn("group flex size-6 items-center justify-center", variantClassName.clear)}
onClick={onRemove}
>
<XMarkIcon className="size-3.5" />
</button>
)}
</div>
);
}
+601 -120
View File
@@ -1,143 +1,624 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import * as Ariakit from "@ariakit/react";
import { SelectProps as AriaSelectProps } from "@ariakit/react";
import { SelectValue } from "@ariakit/react-core/select/select-value";
import { Link } from "@remix-run/react";
import * as React from "react";
import { Fragment, useMemo, useState } from "react";
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import { ShortcutKey } from "./ShortcutKey";
import { ChevronDown } from "lucide-react";
const sizes = {
"secondary/small":
"text-xs h-6 bg-tertiary border border-tertiary group-hover:text-text-bright hover:border-charcoal-600 pr-2 pl-1.5",
medium: "text-sm h-8 bg-tertiary border border-tertiary hover:border-charcoal-600 px-2.5",
minimal: "text-xs h-6 bg-transparent hover:bg-tertiary pl-1.5 pr-2",
small: {
button: "h-6 rounded text-xs px-2 ",
},
medium: {
button: "h-8 rounded text-xs px-3 text-sm",
},
};
export type SelectProps = {
size?: keyof typeof sizes;
width?: "content" | "full";
const style = {
tertiary: {
button:
"bg-tertiary focus-within:ring-charcoal-500 border border-tertiary hover:text-text-bright hover:border-charcoal-600",
},
minimal: {
button:
"bg-transparent focus-within:ring-charcoal-500 hover:bg-tertiary disabled:bg-transparent disabled:pointer-events-none",
},
};
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & SelectProps
>(({ className, children, width = "content", size = "secondary/small", ...props }, ref) => {
const sizeClassName = sizes[size];
const variants = {
"tertiary/small": {
button: cn(sizes.small.button, style.tertiary.button),
},
"tertiary/medium": {
button: cn(sizes.medium.button, style.tertiary.button),
},
"minimal/small": {
button: cn(sizes.small.button, style.minimal.button),
},
"minimal/medium": {
button: cn(sizes.medium.button, style.minimal.button),
},
};
type Variant = keyof typeof variants;
type Section<TItem> = {
type: "section";
title?: string;
items: TItem[];
};
function isSection<TItem>(data: TItem[] | Section<TItem>[]): data is Section<TItem>[] {
const firstItem = data[0];
return (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"ring-offset-background focus-visible:ring-ring group flex items-center justify-between gap-x-1 rounded text-text-dimmed transition placeholder:text-text-dimmed hover:text-text-bright focus-visible:bg-tertiary focus-visible:text-text-bright focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
width === "full" ? "w-full" : "w-min",
sizeClassName,
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown
className={cn(
"size-4 text-text-dimmed transition group-hover:text-text-bright group-focus:text-text-bright"
)}
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
(firstItem as Section<TItem>).type === "section" &&
(firstItem as Section<TItem>).items !== undefined &&
Array.isArray((firstItem as Section<TItem>).items)
);
});
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
}
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-max overflow-hidden rounded-md border border-charcoal-700 bg-background-dimmed text-text-bright shadow-md animate-in fade-in-40",
position === "popper" && "translate-y-1",
className
)}
position={position}
{...props}
type ItemFromSection<TItemOrSection> = TItemOrSection extends Section<infer U> ? U : TItemOrSection;
export interface SelectProps<TValue extends string | string[], TItem>
extends Omit<Ariakit.SelectProps, "children"> {
icon?: React.ReactNode;
text?: React.ReactNode | ((value: TValue) => React.ReactNode);
placeholder?: React.ReactNode;
value?: Ariakit.SelectProviderProps<TValue>["value"];
setValue?: Ariakit.SelectProviderProps<TValue>["setValue"];
defaultValue?: Ariakit.SelectProviderProps<TValue>["defaultValue"];
label?: string | Ariakit.SelectLabelProps["render"];
heading?: string;
showHeading?: boolean;
items?: TItem[] | Section<TItem>[];
empty?: React.ReactNode;
filter?: (item: ItemFromSection<TItem>, search: string, title?: string) => boolean;
children:
| React.ReactNode
| ((
items: ItemFromSection<TItem>[],
meta: {
shortcutsEnabled?: boolean;
section?: {
title?: string;
startIndex: number;
count: number;
};
}
) => React.ReactNode);
variant?: Variant;
open?: boolean;
setOpen?: (open: boolean) => void;
shortcut?: ShortcutDefinition;
allowItemShortcuts?: boolean;
clearSearchOnSelection?: boolean;
dropdownIcon?: boolean | React.ReactNode;
}
export function Select<TValue extends string | string[], TItem>({
children,
icon,
text,
placeholder,
value,
setValue,
defaultValue,
label,
heading,
showHeading = false,
items,
filter,
empty = null,
variant = "tertiary/small",
open,
setOpen,
shortcut,
allowItemShortcuts = true,
disabled,
clearSearchOnSelection = true,
dropdownIcon,
...props
}: SelectProps<TValue, TItem>) {
const [searchValue, setSearchValue] = useState("");
const searchable = items !== undefined && filter !== undefined;
const matches = useMemo(() => {
if (!items) return [];
if (!searchValue || !filter) return items;
if (isSection(items)) {
return items
.map((section) => ({
...section,
items: section.items.filter((item) =>
filter(item as ItemFromSection<TItem>, searchValue, section.title)
),
}))
.filter((section) => section.items.length > 0);
}
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
}, [searchValue, items]);
const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length;
const select = (
<SelectProvider
open={open}
setOpen={setOpen}
virtualFocus={searchable}
value={value}
setValue={(v) => {
if (clearSearchOnSelection) {
setSearchValue("");
}
if (setValue) {
setValue(v as any);
}
}}
defaultValue={defaultValue}
>
<SelectPrimitive.Viewport
className={cn(
"space-y-0.5 px-1 py-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
{label && <SelectLabel render={typeof label === "string" ? <div>{label}</div> : label} />}
<SelectTrigger
icon={icon}
variant={variant}
text={text}
placeholder={placeholder}
shortcut={shortcut}
tooltipTitle={heading}
disabled={disabled}
dropdownIcon={dropdownIcon}
{...props}
/>
<SelectPopover>
{!searchable && showHeading && heading && <SelectHeading render={<>{heading}</>} />}
{searchable && <ComboBox placeholder={heading} shortcut={shortcut} value={searchValue} />}
<SelectList>
{typeof children === "function" ? (
matches.length > 0 ? (
isSection(matches) ? (
<SelectGroupedRenderer
items={matches}
children={children}
enableItemShortcuts={enableItemShortcuts}
/>
) : (
children(matches as ItemFromSection<TItem>[], {
shortcutsEnabled: enableItemShortcuts,
})
)
) : (
empty
)
) : (
children
)}
</SelectList>
</SelectPopover>
</SelectProvider>
);
if (searchable) {
return (
<ComboboxProvider
resetValueOnHide
setValue={(value) => {
React.startTransition(() => {
setSearchValue(value);
});
}}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
{select}
</ComboboxProvider>
);
}
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn(
"-ml-1 -mr-1 mb-1 bg-charcoal-900 py-1.5 pl-2 pr-2 font-sans text-xxs font-normal uppercase leading-normal tracking-wider text-text-dimmed first-of-type:-mt-0",
className
)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
return select;
}
type SelectItemProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
contentClassName?: string;
};
export interface SelectTriggerProps<TValue = any> extends AriaSelectProps {
icon?: React.ReactNode;
text?: React.ReactNode | ((value: TValue) => React.ReactNode);
placeholder?: React.ReactNode;
variant?: Variant;
shortcut?: ShortcutDefinition;
tooltipTitle?: string;
dropdownIcon?: boolean | React.ReactNode;
}
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, SelectItemProps>(
({ className, children, contentClassName, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
export function SelectTrigger({
icon,
variant = "tertiary/small",
text,
shortcut,
tooltipTitle,
disabled,
placeholder,
dropdownIcon = false,
children,
className,
...props
}: SelectTriggerProps) {
const ref = React.useRef<HTMLButtonElement>(null);
useShortcutKeys({
shortcut: shortcut,
action: (e) => {
e.preventDefault();
e.stopPropagation();
if (ref.current) {
ref.current.click();
}
},
disabled,
});
const showTooltip = tooltipTitle || shortcut;
const variantClasses = variants[variant];
let content: React.ReactNode = "";
if (children) {
content = children;
} else if (text !== undefined) {
if (typeof text === "function") {
content = <SelectValue>{(value) => <>{text(value) ?? placeholder}</>}</SelectValue>;
} else {
content = text;
}
} else {
content = (
<SelectValue>
{(value) => (
<>
{typeof value === "string"
? value ?? placeholder
: value.length === 0
? placeholder
: value.join(", ")}
</>
)}
</SelectValue>
);
}
return (
<Ariakit.TooltipProvider timeout={200}>
<Ariakit.TooltipAnchor
className="button"
render={
<Ariakit.Select
className={cn(
"group flex items-center gap-1 outline-offset-0 focus-within:outline-none focus-within:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
variantClasses.button,
className
)}
ref={ref}
{...props}
/>
}
>
<div className="flex grow items-center gap-0.5">
{icon && <div className="-ml-1 flex-none">{icon}</div>}
<div className="truncate">{content}</div>
</div>
{dropdownIcon === true ? (
<ChevronDown
className={cn(
"size-4 flex-none text-text-dimmed transition group-hover:text-text-bright group-focus:text-text-bright"
)}
/>
) : !dropdownIcon ? null : (
dropdownIcon
)}
</Ariakit.TooltipAnchor>
{showTooltip && (
<Ariakit.Tooltip
disabled={shortcut === undefined}
className="z-40 cursor-default rounded border border-charcoal-700 bg-background-bright px-2 py-1.5 text-xs"
>
<div className="flex items-center gap-2">
<span>{tooltipTitle ?? "Open menu"}</span>
{shortcut && (
<ShortcutKey
className={cn("size-4 flex-none")}
shortcut={shortcut}
variant={"small"}
/>
)}
</div>
</Ariakit.Tooltip>
)}
</Ariakit.TooltipProvider>
);
}
export interface SelectProviderProps<TValue extends string | string[]>
extends Ariakit.SelectProviderProps<TValue> {}
export function SelectProvider<TValue extends string | string[]>(
props: SelectProviderProps<TValue>
) {
return <Ariakit.SelectProvider {...props} />;
}
export interface ComboboxProviderProps extends Ariakit.ComboboxProviderProps {}
export function ComboboxProvider(props: ComboboxProviderProps) {
return <Ariakit.ComboboxProvider {...props} />;
}
function SelectGroupedRenderer<TItem>({
items,
children,
enableItemShortcuts,
}: {
items: Section<TItem>[];
children: (
items: ItemFromSection<TItem>[],
meta: {
shortcutsEnabled?: boolean;
section?: { title?: string; startIndex: number; count: number };
}
) => React.ReactNode;
enableItemShortcuts: boolean;
}) {
let count = 0;
return (
<>
{items.map((section, index) => {
const previousItem = items.at(index - 1);
count += previousItem ? previousItem.items.length : 0;
return (
<Fragment key={index}>
{children(section.items as ItemFromSection<TItem>[], {
shortcutsEnabled: enableItemShortcuts,
section: {
title: section.title,
startIndex: count - 1,
count: section.items.length,
},
})}
</Fragment>
);
})}
</>
);
}
export interface SelectListProps extends Omit<Ariakit.SelectListProps, "store"> {}
export function SelectList(props: SelectListProps) {
const combobox = Ariakit.useComboboxContext();
const Component = combobox ? Ariakit.ComboboxList : Ariakit.SelectList;
return (
<Component
{...props}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-charcoal-750 focus:bg-charcoal-750/50",
"overflow-y-auto overscroll-contain outline-none scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
props.className
)}
/>
);
}
export interface SelectItemProps extends Ariakit.SelectItemProps {
icon?: React.ReactNode;
checkIcon?: React.ReactNode;
shortcut?: ShortcutDefinition;
}
const selectItemClasses =
"group cursor-pointer px-1 pt-1 text-xs text-text-dimmed outline-none last:pb-1";
export function SelectItem({
icon,
checkIcon = <Ariakit.SelectItemCheck className="size-8 flex-none text-white" />,
shortcut,
...props
}: SelectItemProps) {
const combobox = Ariakit.useComboboxContext();
const render = combobox ? <Ariakit.ComboboxItem render={props.render} /> : undefined;
const ref = React.useRef<HTMLDivElement>(null);
useShortcutKeys({
shortcut: shortcut,
action: (e) => {
e.preventDefault();
e.stopPropagation();
if (ref.current) {
ref.current.click();
}
},
disabled: props.disabled,
enabledOnInputElements: true,
});
return (
<Ariakit.SelectItem
{...props}
render={render}
blurOnHoverEnd={false}
className={cn(
selectItemClasses,
"[--padding-block:0.5rem] sm:[--padding-block:0.25rem]",
props.className
)}
ref={ref}
>
<div className="flex h-7 w-full items-center gap-1 rounded-sm px-2 group-data-[active-item=true]:bg-tertiary">
{icon}
<div className="grow truncate">{props.children || props.value}</div>
{checkIcon}
{shortcut && (
<ShortcutKey className={cn("size-4 flex-none")} shortcut={shortcut} variant={"small"} />
)}
</div>
</Ariakit.SelectItem>
);
}
export interface SelectLinkItemProps extends Ariakit.SelectItemProps {
icon?: React.ReactNode;
checkIcon?: React.ReactNode;
shortcut?: ShortcutDefinition;
to: string;
}
export function SelectLinkItem({
checkIcon = <Ariakit.SelectItemCheck className="size-8 flex-none text-white" />,
to,
...props
}: SelectLinkItemProps) {
const render = <Link to={to} className={cn("block", selectItemClasses, props.className)} />;
return (
<SelectItem
{...props}
render={render}
blurOnHoverEnd={false}
className={cn(selectItemClasses, props.className)}
/>
);
}
export interface SelectButtonItemProps extends Omit<Ariakit.SelectItemProps, "onClick"> {
icon?: React.ReactNode;
checkIcon?: React.ReactNode;
shortcut?: ShortcutDefinition;
onClick: React.ComponentProps<"button">["onClick"];
}
export function SelectButtonItem({
checkIcon = <Ariakit.SelectItemCheck className="size-8 flex-none text-white" />,
onClick,
...props
}: SelectButtonItemProps) {
const render = (
<button
onClick={onClick}
className={cn("block w-full text-left", selectItemClasses, props.className)}
/>
);
return (
<SelectItem
{...props}
render={render}
blurOnHoverEnd={false}
className={cn(selectItemClasses, props.className)}
/>
);
}
export function shortcutFromIndex(
index: number,
meta: {
shortcutsEnabled?: boolean;
section?: { startIndex: number };
}
): ShortcutDefinition | undefined {
if (!meta.shortcutsEnabled) return;
let adjustedIndex = index + (meta.section?.startIndex ?? 0);
if (adjustedIndex > 9) return;
if (adjustedIndex === 9) {
adjustedIndex = -1;
}
return { key: String(adjustedIndex + 1) };
}
export interface SelectSeparatorProps extends React.ComponentProps<"div"> {}
export function SelectSeparator(props: SelectSeparatorProps) {
return <div {...props} className={cn("h-px bg-charcoal-700", props.className)} />;
}
export interface SelectGroupProps extends Ariakit.SelectGroupProps {}
export function SelectGroup(props: SelectGroupProps) {
return <Ariakit.SelectGroup {...props} />;
}
export interface SelectGroupLabelProps extends Ariakit.SelectGroupLabelProps {}
export function SelectGroupLabel(props: SelectGroupLabelProps) {
return (
<Ariakit.SelectGroupLabel
{...props}
className={cn(
"flex h-[1.375rem] items-center border-b border-charcoal-700 bg-charcoal-750 px-2.5 text-xxs uppercase text-text-bright",
props.className
)}
/>
);
}
export interface SelectHeadingProps extends Ariakit.SelectHeadingProps {}
export function SelectHeading({ render, ...props }: SelectHeadingProps) {
return (
<div className="flex h-[1.375rem] flex-none cursor-default items-center gap-2 border-b border-charcoal-700 bg-charcoal-750 px-2.5 text-xxs uppercase text-text-bright">
<Ariakit.SelectHeading render={<>{render}</>} />
</div>
);
}
export interface SelectPopoverProps extends Ariakit.SelectPopoverProps {}
export function SelectPopover({
gutter = 5,
shift = 0,
unmountOnHide = true,
className,
...props
}: SelectPopoverProps) {
return (
<Ariakit.SelectPopover
gutter={gutter}
shift={shift}
unmountOnHide={unmountOnHide}
className={cn(
"z-50 flex flex-col overflow-clip rounded border border-charcoal-700 bg-background-bright shadow-md outline-none animate-in fade-in-40",
"min-w-[max(180px,calc(var(--popover-anchor-width)+0.5rem))]",
"max-w-[min(480px,var(--popover-available-width))]",
"max-h-[min(480px,var(--popover-available-height))]",
"origin-[var(--popover-transform-origin)]",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
/>
);
}
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
SelectItem.displayName = SelectPrimitive.Item.displayName;
export interface SelectLabelProps extends Ariakit.SelectLabelProps {}
//currently unstyled
export function SelectLabel(props: SelectLabelProps) {
return <Ariakit.SelectLabel {...props} />;
}
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("bg-muted -mx-1 my-1 h-px", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export interface ComboBoxProps extends Ariakit.ComboboxProps {
shortcut?: ShortcutDefinition;
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
};
export function ComboBox({
autoSelect = true,
placeholder = "Filter options",
shortcut,
...props
}: ComboBoxProps) {
return (
<div className="flex h-9 w-full flex-none items-center border-b border-grid-dimmed bg-transparent px-3 text-xs text-text-dimmed outline-none">
<Ariakit.Combobox
autoSelect={autoSelect}
render={<input placeholder={placeholder} />}
className="flex-1 bg-transparent text-xs text-text-dimmed outline-none"
{...props}
/>
{shortcut && (
<ShortcutKey className={cn("size-4 flex-none")} shortcut={shortcut} variant={"small"} />
)}
</div>
);
}
@@ -0,0 +1,143 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "~/utils/cn";
const sizes = {
"secondary/small":
"text-xs h-6 bg-tertiary border border-tertiary group-hover:text-text-bright hover:border-charcoal-600 pr-2 pl-1.5",
medium: "text-sm h-8 bg-tertiary border border-tertiary hover:border-charcoal-600 px-2.5",
minimal: "text-xs h-6 bg-transparent hover:bg-tertiary pl-1.5 pr-2",
};
export type SelectProps = {
size?: keyof typeof sizes;
width?: "content" | "full";
};
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & SelectProps
>(({ className, children, width = "content", size = "secondary/small", ...props }, ref) => {
const sizeClassName = sizes[size];
return (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"ring-offset-background focus-visible:ring-ring group flex items-center justify-between gap-x-1 rounded text-text-dimmed transition placeholder:text-text-dimmed hover:text-text-bright focus-visible:bg-tertiary focus-visible:text-text-bright focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
width === "full" ? "w-full" : "w-min",
sizeClassName,
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown
className={cn(
"size-4 text-text-dimmed transition group-hover:text-text-bright group-focus:text-text-bright"
)}
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
});
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-max overflow-hidden rounded-md border border-charcoal-700 bg-background-dimmed text-text-bright shadow-md animate-in fade-in-40",
position === "popper" && "translate-y-1",
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"space-y-0.5 px-1 py-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn(
"-ml-1 -mr-1 mb-1 bg-charcoal-900 py-1.5 pl-2 pr-2 font-sans text-xxs font-normal uppercase leading-normal tracking-wider text-text-dimmed first-of-type:-mt-0",
className
)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
type SelectItemProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
contentClassName?: string;
};
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, SelectItemProps>(
({ className, children, contentClassName, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-charcoal-750 focus:bg-charcoal-750/50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("bg-muted -mx-1 my-1 h-px", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
};
@@ -20,7 +20,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "../primitives/Select";
} from "../primitives/SimpleSelect";
import { Spinner } from "../primitives/Spinner";
import {
FilterableEnvironment,
+642 -213
View File
@@ -1,9 +1,25 @@
import { XMarkIcon } from "@heroicons/react/20/solid";
import { useNavigate } from "@remix-run/react";
import type { TaskRunStatus as TaskRunStatusType } from "@trigger.dev/database";
import { RuntimeEnvironment, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
import { useCallback } from "react";
import * as Ariakit from "@ariakit/react";
import { CalendarIcon, CpuChipIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { Form } from "@remix-run/react";
import { RuntimeEnvironment, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database";
import { ListFilterIcon } from "lucide-react";
import { ReactNode, startTransition, useCallback, useMemo, useState } from "react";
import { z } from "zod";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
ComboBox,
ComboboxProvider,
SelectButtonItem,
SelectItem,
SelectList,
SelectPopover,
SelectProvider,
SelectTrigger,
shortcutFromIndex,
} from "~/components/primitives/Select";
import {
Tooltip,
TooltipContent,
@@ -11,243 +27,656 @@ import {
TooltipTrigger,
} from "~/components/primitives/Tooltip";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
import { useSearchParams } from "~/hooks/useSearchParam";
import { Button } from "../../primitives/Buttons";
import { Paragraph } from "../../primitives/Paragraph";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../primitives/Select";
import { TimeFrameFilter } from "../TimeFrameFilter";
import { TaskRunStatusCombo, descriptionForTaskRunStatus } from "./TaskRunStatus";
export const allTaskRunStatuses = [
"PENDING",
"WAITING_FOR_DEPLOY",
"EXECUTING",
"RETRYING_AFTER_FAILURE",
"WAITING_TO_RESUME",
"COMPLETED_SUCCESSFULLY",
"CANCELED",
"COMPLETED_WITH_ERRORS",
"INTERRUPTED",
"SYSTEM_FAILURE",
"CRASHED",
] as TaskRunStatusType[];
TaskRunStatusCombo,
allTaskRunStatuses,
descriptionForTaskRunStatus,
runStatusTitle,
} from "./TaskRunStatus";
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
export const TaskAttemptStatus = z.nativeEnum(TaskRunStatus);
export const TaskRunListSearchFilters = z.object({
cursor: z.string().optional(),
direction: z.enum(["forward", "backward"]).optional(),
environments: z
.string()
.optional()
.transform((value) => (value ? value.split(",") : undefined)),
tasks: z
.string()
.optional()
.transform((value) => (value ? value.split(",") : undefined)),
versions: z
.string()
.optional()
.transform((value) => (value ? value.split(",") : undefined)),
environments: z.preprocess(
(value) => (typeof value === "string" ? [value] : value),
z.string().array().optional()
),
tasks: z.preprocess(
(value) => (typeof value === "string" ? [value] : value),
z.string().array().optional()
),
versions: z.preprocess(
(value) => (typeof value === "string" ? [value] : value),
z.string().array().optional()
),
statuses: z.preprocess(
(value) => (typeof value === "string" ? value.split(",") : undefined),
(value) => (typeof value === "string" ? [value] : value),
TaskAttemptStatus.array().optional()
),
period: z.preprocess((value) => (value === "all" ? undefined : value), z.string().optional()),
from: z.coerce.number().optional(),
to: z.coerce.number().optional(),
});
export type TaskRunListSearchFilters = z.infer<typeof TaskRunListSearchFilters>;
const All = "ALL";
type DisplayableEnvironment = Pick<RuntimeEnvironment, "type" | "id"> & {
userName?: string;
};
type RunFiltersProps = {
possibleEnvironments: DisplayableEnvironment[];
possibleTasks: string[];
possibleTasks: { slug: string; triggerSource: TaskTriggerSource }[];
hasFilters: boolean;
};
export function RunsFilters({ possibleEnvironments, possibleTasks }: RunFiltersProps) {
const navigate = useNavigate();
export function RunsFilters(props: RunFiltersProps) {
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
const { environments, tasks, versions, statuses, from, to } = TaskRunListSearchFilters.parse(
Object.fromEntries(searchParams.entries())
);
const handleFilterChange = useCallback((filterType: string, value: string | undefined) => {
if (value) {
searchParams.set(filterType, value);
} else {
searchParams.delete(filterType);
}
searchParams.delete("cursor");
searchParams.delete("direction");
navigate(`${location.pathname}?${searchParams.toString()}`);
}, []);
const handleStatusChange = useCallback((value: TaskRunAttemptStatus | typeof All) => {
handleFilterChange("statuses", value === "ALL" ? undefined : value);
}, []);
const handleTaskChange = useCallback((value: string | typeof All) => {
handleFilterChange("tasks", value === "ALL" ? undefined : value);
}, []);
const handleEnvironmentChange = useCallback((value: string | typeof All) => {
handleFilterChange("environments", value === "ALL" ? undefined : value);
}, []);
const handleTimeFrameChange = useCallback((range: { from?: number; to?: number }) => {
if (range.from) {
searchParams.set("from", range.from.toString());
} else {
searchParams.delete("from");
}
if (range.to) {
searchParams.set("to", range.to.toString());
} else {
searchParams.delete("to");
}
searchParams.delete("cursor");
searchParams.delete("direction");
navigate(`${location.pathname}?${searchParams.toString()}`);
}, []);
const clearFilters = useCallback(() => {
searchParams.delete("statuses");
searchParams.delete("environments");
searchParams.delete("tasks");
searchParams.delete("from");
searchParams.delete("to");
navigate(`${location.pathname}?${searchParams.toString()}`);
}, []);
const hasFilters =
searchParams.has("statuses") ||
searchParams.has("environments") ||
searchParams.has("tasks") ||
searchParams.has("period");
return (
<div className="flex flex-row justify-between">
<SelectGroup>
<Select
name="environment"
value={environments?.at(0) ?? "ALL"}
onValueChange={handleEnvironmentChange}
>
<SelectTrigger size="minimal" width="full">
<SelectValue placeholder={"Select environment"} className="ml-2 p-0" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<Paragraph
variant="extra-small"
className="pl-0.5 transition group-hover:text-text-bright"
>
All environments
</Paragraph>
</SelectItem>
{possibleEnvironments.map((env) => (
<SelectItem key={env.id} value={env.id}>
<div className="flex items-center gap-x-2">
<EnvironmentLabel environment={env} userName={env.userName} />
<Paragraph variant="extra-small">environment</Paragraph>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
<SelectGroup>
<Select name="status" value={statuses?.at(0) ?? "ALL"} onValueChange={handleStatusChange}>
<SelectTrigger size="minimal" width="full">
<SelectValue placeholder="Select status" className="ml-2 p-0">
{statuses?.at(0) ? (
<TaskRunStatusCombo
status={statuses[0]}
className="text-xs"
iconClassName="animate-none"
/>
) : (
"All statuses"
)}
</SelectValue>
</SelectTrigger>
<SelectContent className="overflow-visible">
<SelectItem value={"ALL"} className="">
<Paragraph
variant="extra-small"
className="pl-0.5 transition group-hover:text-text-bright"
>
All statuses
</Paragraph>
</SelectItem>
{allTaskRunStatuses.map((status) => (
<TooltipProvider key={status}>
<Tooltip>
<TooltipTrigger className="group flex w-full flex-col py-0">
<SelectItem value={status} className="">
<TaskRunStatusCombo
status={status}
className="text-xs"
iconClassName="animate-none"
/>
<TooltipContent side="right" sideOffset={9}>
<Paragraph variant="extra-small">
{descriptionForTaskRunStatus(status)}
</Paragraph>
</TooltipContent>
</SelectItem>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
))}
</SelectContent>
</Select>
</SelectGroup>
<SelectGroup>
<Select name="tasks" value={tasks?.at(0) ?? "ALL"} onValueChange={handleTaskChange}>
<SelectTrigger size="minimal" width="full">
<SelectValue placeholder="Select task" className="ml-2 p-0" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<Paragraph
variant="extra-small"
className="pl-0.5 transition group-hover:text-text-bright"
>
All tasks
</Paragraph>
</SelectItem>
{possibleTasks
.sort((a, b) => a.localeCompare(b)) // 🔤
.map((task) => (
<SelectItem key={task} value={task}>
<Paragraph
variant="extra-small"
className="pl-0.5 transition group-hover:text-text-bright"
>
{task}
</Paragraph>
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
<TimeFrameFilter from={from} to={to} onRangeChanged={handleTimeFrameChange} />
<Button variant="minimal/small" onClick={() => clearFilters()} LeadingIcon={XMarkIcon} />
<div className="flex flex-row flex-wrap items-center gap-1">
<FilterMenu {...props} />
<AppliedFilters {...props} />
{hasFilters && (
<Form>
<Button variant="minimal/small" LeadingIcon={XMarkIcon}>
Clear all
</Button>
</Form>
)}
</div>
);
}
const filterTypes = [
{
name: "statuses",
title: "Status",
icon: (
<div className="flex size-4 items-center justify-center">
<div className="size-3 rounded-full border-2 border-text-dimmed" />
</div>
),
},
{ name: "environments", title: "Environment", icon: <CpuChipIcon className="size-4" /> },
{ name: "tasks", title: "Tasks", icon: <TaskIcon className="size-4" /> },
{ name: "created", title: "Created", icon: <CalendarIcon className="size-4" /> },
];
type FilterType = (typeof filterTypes)[number]["name"];
const shortcut = { key: "f" };
function FilterMenu(props: RunFiltersProps) {
const [filterType, setFilterType] = useState<FilterType | undefined>();
const filterTrigger = (
<SelectTrigger
icon={
<div className="flex size-4 items-center justify-center">
<ListFilterIcon className="size-3.5" />
</div>
}
variant={"minimal/small"}
shortcut={shortcut}
tooltipTitle={"Filter runs"}
>
Filter
</SelectTrigger>
);
return (
<FilterMenuProvider onClose={() => setFilterType(undefined)}>
{(search, setSearch) => (
<Menu
searchValue={search}
clearSearchValue={() => setSearch("")}
trigger={filterTrigger}
filterType={filterType}
setFilterType={setFilterType}
{...props}
/>
)}
</FilterMenuProvider>
);
}
function FilterMenuProvider({
children,
onClose,
}: {
children: (search: string, setSearch: (value: string) => void) => React.ReactNode;
onClose?: () => void;
}) {
const [searchValue, setSearchValue] = useState("");
return (
<ComboboxProvider
resetValueOnHide
setValue={(value) => {
startTransition(() => {
setSearchValue(value);
});
}}
setOpen={(open) => {
if (!open && onClose) {
onClose();
}
}}
>
{children(searchValue, setSearchValue)}
</ComboboxProvider>
);
}
function AppliedFilters({ possibleEnvironments, possibleTasks }: RunFiltersProps) {
return (
<>
<AppliedStatusFilter />
<AppliedEnvironmentFilter possibleEnvironments={possibleEnvironments} />
<AppliedTaskFilter possibleTasks={possibleTasks} />
<AppliedPeriodFilter />
</>
);
}
type MenuProps = {
searchValue: string;
clearSearchValue: () => void;
trigger: React.ReactNode;
filterType: FilterType | undefined;
setFilterType: (filterType: FilterType | undefined) => void;
} & RunFiltersProps;
function Menu(props: MenuProps) {
switch (props.filterType) {
case undefined:
return <MainMenu {...props} />;
case "statuses":
return <StatusDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
case "environments":
return <EnvironmentsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
case "tasks":
return <TasksDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
case "created":
return <CreatedDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
}
}
function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: MenuProps) {
const filtered = useMemo(() => {
return filterTypes.filter((item) =>
item.title.toLowerCase().includes(searchValue.toLowerCase())
);
}, [searchValue]);
return (
<SelectProvider virtualFocus={true}>
{trigger}
<SelectPopover>
<ComboBox placeholder={"Filter by..."} shortcut={shortcut} value={searchValue} />
<SelectList>
{filtered.map((type, index) => (
<SelectButtonItem
key={type.name}
onClick={() => {
clearSearchValue();
setFilterType(type.name);
}}
icon={type.icon}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
{type.title}
</SelectButtonItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
const statuses = allTaskRunStatuses.map((status) => ({
title: runStatusTitle(status),
value: status,
}));
function StatusDropdown({
trigger,
clearSearchValue,
searchValue,
onClose,
}: {
trigger: ReactNode;
clearSearchValue: () => void;
searchValue: string;
onClose?: () => void;
}) {
const { values, replace } = useSearchParams();
const handleChange = (values: string[]) => {
clearSearchValue();
replace({ statuses: values, cursor: undefined, direction: undefined });
};
const filtered = useMemo(() => {
return statuses.filter((item) => item.title.toLowerCase().includes(searchValue.toLowerCase()));
}, [searchValue]);
return (
<SelectProvider value={values("statuses")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder={"Filter by status..."} value={searchValue} />
<SelectList>
{filtered.map((item, index) => (
<SelectItem
key={item.value}
value={item.value}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="group flex w-full flex-col py-0">
<TaskRunStatusCombo status={item.value} iconClassName="animate-none" />
</TooltipTrigger>
<TooltipContent side="right" sideOffset={9}>
<Paragraph variant="extra-small">
{descriptionForTaskRunStatus(item.value)}
</Paragraph>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
function AppliedStatusFilter() {
const { values, del } = useSearchParams();
const statuses = values("statuses");
if (statuses.length === 0) {
return null;
}
return (
<FilterMenuProvider>
{(search, setSearch) => (
<StatusDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer" />}>
<AppliedFilter
label="Status"
value={appliedSummary(statuses.map((v) => runStatusTitle(v as TaskRunStatus)))}
onRemove={() => del(["statuses", "cursor", "direction"])}
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
function EnvironmentsDropdown({
trigger,
clearSearchValue,
searchValue,
onClose,
possibleEnvironments,
}: {
trigger: ReactNode;
clearSearchValue: () => void;
searchValue: string;
onClose?: () => void;
possibleEnvironments: DisplayableEnvironment[];
}) {
const { values, replace } = useSearchParams();
const handleChange = (values: string[]) => {
clearSearchValue();
replace({ environments: values, cursor: undefined, direction: undefined });
};
const filtered = useMemo(() => {
return possibleEnvironments.filter((item) => {
const title = environmentTitle(item, item.userName);
return title.toLowerCase().includes(searchValue.toLowerCase());
});
}, [searchValue, possibleEnvironments]);
return (
<SelectProvider value={values("environments")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder={"Filter by environment..."} value={searchValue} />
<SelectList>
{filtered.map((item, index) => (
<SelectItem
key={item.id}
value={item.id}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
<EnvironmentLabel environment={item} userName={item.userName} />
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
function AppliedEnvironmentFilter({
possibleEnvironments,
}: Pick<RunFiltersProps, "possibleEnvironments">) {
const { values, del } = useSearchParams();
if (values("environments").length === 0) {
return null;
}
return (
<FilterMenuProvider>
{(search, setSearch) => (
<EnvironmentsDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer" />}>
<AppliedFilter
label="Environment"
value={appliedSummary(
values("environments").map((v) => {
const environment = possibleEnvironments.find((env) => env.id === v);
return environment ? environmentTitle(environment, environment.userName) : v;
})
)}
onRemove={() => del(["environments", "cursor", "direction"])}
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
possibleEnvironments={possibleEnvironments}
/>
)}
</FilterMenuProvider>
);
}
function TasksDropdown({
trigger,
clearSearchValue,
searchValue,
onClose,
possibleTasks,
}: {
trigger: ReactNode;
clearSearchValue: () => void;
searchValue: string;
onClose?: () => void;
possibleTasks: { slug: string; triggerSource: TaskTriggerSource }[];
}) {
const { values, replace } = useSearchParams();
const handleChange = (values: string[]) => {
clearSearchValue();
replace({ tasks: values, cursor: undefined, direction: undefined });
};
const filtered = useMemo(() => {
return possibleTasks.filter((item) => {
return item.slug.toLowerCase().includes(searchValue.toLowerCase());
});
}, [searchValue, possibleTasks]);
return (
<SelectProvider value={values("tasks")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder={"Filter by task..."} value={searchValue} />
<SelectList>
{filtered.map((item, index) => (
<SelectItem
key={item.slug}
value={item.slug}
icon={<TaskTriggerSourceIcon source={item.triggerSource} className="size-4" />}
>
{item.slug}
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
function AppliedTaskFilter({ possibleTasks }: Pick<RunFiltersProps, "possibleTasks">) {
const { values, del } = useSearchParams();
if (values("tasks").length === 0) {
return null;
}
return (
<FilterMenuProvider>
{(search, setSearch) => (
<TasksDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer" />}>
<AppliedFilter
label="Task"
value={appliedSummary(
values("tasks").map((v) => {
const task = possibleTasks.find((task) => task.slug === v);
return task ? task.slug : v;
})
)}
onRemove={() => del(["tasks", "cursor", "direction"])}
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
possibleTasks={possibleTasks}
/>
)}
</FilterMenuProvider>
);
}
const timePeriods = [
{
label: "All periods",
value: "all",
},
{
label: "5 mins ago",
value: "5m",
},
{
label: "15 mins ago",
value: "15m",
},
{
label: "30 mins ago",
value: "30m",
},
{
label: "1 hour ago",
value: "1h",
},
{
label: "3 hours ago",
value: "3h",
},
{
label: "6 hours ago",
value: "6h",
},
{
label: "1 day ago",
value: "1d",
},
{
label: "3 days ago",
value: "3d",
},
{
label: "7 days ago",
value: "7d",
},
{
label: "10 days ago",
value: "10d",
},
{
label: "14 days ago",
value: "14d",
},
{
label: "30 days ago",
value: "30d",
},
];
function CreatedDropdown({
trigger,
clearSearchValue,
searchValue,
onClose,
}: {
trigger: ReactNode;
clearSearchValue: () => void;
searchValue: string;
onClose?: () => void;
}) {
const { value, replace } = useSearchParams();
const handleChange = (newValue: string) => {
clearSearchValue();
if (newValue === "all") {
if (!value) return;
}
replace({ period: newValue, cursor: undefined, direction: undefined });
};
const filtered = useMemo(() => {
return timePeriods.filter((item) =>
item.label.toLowerCase().includes(searchValue.toLowerCase())
);
}, [searchValue]);
return (
<SelectProvider value={value("period")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
hideOnEnter={false}
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder={"Filter by period..."} value={searchValue} />
<SelectList>
{filtered.map((item) => (
<SelectItem key={item.value} value={item.value} hideOnClick={false}>
{item.label}
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
function AppliedPeriodFilter() {
const { value, del } = useSearchParams();
if (value("period") === undefined || value("period") === "all") {
return null;
}
return (
<FilterMenuProvider>
{(search, setSearch) => (
<CreatedDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer" />}>
<AppliedFilter
label="Created"
value={
timePeriods.find((t) => t.value === value("period"))?.label ?? value("period")
}
onRemove={() => del(["period", "cursor", "direction"])}
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
function appliedSummary(values: string[], maxValues = 3) {
if (values.length === 0) {
return null;
}
if (values.length > maxValues) {
return `${values.slice(0, maxValues).join(", ")} + ${values.length - maxValues} more`;
}
return values.join(", ");
}
@@ -1,9 +1,11 @@
import { TrashIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { XMarkIcon } from "@heroicons/react/20/solid";
import { useNavigate } from "@remix-run/react";
import { RuntimeEnvironment } from "@trigger.dev/database";
import { useCallback } from "react";
import { z } from "zod";
import { Input } from "~/components/primitives/Input";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { useThrottle } from "~/hooks/useThrottle";
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
import { Button } from "../../primitives/Buttons";
import { Paragraph } from "../../primitives/Paragraph";
@@ -14,10 +16,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "../../primitives/Select";
import { Input } from "~/components/primitives/Input";
import { useDebounce } from "~/hooks/useDebounce";
import { useThrottle } from "~/hooks/useThrottle";
} from "../../primitives/SimpleSelect";
export const ScheduleListFilters = z.object({
page: z.coerce.number().default(1),
@@ -53,6 +52,9 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
Object.fromEntries(searchParams.entries())
);
const hasFilters =
searchParams.has("tasks") || searchParams.has("environments") || searchParams.has("search");
const handleFilterChange = useCallback((filterType: string, value: string | undefined) => {
if (value) {
searchParams.set(filterType, value);
@@ -146,7 +148,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
<SelectItem key={task} value={task}>
<Paragraph
variant="extra-small"
className="pl-0.5 transition group-hover:text-text-bright"
className="whitespace-nowrap pl-0.5 transition group-hover:text-text-bright"
>
{task}
</Paragraph>
@@ -156,7 +158,11 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
</Select>
</SelectGroup>
<Button variant="minimal/small" onClick={() => clearFilters()} LeadingIcon={XMarkIcon} />
{hasFilters && (
<Button variant="minimal/small" onClick={() => clearFilters()} LeadingIcon={XMarkIcon}>
Clear all
</Button>
)}
</div>
);
}
@@ -15,6 +15,20 @@ import { SnowflakeIcon } from "lucide-react";
import { Spinner } from "~/components/primitives/Spinner";
import { cn } from "~/utils/cn";
export const allTaskRunStatuses = [
"WAITING_FOR_DEPLOY",
"PENDING",
"EXECUTING",
"RETRYING_AFTER_FAILURE",
"WAITING_TO_RESUME",
"COMPLETED_SUCCESSFULLY",
"CANCELED",
"COMPLETED_WITH_ERRORS",
"CRASHED",
"INTERRUPTED",
"SYSTEM_FAILURE",
] as TaskRunStatus[];
const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
PENDING: "Task is waiting to be executed",
WAITING_FOR_DEPLOY: "Task needs to be deployed first to start executing",
+1
View File
@@ -104,6 +104,7 @@ const EnvironmentSchema = z.object({
COORDINATOR_SECRET: z.string().default("coordinator-secret"),
DEPOT_TOKEN: z.string().optional(),
DEPOT_PROJECT_ID: z.string().optional(),
DEPOT_ORG_ID: z.string().optional(),
CONTAINER_REGISTRY_ORIGIN: z.string().optional(),
CONTAINER_REGISTRY_USERNAME: z.string().optional(),
CONTAINER_REGISTRY_PASSWORD: z.string().optional(),
+43
View File
@@ -0,0 +1,43 @@
import { useTextFilter } from "./useTextFilter";
type Task = {
id: string;
friendlyId: string;
taskIdentifier: string;
exportName: string;
filePath: string;
triggerSource: string;
};
export function useFilterTasks<T extends Task>({ tasks }: { tasks: T[] }) {
return useTextFilter<T>({
items: tasks,
filter: (task, text) => {
if (task.taskIdentifier.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.exportName.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.filePath.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.id.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.friendlyId.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) {
return true;
}
return false;
},
});
}
@@ -53,3 +53,11 @@ export function useIsNewOrganizationPage(matches?: UIMatch[]): boolean {
export const useOrganizationChanged = (action: (org: MatchedOrganization | undefined) => void) => {
useChanged(useOptionalOrganization, action);
};
export function useIsImpersonating(matches?: UIMatch[]) {
const data = useTypedMatchesData<typeof orgLoader>({
id: "routes/_app.orgs.$organizationSlug",
matches,
});
return data?.isImpersonating === true;
}
+76
View File
@@ -0,0 +1,76 @@
import { useNavigate } from "@remix-run/react";
import { useOptimisticLocation } from "./useOptimisticLocation";
import { useCallback } from "react";
type Values = Record<string, string | string[] | undefined>;
export function useSearchParams() {
const navigate = useNavigate();
const location = useOptimisticLocation();
const search = new URLSearchParams(location.search);
const set = useCallback(
(values: Values) => {
for (const [param, value] of Object.entries(values)) {
if (value === undefined) {
search.delete(param);
continue;
}
if (typeof value === "string") {
search.set(param, value);
continue;
}
search.delete(param);
for (const v of value) {
search.append(param, v);
}
}
},
[location, search]
);
const replace = useCallback(
(values: Values) => {
set(values);
navigate(`${location.pathname}?${search.toString()}`, { replace: true });
},
[location, search]
);
const del = useCallback(
(keys: string | string[]) => {
if (!Array.isArray(keys)) {
keys = [keys];
}
for (const key of keys) {
search.delete(key);
}
navigate(`${location.pathname}?${search.toString()}`, { replace: true });
},
[location, search]
);
const value = useCallback(
(param: string) => {
return search.get(param) ?? undefined;
},
[location, search]
);
const values = useCallback(
(param: string) => {
return search.getAll(param);
},
[location, search]
);
return {
value,
values,
set,
replace,
del,
};
}
+16 -6
View File
@@ -17,16 +17,22 @@ export type ShortcutDefinition =
| Shortcut;
type useShortcutKeysProps = {
shortcut: ShortcutDefinition;
shortcut: ShortcutDefinition | undefined;
action: (event: KeyboardEvent) => void;
disabled?: boolean;
enabledOnInputElements?: boolean;
};
export function useShortcutKeys({ shortcut, action, disabled = false }: useShortcutKeysProps) {
export function useShortcutKeys({
shortcut,
action,
disabled = false,
enabledOnInputElements,
}: useShortcutKeysProps) {
const { platform } = useOperatingSystem();
const isMac = platform === "mac";
const relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
const relevantShortcut =
shortcut && "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
const keys = createKeysFromShortcut(relevantShortcut);
useHotkeys(
@@ -36,13 +42,17 @@ export function useShortcutKeys({ shortcut, action, disabled = false }: useShort
},
{
enabled: !disabled,
enableOnFormTags: relevantShortcut.enabledOnInputElements,
enableOnContentEditable: relevantShortcut.enabledOnInputElements,
enableOnFormTags: enabledOnInputElements ?? relevantShortcut?.enabledOnInputElements,
enableOnContentEditable: enabledOnInputElements ?? relevantShortcut?.enabledOnInputElements,
}
);
}
function createKeysFromShortcut(shortcut: Shortcut) {
function createKeysFromShortcut(shortcut: Shortcut | undefined) {
if (!shortcut) {
return [];
}
const modifiers = shortcut.modifiers;
const character = shortcut.key;
+6
View File
@@ -26,3 +26,9 @@ export function useUser(matches?: UIMatch[]): User {
export function useUserChanged(callback: (user: User | undefined) => void) {
useChanged(useOptionalUser, callback);
}
export function useHasAdminAccess(matches?: UIMatch[]): boolean {
const user = useOptionalUser(matches);
return Boolean(user?.admin);
}
+6
View File
@@ -168,6 +168,12 @@ export async function adminGetOrganizations(userId: string, { page, search }: Se
mode: "insensitive",
},
},
{
id: {
contains: search,
mode: "insensitive",
},
},
],
}
: undefined,
+1 -1
View File
@@ -1,6 +1,6 @@
import { json, Session } from "@remix-run/node";
import { redirect } from "remix-typedjson";
import { createCookieSessionStorage } from "@remix-run/node";
import { redirect } from "remix-typedjson";
import { env } from "~/env.server";
export type ToastMessage = {
@@ -0,0 +1,114 @@
import { AttemptStatus, RetrieveRunResponse, RunStatus, logger } from "@trigger.dev/core/v3";
import { TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
import assertNever from "assert-never";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { BasePresenter } from "./basePresenter.server";
export class ApiRetrieveRunPresenter extends BasePresenter {
public async call(
friendlyId: string,
env: AuthenticatedEnvironment,
showSecretDetails: boolean
): Promise<RetrieveRunResponse | undefined> {
return this.traceWithEnv("call", env, async (span) => {
const taskRun = await this._prisma.taskRun.findUnique({
where: {
friendlyId,
runtimeEnvironmentId: env.id,
},
include: {
attempts: {
orderBy: {
createdAt: "desc",
},
},
lockedToVersion: true,
},
});
if (!taskRun) {
logger.debug("Task run not found", { friendlyId, envId: env.id });
return undefined;
}
return {
id: taskRun.friendlyId,
status: ApiRetrieveRunPresenter.apiStatusFromRunStatus(taskRun.status),
taskIdentifier: taskRun.taskIdentifier,
idempotencyKey: taskRun.idempotencyKey ?? undefined,
version: taskRun.lockedToVersion ? taskRun.lockedToVersion.version : undefined,
createdAt: taskRun.createdAt ?? undefined,
updatedAt: taskRun.updatedAt ?? undefined,
attempts: !showSecretDetails
? []
: taskRun.attempts.map((a) => ({
id: a.friendlyId,
status: ApiRetrieveRunPresenter.apiStatusFromAttemptStatus(a.status),
createdAt: a.createdAt ?? undefined,
updatedAt: a.updatedAt ?? undefined,
startedAt: a.startedAt ?? undefined,
completedAt: a.completedAt ?? undefined,
})),
};
});
}
static apiStatusFromRunStatus(status: TaskRunStatus): RunStatus {
switch (status) {
case "WAITING_FOR_DEPLOY":
case "PENDING": {
return "PENDING";
}
case "RETRYING_AFTER_FAILURE":
case "EXECUTING": {
return "EXECUTING";
}
case "WAITING_TO_RESUME":
case "PAUSED": {
return "PAUSED";
}
case "CANCELED": {
return "CANCELED";
}
case "COMPLETED_SUCCESSFULLY": {
return "COMPLETED";
}
case "SYSTEM_FAILURE":
case "INTERRUPTED":
case "CRASHED":
case "COMPLETED_WITH_ERRORS": {
return "FAILED";
}
default: {
assertNever(status);
}
}
}
static apiStatusFromAttemptStatus(status: TaskRunAttemptStatus): AttemptStatus {
switch (status) {
case "PENDING": {
return "PENDING";
}
case "PAUSED": {
return "PAUSED";
}
case "EXECUTING": {
return "EXECUTING";
}
case "COMPLETED": {
return "COMPLETED";
}
case "FAILED": {
return "FAILED";
}
case "CANCELED": {
return "CANCELED";
}
default: {
assertNever(status);
}
}
}
}
@@ -1,5 +1,6 @@
import {
DeploymentErrorData,
ExternalBuildData,
TaskMetadataFailedToParseData,
groupTaskMetadataIssuesByTask,
} from "@trigger.dev/core/v3";
@@ -39,6 +40,7 @@ export class DeploymentPresenter {
const project = await this.#prismaClient.project.findFirstOrThrow({
select: {
id: true,
organizationId: true,
},
where: {
slug: projectSlug,
@@ -65,6 +67,9 @@ export class DeploymentPresenter {
shortCode: true,
version: true,
errorData: true,
imageReference: true,
externalBuildData: true,
projectId: true,
environment: {
select: {
id: true,
@@ -117,6 +122,10 @@ export class DeploymentPresenter {
},
});
const externalBuildData = deployment.externalBuildData
? ExternalBuildData.safeParse(deployment.externalBuildData)
: undefined;
return {
deployment: {
id: deployment.id,
@@ -137,6 +146,11 @@ export class DeploymentPresenter {
deployedBy: deployment.triggeredBy,
errorData: this.#prepareErrorData(deployment.errorData),
sdkVersion: deployment.worker?.sdkVersion,
imageReference: deployment.imageReference,
externalBuildData:
externalBuildData && externalBuildData.success ? externalBuildData.data : undefined,
projectId: deployment.projectId,
organizationId: project.organizationId,
},
};
}
@@ -1,4 +1,5 @@
import { Prisma, TaskRunStatus } from "@trigger.dev/database";
import parse from "parse-duration";
import { Direction } from "~/components/runs/RunStatuses";
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
import { sqlDatabaseSchema } from "~/db.server";
@@ -15,6 +16,7 @@ type RunListOptions = {
statuses?: TaskRunStatus[];
environments?: string[];
scheduleId?: string;
period?: string;
from?: number;
to?: number;
//pagination
@@ -38,6 +40,7 @@ export class RunListPresenter extends BasePresenter {
statuses,
environments,
scheduleId,
period,
from,
to,
direction = "forward",
@@ -47,10 +50,11 @@ export class RunListPresenter extends BasePresenter {
const hasStatusFilters = statuses && statuses.length > 0;
const hasFilters =
tasks !== undefined ||
versions !== undefined ||
(tasks !== undefined && tasks.length > 0) ||
(versions !== undefined && versions.length > 0) ||
hasStatusFilters ||
environments !== undefined ||
(environments !== undefined && environments.length > 0) ||
(period !== undefined && period !== "all") ||
from !== undefined ||
to !== undefined;
@@ -90,6 +94,8 @@ export class RunListPresenter extends BasePresenter {
},
});
const periodMs = period ? parse(period) : undefined;
//get the runs
let runs = await this._replica.$queryRaw<
{
@@ -152,6 +158,11 @@ export class RunListPresenter extends BasePresenter {
: Prisma.empty
}
${scheduleId ? Prisma.sql`AND tr."scheduleId" = ${scheduleId}` : 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`
@@ -224,7 +235,11 @@ export class RunListPresenter extends BasePresenter {
next,
previous,
},
possibleTasks: possibleTasks.map((task) => task.slug),
possibleTasks: possibleTasks
.map((task) => ({ slug: task.slug, triggerSource: task.triggerSource }))
.sort((a, b) => {
return a.slug.localeCompare(b.slug);
}),
filters: {
tasks: tasks || [],
versions: versions || [],
@@ -38,6 +38,7 @@ export class RunPresenter {
id: true,
type: true,
slug: true,
organizationId: true,
orgMember: {
select: {
user: {
@@ -66,9 +67,13 @@ export class RunPresenter {
if (!traceSummary) {
return {
run: {
id: run.id,
number: run.number,
friendlyId: run.friendlyId,
traceId: run.traceId,
environment: {
id: run.runtimeEnvironment.id,
organizationId: run.runtimeEnvironment.organizationId,
type: run.runtimeEnvironment.type,
slug: run.runtimeEnvironment.slug,
userId: run.runtimeEnvironment.orgMember?.user.id,
@@ -118,9 +123,13 @@ export class RunPresenter {
return {
run: {
id: run.id,
number: run.number,
friendlyId: run.friendlyId,
traceId: run.traceId,
environment: {
id: run.runtimeEnvironment.id,
organizationId: run.runtimeEnvironment.organizationId,
type: run.runtimeEnvironment.type,
slug: run.runtimeEnvironment.slug,
userId: run.runtimeEnvironment.orgMember?.user.id,
@@ -42,7 +42,7 @@ export class SpanPresenter {
const output =
span.outputType === "application/store"
? `/resources/packets/${span.environmentId}/${span.output}`
: typeof span.output !== "undefined" && span.output !== null
: typeof span.output !== "undefined"
? await prettyPrintPacket(span.output, span.outputType ?? undefined)
: undefined;
@@ -27,7 +27,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
} from "~/components/primitives/SimpleSelect";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
type FirstEndpointSheetProps = {
@@ -25,7 +25,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
} from "~/components/primitives/SimpleSelect";
import { TextLink } from "~/components/primitives/TextLink";
import { runStatusClassNameColor, runStatusTitle } from "~/components/runs/RunStatuses";
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
@@ -9,6 +9,7 @@ import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
import { Feedback } from "~/components/Feedback";
import { InitCommandV3, TriggerDevStepV3, TriggerLoginStepV3 } from "~/components/SetupCommands";
import { StepContentContainer } from "~/components/StepContentContainer";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
@@ -17,8 +18,9 @@ import { Callout } from "~/components/primitives/Callout";
import { formatDateTime } from "~/components/primitives/DateTime";
import { Header1, Header2, Header3 } from "~/components/primitives/Headers";
import { Input } from "~/components/primitives/Input";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
import { Spinner } from "~/components/primitives/Spinner";
import { StepNumber } from "~/components/primitives/StepNumber";
import {
@@ -125,6 +127,25 @@ export default function Page() {
<PageContainer>
<NavBar>
<PageTitle title="Tasks" />
<PageAccessories>
<AdminDebugTooltip>
<PropertyTable>
{tasks.map((task) => (
<Property label={task.exportName} key={task.slug}>
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">
{task.environments
.map((e) =>
e.userName ? `${e.userName}/${e.id}` : `${e.type.slice(0, 3)}/${e.id}`
)
.join(", ")}
</Paragraph>
</div>
</Property>
))}
</PropertyTable>
</AdminDebugTooltip>
</PageAccessories>
</NavBar>
<PageBody>
<div className={cn("grid h-full grid-rows-1")}>
@@ -1,7 +1,8 @@
import { BookOpenIcon } from "@heroicons/react/20/solid";
import { BookOpenIcon, LightBulbIcon, ShieldCheckIcon } from "@heroicons/react/20/solid";
import { Form } from "@remix-run/react";
import { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
import { RegenerateApiKeyModal } from "~/components/environments/RegenerateApiKeyModal";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
@@ -10,8 +11,9 @@ import { Callout } from "~/components/primitives/Callout";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { Header3 } from "~/components/primitives/Headers";
import { PageAccessories, NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
import {
Table,
TableBody,
@@ -24,7 +26,6 @@ import {
import { TextLink } from "~/components/primitives/TextLink";
import { prisma } from "~/db.server";
import { useFeatures } from "~/hooks/useFeatures";
import { useProject } from "~/hooks/useProject";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { createEnvironment } from "~/models/organization.server";
import { ApiKeysPresenter } from "~/presenters/v3/ApiKeysPresenter.server";
@@ -132,6 +133,18 @@ export default function Page() {
<NavBar>
<PageTitle title="API keys" />
<PageAccessories>
<AdminDebugTooltip>
<PropertyTable>
{environments.map((environment) => (
<Property label={environment.slug} key={environment.id}>
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{environment.id}</Paragraph>
</div>
</Property>
))}
</PropertyTable>
</AdminDebugTooltip>
<LinkButton
variant={"minimal/small"}
LeadingIcon={BookOpenIcon}
@@ -1,8 +1,9 @@
import { useLocation } from "@remix-run/react";
import { Link, useLocation } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { ExitIcon } from "~/assets/icons/ExitIcon";
import { UserAvatar } from "~/components/UserProfilePhoto";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { CodeBlock } from "~/components/code/CodeBlock";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { Badge } from "~/components/primitives/Badge";
@@ -68,6 +69,48 @@ export default function Page() {
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright">
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
<Header2 className={cn("whitespace-nowrap")}>Deploy: {deployment.shortCode}</Header2>
<AdminDebugTooltip>
<PropertyTable>
<Property label="ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{deployment.id}</Paragraph>
</div>
</Property>
<Property label="Project ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{deployment.projectId}</Paragraph>
</div>
</Property>
<Property label="Org ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{deployment.organizationId}</Paragraph>
</div>
</Property>
{deployment.imageReference && (
<Property label="Image">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">
{deployment.imageReference}
</Paragraph>
</div>
</Property>
)}
{deployment.externalBuildData && (
<Property label="Build Server">
<div className="flex items-center gap-2">
<Link
to={`/resources/${deployment.projectId}/deployments/${deployment.id}/logs`}
className="extra-small/bright/mono underline"
>
{deployment.externalBuildData.buildId}
</Link>
</div>
</Property>
)}
</PropertyTable>
</AdminDebugTooltip>
<LinkButton
to={`${v3DeploymentsPath(organization, project)}${page ? `?page=${page}` : ""}`}
variant="minimal/medium"
@@ -67,6 +67,8 @@ import {
v3RunsPath,
} from "~/utils/pathBuilder";
import { SpanView } from "../resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -116,6 +118,33 @@ export default function Page() {
title={`Run #${run.number}`}
/>
<PageAccessories>
<AdminDebugTooltip>
<PropertyTable>
<Property label="ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{run.id}</Paragraph>
</div>
</Property>
<Property label="Trace ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{run.traceId}</Paragraph>
</div>
</Property>
<Property label="Env ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{run.environment.id}</Paragraph>
</div>
</Property>
<Property label="Org ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">
{run.environment.organizationId}
</Paragraph>
</div>
</Property>
</PropertyTable>
</AdminDebugTooltip>
<EnvironmentLabel
size="large"
environment={run.environment}
@@ -165,6 +194,33 @@ export default function Page() {
title={`Run #${run.number}`}
/>
<PageAccessories>
<AdminDebugTooltip>
<PropertyTable>
<Property label="ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{run.id}</Paragraph>
</div>
</Property>
<Property label="Trace ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{run.traceId}</Paragraph>
</div>
</Property>
<Property label="Env ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{run.environment.id}</Paragraph>
</div>
</Property>
<Property label="Org ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">
{run.environment.organizationId}
</Paragraph>
</div>
</Property>
</PropertyTable>
</AdminDebugTooltip>
<EnvironmentLabel size="large" environment={run.environment} userName={usernameForEnv} />
</PageAccessories>
</NavBar>
@@ -30,8 +30,15 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { projectParam, organizationSlug } = ProjectParamSchema.parse(params);
const url = new URL(request.url);
const s = Object.fromEntries(url.searchParams.entries());
const { tasks, versions, statuses, environments, from, to, cursor, direction } =
const s = {
cursor: url.searchParams.get("cursor") ?? undefined,
direction: url.searchParams.get("direction") ?? undefined,
statuses: url.searchParams.getAll("statuses"),
environments: url.searchParams.getAll("environments"),
tasks: url.searchParams.getAll("tasks"),
period: url.searchParams.get("period") ?? undefined,
};
const { tasks, versions, statuses, environments, period, from, to, cursor, direction } =
TaskRunListSearchFilters.parse(s);
const presenter = new RunListPresenter();
@@ -42,6 +49,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
versions,
statuses,
environments,
period,
from,
to,
direction: direction,
@@ -88,10 +96,11 @@ export default function Page() {
) : (
<div className={cn("grid h-fit grid-cols-1 gap-4")}>
<div>
<div className="mb-2 flex items-center justify-between gap-x-2">
<div className="mb-2 flex items-start justify-between gap-x-2">
<RunsFilters
possibleEnvironments={project.environments}
possibleTasks={list.possibleTasks}
hasFilters={list.hasFilters}
/>
<div className="flex items-center justify-end gap-x-2">
<ListPagination list={list} />
@@ -4,6 +4,7 @@ import { Outlet, useLocation, useParams } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
@@ -12,6 +13,7 @@ import { DateTime } from "~/components/primitives/DateTime";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
import {
ResizableHandle,
ResizablePanel,
@@ -93,6 +95,18 @@ export default function Page() {
<NavBar>
<PageTitle title="Schedules" />
<PageAccessories>
<AdminDebugTooltip>
<PropertyTable>
{schedules.map((schedule) => (
<Property label={schedule.friendlyId} key={schedule.id}>
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{schedule.id}</Paragraph>
</div>
</Property>
))}
</PropertyTable>
</AdminDebugTooltip>
<LinkButton
LeadingIcon={PlusIcon}
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
@@ -3,6 +3,7 @@ import { parse } from "@conform-to/zod";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { ActionFunction, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { InlineCode } from "~/components/code/InlineCode";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Button } from "~/components/primitives/Buttons";
@@ -14,7 +15,9 @@ import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
import { prisma } from "~/db.server";
import { useProject } from "~/hooks/useProject";
import { redirectWithSuccessMessage } from "~/models/message.server";
@@ -110,6 +113,23 @@ export default function Page() {
<PageContainer>
<NavBar>
<PageTitle title={`${project.name} project settings`} />
<PageAccessories>
<AdminDebugTooltip>
<PropertyTable>
<Property label="ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{project.id}</Paragraph>
</div>
</Property>
<Property label="Org ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{project.organizationId}</Paragraph>
</div>
</Property>
</PropertyTable>
</AdminDebugTooltip>
</PageAccessories>
</NavBar>
<PageBody>
@@ -30,11 +30,11 @@ import {
} from "~/components/primitives/Table";
import { TaskFunctionName } from "~/components/runs/v3/TaskPath";
import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource";
import { useFilterTasks } from "~/hooks/useFilterTasks";
import { useLinkStatus } from "~/hooks/useLinkStatus";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useTextFilter } from "~/hooks/useTextFilter";
import {
SelectedEnvironment,
TaskListItem,
@@ -153,36 +153,7 @@ function TaskSelector({
tasks: TaskListItem[];
environmentSlug: string;
}) {
const { filterText, setFilterText, filteredItems } = useTextFilter<TaskListItem>({
items: tasks,
filter: (task, text) => {
if (task.taskIdentifier.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.exportName.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.filePath.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.id.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.friendlyId.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) {
return true;
}
return false;
},
});
const { filterText, setFilterText, filteredItems } = useFilterTasks<TaskListItem>({ tasks });
return (
<div className="divide-y divide-charcoal-800 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
@@ -8,6 +8,7 @@ import { UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-type
import invariant from "tiny-invariant";
import { z } from "zod";
import { UserAvatar } from "~/components/UserProfilePhoto";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import {
Alert,
@@ -23,8 +24,9 @@ import { Button, ButtonContent, LinkButton } from "~/components/primitives/Butto
import { DateTime } from "~/components/primitives/DateTime";
import { Header2, Header3 } from "~/components/primitives/Headers";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { useOrganization } from "~/hooks/useOrganizations";
import { useUser } from "~/hooks/useUser";
@@ -107,6 +109,28 @@ export default function Page() {
<PageContainer>
<NavBar>
<PageTitle title="Team" />
<PageAccessories>
<AdminDebugTooltip>
<PropertyTable>
<Property label="Org ID">
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">{organization.id}</Paragraph>
</div>
</Property>
{members.map((member) => (
<Property label={member.user.name} key={member.id}>
<div className="flex items-center gap-2">
<Paragraph variant="extra-small/bright/mono">
{member.user.email} - {member.user.id}
</Paragraph>
</div>
</Property>
))}
</PropertyTable>
</AdminDebugTooltip>
</PageAccessories>
</NavBar>
<PageBody>
<Header2>Members</Header2>
@@ -16,14 +16,7 @@ import { FormTitle } from "~/components/primitives/FormTitle";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
import { Select, SelectItem } from "~/components/primitives/Select";
import { prisma } from "~/db.server";
import { useFeatures } from "~/hooks/useFeatures";
import { redirectWithSuccessMessage } from "~/models/message.server";
@@ -148,20 +141,24 @@ export default function NewOrganizationPage() {
{canCreateV3Projects ? (
<InputGroup>
<Label htmlFor={projectVersion.id}>Project version</Label>
<SelectGroup>
<Select
{...conform.input(projectVersion, { type: "select" })}
defaultValue={defaultVersion}
>
<SelectTrigger width="full" size="medium">
<SelectValue placeholder="Project version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="v2">Version 2</SelectItem>
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
</SelectContent>
</Select>
</SelectGroup>
<Select
{...conform.select(projectVersion)}
defaultValue={undefined}
variant="tertiary/medium"
placeholder="Select version"
dropdownIcon
text={(value) => {
switch (value) {
case "v2":
return "Version 2";
case "v3":
return "Version 3";
}
}}
>
<SelectItem value="v2">Version 2</SelectItem>
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
</Select>
<FormError id={projectVersion.errorId}>{projectVersion.error}</FormError>
</InputGroup>
) : (
+19 -19
View File
@@ -17,14 +17,7 @@ import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { RadioGroupItem } from "~/components/primitives/RadioButton";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
import { Select, SelectItem } from "~/components/primitives/Select";
import { featuresForRequest } from "~/features.server";
import { useFeatures } from "~/hooks/useFeatures";
import { createOrganization } from "~/models/organization.server";
@@ -143,17 +136,24 @@ export default function NewOrganizationPage() {
{v3Enabled ? (
<InputGroup>
<Label htmlFor={projectVersion.id}>Project version</Label>
<SelectGroup>
<Select {...conform.input(projectVersion, { type: "select" })} defaultValue={"v2"}>
<SelectTrigger width="full" size="medium">
<SelectValue placeholder="Project version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="v2">Version 2</SelectItem>
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
</SelectContent>
</Select>
</SelectGroup>
<Select
{...conform.select(projectVersion)}
defaultValue={undefined}
variant="tertiary/medium"
placeholder="Select version"
dropdownIcon
text={(value) => {
switch (value) {
case "v2":
return "Version 2";
case "v3":
return "Version 3";
}
}}
>
<SelectItem value="v2">Version 2</SelectItem>
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
</Select>
<FormError id={projectVersion.errorId}>{projectVersion.error}</FormError>
</InputGroup>
) : (
+6 -3
View File
@@ -15,9 +15,12 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
return redirect(confirmBasicDetailsPath());
}
return typedjson({
headers: [["Set-Cookie", await commitSession(await clearRedirectTo(request))]],
});
return typedjson(
{},
{
headers: { "Set-Cookie": await commitSession(await clearRedirectTo(request)) },
}
);
};
export default function App() {
+1 -5
View File
@@ -1,11 +1,9 @@
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
import { Form } from "@remix-run/react";
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
import { redirect } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Header1 } from "~/components/primitives/Headers";
import { Input } from "~/components/primitives/Input";
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
@@ -18,10 +16,8 @@ import {
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { useUser } from "~/hooks/useUser";
import { adminGetOrganizations, adminGetUsers, setV3Enabled } from "~/models/admin.server";
import { adminGetOrganizations, setV3Enabled } from "~/models/admin.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { commitImpersonationSession, setImpersonationId } from "~/services/impersonation.server";
import { requireUserId } from "~/services/session.server";
import { createSearchParams } from "~/utils/searchParams";
@@ -0,0 +1,44 @@
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { apiCors } from "~/utils/apiCors";
const ParamsSchema = z.object({
runId: z.string(),
});
export async function loader({ request, params }: LoaderFunctionArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
const authenticationResult = await authenticateApiRequest(request, {
allowPublicKey: true,
});
if (!authenticationResult) {
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
}
const authenticatedEnv = authenticationResult.environment;
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return apiCors(request, json({ error: "Invalid or missing runId" }, { status: 400 }));
}
const { runId } = parsed.data;
const showSecretDetails = authenticationResult.type === "PRIVATE";
const presenter = new ApiRetrieveRunPresenter();
const result = await presenter.call(runId, authenticatedEnv, showSecretDetails);
if (!result) {
return apiCors(request, json({ error: "Run not found" }, { status: 404 }));
}
return apiCors(request, json(result));
}
@@ -0,0 +1,57 @@
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { ExternalBuildData } from "@trigger.dev/core/v3";
import { z } from "zod";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { redirectBackWithErrorMessage } from "~/models/message.server";
import { requireUserId } from "~/services/session.server";
const ParamsSchema = z.object({
projectId: z.string(),
deploymentId: z.string(),
});
// Redirects to Depot.dev
export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const { projectId, deploymentId } = ParamsSchema.parse(params);
await prisma.project.findFirstOrThrow({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
},
});
const deployment = await prisma.workerDeployment.findUniqueOrThrow({
where: {
id: deploymentId,
projectId: projectId,
},
});
const externalBuildData = deployment.externalBuildData
? ExternalBuildData.safeParse(deployment.externalBuildData)
: undefined;
if (!externalBuildData || externalBuildData.success === false) {
return redirectBackWithErrorMessage(request, "No build data found for this deployment.");
}
if (!env.DEPOT_ORG_ID) {
return redirectBackWithErrorMessage(request, "No Depot organization ID found.");
}
return new Response(null, {
status: 302,
headers: {
Location: `https://depot.dev/orgs/${env.DEPOT_ORG_ID}/projects/${externalBuildData.data.projectId}/builds/${externalBuildData.data.buildId}/logs`,
},
});
}
@@ -21,14 +21,7 @@ import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
import { Select, SelectItem } from "~/components/primitives/Select";
import {
Table,
TableBody,
@@ -189,28 +182,25 @@ export function UpsertScheduleForm({
<Fieldset>
<InputGroup>
<Label htmlFor={taskIdentifier.id}>Task</Label>
<SelectGroup>
<Select
{...conform.input(taskIdentifier, { type: "select" })}
defaultValue={schedule?.taskIdentifier}
>
<SelectTrigger size="medium" width="full">
<SelectValue placeholder="Select task" className="ml-2 p-0" />
</SelectTrigger>
<SelectContent>
{possibleTasks.map((task) => (
<Select
{...conform.select(taskIdentifier)}
placeholder="Select a task"
defaultValue={schedule?.taskIdentifier}
heading={"Filter..."}
items={possibleTasks}
filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())}
dropdownIcon
>
{(matches) => (
<>
{matches?.map((task) => (
<SelectItem key={task} value={task}>
<Paragraph
variant="extra-small"
className="pl-0.5 transition group-hover:text-text-bright"
>
{task}
</Paragraph>
{task}
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
</>
)}
</Select>
<FormError id={taskIdentifier.errorId}>{taskIdentifier.error}</FormError>
</InputGroup>
{showGenerateField && <AIGeneratedCronField onSuccess={setCronPattern} />}
@@ -63,6 +63,14 @@ export const action: ActionFunction = async ({ request, params }) => {
{ spanId: newRun.spanId }
);
logger.debug("Replayed run", {
taskRunId: taskRun.id,
taskRunFriendlyId: taskRun.friendlyId,
newRunId: newRun.id,
newRunFriendlyId: newRun.friendlyId,
runPath,
});
return redirectWithSuccessMessage(runPath, request, `Replaying run`);
} catch (error) {
if (error instanceof Error) {
@@ -0,0 +1,246 @@
import { CpuChipIcon } from "@heroicons/react/20/solid";
import { CircleStackIcon } from "@heroicons/react/24/outline";
import { Form } from "@remix-run/react";
import { startTransition, useCallback, useMemo, useState } from "react";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import {
ComboBox,
ComboboxProvider,
SelectButtonItem,
SelectItem,
SelectList,
SelectPopover,
SelectProvider,
SelectTrigger,
shortcutFromIndex,
} from "~/components/primitives/Select";
import {
TaskRunStatusCombo,
allTaskRunStatuses,
runStatusTitle,
} from "~/components/runs/v3/TaskRunStatus";
import { useSearchParams } from "~/hooks/useSearchParam";
import { ShortcutDefinition } from "~/hooks/useShortcutKeys";
export default function Story() {
return (
<div className="flex h-full max-w-full flex-wrap items-start justify-start gap-2 px-8 py-16">
<Form className="space-y-4">
<div className="flex gap-16">
<Filter />
</div>
</Form>
</div>
);
}
const filterTypes = [
{
name: "status",
title: "Status",
icon: <CircleStackIcon className="h-4 w-4" />,
},
{ name: "environment", title: "Environment", icon: <CpuChipIcon className="h-4 w-4" /> },
];
type FilterType = (typeof filterTypes)[number]["name"];
//todo what if we had shortcut keys that would deeplink you to the appropriate filter?
function Filter() {
const [filterType, setFilterType] = useState<FilterType | undefined>();
const [searchValue, setSearchValue] = useState("");
const shortcut = { key: "f" };
const clearSearchValue = useCallback(() => {
setSearchValue("");
}, [setSearchValue]);
const filterTrigger = (
<SelectTrigger variant={"tertiary/small"} shortcut={shortcut} tooltipTitle={"Filter runs"}>
Filter
</SelectTrigger>
);
return (
<ComboboxProvider
resetValueOnHide
setValue={(value) => {
startTransition(() => {
setSearchValue(value);
});
}}
setOpen={(open) => {
if (!open) {
setFilterType(undefined);
}
}}
>
<Menu
searchValue={searchValue}
clearSearchValue={clearSearchValue}
shortcut={shortcut}
trigger={filterTrigger}
filterType={filterType}
setFilterType={setFilterType}
/>
</ComboboxProvider>
);
}
type MenuProps = {
searchValue: string;
clearSearchValue: () => void;
shortcut: ShortcutDefinition;
trigger: React.ReactNode;
filterType: FilterType | undefined;
setFilterType: (filterType: FilterType | undefined) => void;
};
function Menu(props: MenuProps) {
switch (props.filterType) {
case undefined:
return <MainMenu {...props} />;
case "status":
return <Statuses {...props} />;
case "environment":
return <Environments {...props} />;
}
return <></>;
}
function MainMenu({ searchValue, clearSearchValue, setFilterType, trigger, shortcut }: MenuProps) {
const filtered = useMemo(() => {
return filterTypes.filter((item) =>
item.title.toLowerCase().includes(searchValue.toLowerCase())
);
}, [searchValue]);
return (
<SelectProvider virtualFocus={true}>
{trigger}
<SelectPopover>
<ComboBox placeholder={"Filter..."} shortcut={shortcut} value={searchValue} />
<SelectList>
{filtered.map((type, index) => (
<SelectButtonItem
key={type.name}
onClick={() => {
clearSearchValue();
setFilterType(type.name);
}}
icon={type.icon}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
{type.title}
</SelectButtonItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
const statuses = allTaskRunStatuses.map((status) => ({
title: runStatusTitle(status),
value: status,
}));
function Statuses({ trigger, clearSearchValue, shortcut, searchValue, setFilterType }: MenuProps) {
const { values, replace } = useSearchParams();
const handleChange = useCallback((values: string[]) => {
clearSearchValue();
replace({ status: values });
}, []);
const filtered = useMemo(() => {
return statuses.filter((item) => item.title.toLowerCase().includes(searchValue.toLowerCase()));
}, [searchValue]);
return (
<SelectProvider value={values("status")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
hideOnEscape={() => {
setFilterType(undefined);
return false;
}}
>
<ComboBox placeholder={"Filter by status..."} shortcut={shortcut} value={searchValue} />
<SelectList>
{filtered.map((item, index) => (
<SelectItem
key={item.value}
value={item.value}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
<TaskRunStatusCombo status={item.value} iconClassName="animate-none" />
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
const environments = [
{
type: "DEVELOPMENT" as const,
},
{
type: "STAGING" as const,
},
{
type: "PRODUCTION" as const,
},
];
function Environments({
trigger,
clearSearchValue,
shortcut,
searchValue,
setFilterType,
}: MenuProps) {
const { values, replace } = useSearchParams();
const handleChange = useCallback((values: string[]) => {
clearSearchValue();
replace({ environment: values });
}, []);
const filtered = useMemo(() => {
return environments.filter((item) =>
item.type.toLowerCase().includes(searchValue.toLowerCase())
);
}, [searchValue]);
return (
<SelectProvider value={values("environment")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
hideOnEscape={() => {
setFilterType(undefined);
return false;
}}
>
<ComboBox
placeholder={"Filter by environment..."}
shortcut={shortcut}
value={searchValue}
/>
<SelectList>
{filtered.map((item, index) => (
<SelectItem
key={item.type}
value={item.type}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
<EnvironmentLabel environment={item} />
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
+257 -76
View File
@@ -1,91 +1,272 @@
import { Header1, Header2 } from "~/components/primitives/Headers";
import { CircleStackIcon } from "@heroicons/react/20/solid";
import { Form, useNavigate } from "@remix-run/react";
import { useCallback, useState } from "react";
import { LogoIcon } from "~/components/LogoIcon";
import { Button } from "~/components/primitives/Buttons";
import {
Select,
SelectContent,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
SelectLinkItem,
shortcutFromIndex,
} from "~/components/primitives/Select";
import {
TaskRunStatusCombo,
allTaskRunStatuses,
runStatusTitle,
} from "~/components/runs/v3/TaskRunStatus";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
const branches = [
"main",
"0.10-stable",
"0.11-stable",
"0.12-stable",
"0.13-stable",
"0.14-stable",
"15-stable",
"15.6-dev",
"16.3-dev",
"16.4.2-dev",
"16.8.3",
"16.8.4",
"16.8.5",
"16.8.6",
"17.0.0-dev",
"builds/facebook-www",
"devtools-v4-merge",
"fabric-cleanup",
"fabric-focus-blur",
"gh-pages",
"leg",
"nativefb-enable-cache",
"nov-main-trigger",
"rsckeys",
];
export default function Story() {
return (
<div className="p-20">
<div className="flex flex-col">
<Header1 className="mb-4">Variants</Header1>
<Header2 className="my-4">size=small width=content</Header2>
<SelectGroup>
<Select name="colorScheme" defaultValue="dark">
<SelectTrigger>
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectLabel>Color Scheme</SelectLabel>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
<SelectLabel>Other themes</SelectLabel>
<SelectItem value="sunset">Sunset</SelectItem>
<SelectItem value="midnight">Midnight</SelectItem>
<SelectSeparator />
<SelectItem value="lunar">Lunar</SelectItem>
</SelectContent>
<div className="flex h-full max-w-full flex-wrap items-start justify-start gap-2 px-4 py-16">
<Form className="space-y-4">
<div className="flex gap-16">
<ProjectSelector />
<Statuses />
<Select name="static" text="Static" defaultValue={[]} shortcut={{ key: "e" }}>
<SelectItem value={"value"} shortcut={{ key: "1" }}>
Item 1
</SelectItem>
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
Item 2
</SelectItem>
</Select>
</SelectGroup>
<Header2 className="my-4">size=small width=full</Header2>
<SelectGroup>
<Select name="colorScheme" defaultValue="dark">
<SelectTrigger width="full">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
<SelectItem value="sunset">Sunset</SelectItem>
<SelectItem value="midnight">Midnight</SelectItem>
<SelectSeparator />
<SelectItem value="lunar">Lunar</SelectItem>
</SelectContent>
<Select
variant="tertiary/medium"
text="Tertiary medium"
defaultValue={[]}
shortcut={{ key: "e" }}
>
<SelectItem value={"value"} shortcut={{ key: "1" }}>
Item 1
</SelectItem>
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
Item 2
</SelectItem>
</Select>
</SelectGroup>
<Header2 className="my-4">size=medium width=content</Header2>
<SelectGroup>
<Select name="colorScheme" defaultValue="dark">
<SelectTrigger size="medium">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
<SelectItem value="sunset">Sunset</SelectItem>
<SelectItem value="midnight">Midnight</SelectItem>
<SelectSeparator />
<SelectItem value="lunar">Lunar</SelectItem>
</SelectContent>
<Select variant="minimal/small" text="Minimal" defaultValue={[]} shortcut={{ key: "e" }}>
<SelectItem value={"value"} shortcut={{ key: "1" }}>
Item 1
</SelectItem>
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
Item 2
</SelectItem>
</Select>
</SelectGroup>
<Header2 className="my-4">size=medium width=full</Header2>
<SelectGroup>
<Select name="colorScheme" defaultValue="dark">
<SelectTrigger size="medium" width="full">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
<SelectItem value="sunset">Sunset</SelectItem>
<SelectItem value="midnight">Midnight</SelectItem>
<SelectSeparator />
<SelectItem value="lunar">Lunar</SelectItem>
</SelectContent>
<Select
variant="minimal/medium"
text="Tertiary medium"
defaultValue={[]}
shortcut={{ key: "e" }}
>
<SelectItem value={"value"} shortcut={{ key: "1" }}>
Item 1
</SelectItem>
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
Item 2
</SelectItem>
</Select>
</SelectGroup>
</div>
<Select
name="static"
text="Heading"
defaultValue={[]}
showHeading={true}
heading="A heading"
shortcut={{ key: "h" }}
>
<SelectItem value={"value"} shortcut={{ key: "1" }}>
Item 1
</SelectItem>
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
Item 2
</SelectItem>
</Select>
<Select
name="branch2"
heading={"Filter by status..."}
defaultValue={"main"}
items={branches}
>
{(matches) => matches?.map((value) => <SelectItem key={value} value={value} />)}
</Select>
<Button variant="tertiary/small">Submit</Button>
</div>
</Form>
</div>
);
}
const statuses = allTaskRunStatuses.map((status) => ({
title: runStatusTitle(status),
value: status,
}));
function Statuses() {
const navigate = useNavigate();
const location = useOptimisticLocation();
const search = new URLSearchParams(location.search);
const handleChange = useCallback((values: string[]) => {
search.delete("status");
for (const value of values) {
search.append("status", value);
}
navigate(`${location.pathname}?${search.toString()}`, { replace: true });
}, []);
return (
<Select
name="status"
text="Status"
value={search.getAll("status")}
setValue={handleChange}
heading={"Filter by status..."}
items={statuses}
filter={(item, search) => item.title.toLowerCase().includes(search.toLowerCase())}
shortcut={{ key: "s" }}
>
{(matches, { shortcutsEnabled }) => (
<>
{matches?.map((item, index) => (
<SelectItem
key={item.value}
value={item.value}
shortcut={shortcutFromIndex(index, { shortcutsEnabled })}
>
<TaskRunStatusCombo status={item.value} iconClassName="animate-none" />
</SelectItem>
))}
</>
)}
</Select>
);
}
export const projects = [
{
type: "section" as const,
title: "Apple",
items: [
{
title: "iTunes",
value: "itunes",
},
{
title: "App Store",
value: "appstore",
},
],
},
{
type: "section" as const,
title: "Google",
items: [
{
title: "Maps",
value: "maps",
},
{
title: "Gmail",
value: "gmail",
},
{
title: "Waymo",
value: "waymo",
},
{
title: "Android",
value: "android",
},
],
},
{
type: "section" as const,
title: "Uber",
items: [
{
title: "Planner",
value: "planner",
},
],
},
];
function ProjectSelector() {
const location = useOptimisticLocation();
const search = new URLSearchParams(location.search);
const selected = projects
.find((p) => p.items.some((i) => i.value === search.get("project")))
?.items.find((i) => i.value === search.get("project"));
const searchParams = new URLSearchParams(location.search);
searchParams.delete("project");
return (
<Select
name="project"
defaultValue={selected?.value}
text={selected?.title}
heading="Find project..."
icon={<LogoIcon className="h-3 w-3" />}
items={projects}
shortcut={{ key: "p", modifiers: ["alt"] }}
filter={(item, search, sectionTitle) =>
sectionTitle?.toLowerCase().includes(search.toLowerCase()) ||
item.title.toLowerCase().includes(search.toLowerCase())
}
>
{(matches, { shortcutsEnabled, section }) => (
<SelectGroup>
{section && <SelectGroupLabel>{section.title}</SelectGroupLabel>}
{matches?.map((match, index) => (
<SelectLinkItem
icon={<CircleStackIcon className="size-3" />}
key={match.value}
value={match.value}
to={`?${searchParams.toString()}&project=${match.value}`}
shortcut={shortcutFromIndex(index, { shortcutsEnabled, section })}
>
{match.title}
</SelectLinkItem>
))}
</SelectGroup>
)}
</Select>
);
}
@@ -134,6 +134,10 @@ const stories: Story[] = [
name: "Select",
slug: "select",
},
{
name: "Filter",
slug: "filter",
},
{
name: "Popover",
slug: "popover",
+59 -15
View File
@@ -13,6 +13,7 @@ import {
correctErrorStackTrace,
createPacketAttributesAsJson,
flattenAttributes,
NULL_SENTINEL,
isExceptionSpanEvent,
omit,
unflattenAttributes,
@@ -438,21 +439,10 @@ export class EventRepository {
return;
}
const output = isEmptyJson(fullEvent.output)
? null
: unflattenAttributes(fullEvent.output as Attributes);
const output = rehydrateJson(fullEvent.output);
const payload = rehydrateJson(fullEvent.payload);
const payload = isEmptyJson(fullEvent.payload)
? null
: unflattenAttributes(fullEvent.payload as Attributes);
const show = unflattenAttributes(
filteredAttributes(fullEvent.properties as Attributes, SemanticInternalAttributes.SHOW)
)[SemanticInternalAttributes.SHOW] as
| {
actions?: boolean;
}
| undefined;
const show = rehydrateShow(fullEvent.properties);
const properties = sanitizedAttributes(fullEvent.properties);
@@ -1046,7 +1036,7 @@ function isEmptyJson(json: Prisma.JsonValue) {
return false;
}
function sanitizedAttributes(json: Prisma.JsonValue): Record<string, unknown> | undefined {
function sanitizedAttributes(json: Prisma.JsonValue) {
if (json === null || json === undefined) {
return;
}
@@ -1143,3 +1133,57 @@ function getNowInNanoseconds(): bigint {
function getDateFromNanoseconds(nanoseconds: bigint) {
return new Date(Number(nanoseconds) / 1_000_000);
}
function rehydrateJson(json: Prisma.JsonValue): any {
if (json === null) {
return undefined;
}
if (json === NULL_SENTINEL) {
return null;
}
if (typeof json === "string") {
return json;
}
if (typeof json === "number") {
return json;
}
if (typeof json === "boolean") {
return json;
}
if (Array.isArray(json)) {
return json.map((item) => rehydrateJson(item));
}
if (typeof json === "object") {
return unflattenAttributes(json as Attributes);
}
return null;
}
function rehydrateShow(properties: Prisma.JsonValue): { actions?: boolean } | undefined {
if (properties === null || properties === undefined) {
return;
}
if (typeof properties !== "object") {
return;
}
if (Array.isArray(properties)) {
return;
}
const actions = properties[SemanticInternalAttributes.SHOW_ACTIONS];
if (typeof actions === "boolean") {
return { actions };
}
return;
}
+26 -14
View File
@@ -19,6 +19,7 @@ import {
MarQSQueuePriorityStrategy,
MessagePayload,
QueueCapacities,
QueueRange,
} from "./types";
const tracer = trace.getTracer("marqs");
@@ -563,7 +564,7 @@ export class MarQS {
parentQueue
);
const queues = await this.#zrangeWithScores(parentQueue, range[0], range[1]);
const queues = await this.#getChildQueuesWithScores(parentQueue, range);
const queuesWithScores = await this.#calculateQueueScores(queues, calculateCapacities);
@@ -574,21 +575,25 @@ export class MarQS {
selectionId
);
if (typeof choice !== "string") {
return;
}
span.setAttributes({
...flattenAttributes(queues, "marqs.queues"),
});
span.setAttributes({
...flattenAttributes(queuesWithScores, "marqs.queuesWithScores"),
});
span.setAttribute("marqs.nextRange", range);
span.setAttribute("marqs.queueCount", queues.length);
span.setAttribute("marqs.queueChoice", choice);
span.setAttribute("nextRange.offset", range.offset);
span.setAttribute("nextRange.count", range.count);
span.setAttribute("queueCount", queues.length);
return choice;
if (typeof choice !== "string") {
span.setAttribute("noQueueChoice", true);
return;
} else {
span.setAttribute("queueChoice", choice);
return choice;
}
},
{
kind: SpanKind.CONSUMER,
@@ -632,12 +637,19 @@ export class MarQS {
});
}
async #zrangeWithScores(
async #getChildQueuesWithScores(
key: string,
min: number,
max: number
range: QueueRange
): Promise<Array<{ value: string; score: number }>> {
const valuesWithScores = await this.redis.zrange(key, min, max, "WITHSCORES");
const valuesWithScores = await this.redis.zrangebyscore(
key,
"-inf",
Date.now(),
"WITHSCORES",
"LIMIT",
range.offset,
range.count
);
const result: Array<{ value: string; score: number }> = [];
for (let i = 0; i < valuesWithScores.length; i += 2) {
@@ -1537,7 +1549,7 @@ function getMarQSClient() {
return new MarQS({
keysProducer: new MarQSShortKeyProducer(KEY_PREFIX),
queuePriorityStrategy: new SimpleWeightedChoiceStrategy({ queueSelectionCount: 12 }),
queuePriorityStrategy: new SimpleWeightedChoiceStrategy({ queueSelectionCount: 36 }),
envQueuePriorityStrategy: new SimpleWeightedChoiceStrategy({ queueSelectionCount: 12 }),
workers: 1,
redis: redisOptions,
@@ -1,5 +1,10 @@
import { RedisOptions } from "ioredis";
import { MarQSQueuePriorityStrategy, PriorityStrategyChoice, QueueWithScores } from "./types";
import {
MarQSQueuePriorityStrategy,
PriorityStrategyChoice,
QueueRange,
QueueWithScores,
} from "./types";
import { nanoid } from "nanoid";
import seedrandom from "seedrandom";
@@ -26,9 +31,7 @@ export class DynamicWeightedChoiceStrategy implements MarQSQueuePriorityStrategy
throw new Error("Method not implemented.");
}
nextCandidateSelection(
parentQueue: string
): Promise<{ range: [number, number]; selectionId: string }> {
nextCandidateSelection(parentQueue: string): Promise<{ range: QueueRange; selectionId: string }> {
throw new Error("Method not implemented.");
}
}
@@ -39,13 +42,18 @@ export type SimpleWeightedChoiceStrategyOptions = {
};
export class SimpleWeightedChoiceStrategy implements MarQSQueuePriorityStrategy {
private _nextRangesByParentQueue: Map<string, [number, number]> = new Map();
private _nextRangesByParentQueue: Map<string, QueueRange> = new Map();
private _randomGenerator = seedrandom(this.options.randomSeed);
constructor(private options: SimpleWeightedChoiceStrategyOptions) {}
private nextRangeForParentQueue(parentQueue: string) {
return this._nextRangesByParentQueue.get(parentQueue) ?? [0, this.options.queueSelectionCount];
private nextRangeForParentQueue(parentQueue: string): QueueRange {
return (
this._nextRangesByParentQueue.get(parentQueue) ?? {
offset: 0,
count: this.options.queueSelectionCount,
}
);
}
chooseQueue(
@@ -55,25 +63,23 @@ export class SimpleWeightedChoiceStrategy implements MarQSQueuePriorityStrategy
): PriorityStrategyChoice {
const filteredQueues = filterQueuesAtCapacity(queues);
if (filteredQueues.length === 0) {
if (queues.length === this.options.queueSelectionCount) {
const nextRangeForParentQueue = this.nextRangeForParentQueue(parentQueue);
const nextRange: [number, number] = nextRangeForParentQueue
? [
nextRangeForParentQueue[1],
nextRangeForParentQueue[1] + this.options.queueSelectionCount,
]
: [this.options.queueSelectionCount, this.options.queueSelectionCount * 2];
// If all queues are at capacity, and we were passed the max number of queues, then we will slide the window "to the right"
this._nextRangesByParentQueue.set(parentQueue, nextRange);
} else {
this._nextRangesByParentQueue.delete(parentQueue);
}
return { abort: true };
if (queues.length === this.options.queueSelectionCount) {
const nextRangeForParentQueue = this.nextRangeForParentQueue(parentQueue);
const nextRange: QueueRange = nextRangeForParentQueue
? {
offset: nextRangeForParentQueue.offset + this.options.queueSelectionCount,
count: this.options.queueSelectionCount,
}
: { offset: this.options.queueSelectionCount, count: this.options.queueSelectionCount };
// If all queues are at capacity, and we were passed the max number of queues, then we will slide the window "to the right"
this._nextRangesByParentQueue.set(parentQueue, nextRange);
} else {
this._nextRangesByParentQueue.delete(parentQueue);
}
this._nextRangesByParentQueue.delete(parentQueue);
if (filteredQueues.length === 0) {
return { abort: true };
}
const queueWeights = this.#calculateQueueWeights(filteredQueues);
@@ -82,7 +88,7 @@ export class SimpleWeightedChoiceStrategy implements MarQSQueuePriorityStrategy
async nextCandidateSelection(
parentQueue: string
): Promise<{ range: [number, number]; selectionId: string }> {
): Promise<{ range: QueueRange; selectionId: string }> {
return { range: this.nextRangeForParentQueue(parentQueue), selectionId: nanoid(24) };
}
@@ -27,7 +27,7 @@ import { generateFriendlyId } from "../friendlyIdentifiers";
import { socketIo } from "../handleSocketIo.server";
import { findCurrentWorkerDeployment } from "../models/workerDeployment.server";
import { RestoreCheckpointService } from "../services/restoreCheckpoint.server";
import { tracer } from "../tracer.server";
import { SEMINTATTRS_FORCE_RECORDING, tracer } from "../tracer.server";
import { CrashTaskRunService } from "../services/crashTaskRun.server";
const WithTraceContext = z.object({
@@ -67,7 +67,6 @@ export type SharedQueueConsumerOptions = {
traceTimeoutSeconds?: number;
nextTickInterval?: number;
interval?: number;
parentContext?: Context;
};
export class SharedQueueConsumer {
@@ -93,7 +92,6 @@ export class SharedQueueConsumer {
traceTimeoutSeconds: options.traceTimeoutSeconds ?? 60, // 60 seconds
nextTickInterval: options.nextTickInterval ?? 1000, // 1 second
interval: options.interval ?? 100, // 100ms
parentContext: options.parentContext ?? ROOT_CONTEXT,
};
}
@@ -193,19 +191,17 @@ export class SharedQueueConsumer {
) {
this.#endCurrentSpan();
const parentContext = this._options.parentContext ?? ROOT_CONTEXT;
// Create a new trace
this._currentSpan = tracer.startSpan(
"SharedQueueConsumer.doWork()",
{
kind: SpanKind.CONSUMER,
},
parentContext
ROOT_CONTEXT
);
// Get the span trace context
this._currentSpanContext = trace.setSpan(parentContext, this._currentSpan);
this._currentSpanContext = trace.setSpan(ROOT_CONTEXT, this._currentSpan);
this._perTraceCountdown = this._options.maximumItemsPerTrace;
this._lastNewTrace = new Date();
+3 -3
View File
@@ -18,6 +18,8 @@ export type QueueWithScores = {
age: number;
};
export type QueueRange = { offset: number; count: number };
export interface MarQSKeyProducer {
queueConcurrencyLimitKey(env: AuthenticatedEnvironment, queue: string): string;
envConcurrencyLimitKey(env: AuthenticatedEnvironment): string;
@@ -69,9 +71,7 @@ export interface MarQSQueuePriorityStrategy {
*
* @returns The scores and the selectionId for the next candidate selection
*/
nextCandidateSelection(
parentQueue: string
): Promise<{ range: [number, number]; selectionId: string }>;
nextCandidateSelection(parentQueue: string): Promise<{ range: QueueRange; selectionId: string }>;
}
export const MessagePayload = z.object({
@@ -172,7 +172,7 @@ export async function createBackgroundTasks(
},
});
if (taskQueue.concurrencyLimit) {
if (typeof taskQueue.concurrencyLimit === "number") {
await marqs?.updateQueueConcurrencyLimits(
environment,
taskQueue.name,
@@ -51,11 +51,15 @@ export class UpsertTaskScheduleService extends BaseService {
});
if (!task) {
throw new Error(`Task with identifier ${schedule.taskIdentifier} not found in project.`);
throw new ServiceValidationError(
`Task with identifier ${schedule.taskIdentifier} not found in project.`
);
}
if (task.triggerSource !== "SCHEDULED") {
throw new Error(`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`);
throw new ServiceValidationError(
`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`
);
}
const result = await $transaction(this._prisma, async (tx) => {
+6 -24
View File
@@ -1,19 +1,17 @@
import { trace } from "@opentelemetry/api";
import { clientWebsocketMessages, serverWebsocketMessages } from "@trigger.dev/core/v3";
import type { StructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
import {
MessageCatalogToSocketIoEvents,
ZodMessageHandler,
ZodMessageSender,
MessageCatalogToSocketIoEvents,
} from "@trigger.dev/core/v3/zodMessageHandler";
import type { StructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
import { Evt } from "evt";
import { randomUUID } from "node:crypto";
import type { DisconnectReason, Namespace, Socket } from "socket.io";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
import { SharedQueueConsumer } from "./marqs/sharedQueueConsumer.server";
import type { DisconnectReason, Namespace, Socket } from "socket.io";
import { ROOT_CONTEXT, Span, SpanKind, trace } from "@opentelemetry/api";
import { env } from "~/env.server";
const tracer = trace.getTracer("sharedQueueConsumerPool");
interface SharedQueueConsumerPoolOptions {
sender: ZodMessageSender<typeof serverWebsocketMessages>;
@@ -22,22 +20,8 @@ interface SharedQueueConsumerPoolOptions {
class SharedQueueConsumerPool {
#consumers: SharedQueueConsumer[];
#span: Span;
constructor(opts: SharedQueueConsumerPoolOptions) {
this.#span = tracer.startSpan(
"SharedQueueConsumerPool()",
{
kind: SpanKind.CONSUMER,
attributes: {
"pool.size": opts.poolSize,
},
},
ROOT_CONTEXT
);
const spanContext = trace.setSpan(ROOT_CONTEXT, this.#span);
this.#consumers = Array(opts.poolSize)
.fill(null)
.map(
@@ -45,7 +29,6 @@ class SharedQueueConsumerPool {
new SharedQueueConsumer(opts.sender, {
interval: env.SHARED_QUEUE_CONSUMER_INTERVAL_MS,
nextTickInterval: env.SHARED_QUEUE_CONSUMER_NEXT_TICK_INTERVAL_MS,
parentContext: spanContext,
})
);
}
@@ -56,7 +39,6 @@ class SharedQueueConsumerPool {
async stop() {
await Promise.allSettled(this.#consumers.map((consumer) => consumer.stop()));
this.#span.end();
}
}
@@ -100,7 +82,7 @@ export class SharedSocketConnection {
},
});
logger.log("Starting SharedQueueConsumer pool", {
logger.debug("Starting SharedQueueConsumer pool", {
poolSize: opts.poolSize ?? this._defaultPoolSize,
});
+1 -1
View File
@@ -125,7 +125,7 @@ function getTracer() {
);
} else {
if (env.INTERNAL_OTEL_TRACE_LOGGING_ENABLED === "1") {
console.log(`🔦 Tracer: Logger exporter enabled`);
console.log(`🔦 Tracer: Logger exporter enabled (sampling = ${samplingRate})`);
const loggerExporter = new LoggerSpanExporter();
+4
View File
@@ -30,6 +30,8 @@
"/public/build"
],
"dependencies": {
"@ariakit/react": "^0.4.6",
"@ariakit/react-core": "^0.4.6",
"@aws-sdk/client-sqs": "^3.445.0",
"@codemirror/autocomplete": "^6.3.1",
"@codemirror/commands": "^6.1.2",
@@ -124,11 +126,13 @@
"lodash.omit": "^4.5.0",
"lucide-react": "^0.229.0",
"marked": "^4.0.18",
"match-sorter": "^6.3.4",
"morgan": "^1.10.0",
"nanoid": "^3.3.4",
"non.geist": "^1.0.2",
"ohash": "^1.1.3",
"openai": "^4.33.1",
"parse-duration": "^1.1.0",
"posthog-js": "^1.93.3",
"posthog-node": "^3.1.3",
"prism-react-renderer": "^1.3.5",
+64 -11
View File
@@ -88,7 +88,10 @@ describe("SimpleWeightedChoiceStrategy", () => {
const nextSelection = await stategy.nextCandidateSelection("parentQueue");
expect(nextSelection).toEqual({ range: [3, 6], selectionId: expect.any(String) });
expect(nextSelection).toEqual({
range: { offset: 3, count: 3 },
selectionId: expect.any(String),
});
// Now pass some queues that have some capacity
const chosenQueue2 = stategy.chooseQueue(
@@ -129,7 +132,10 @@ describe("SimpleWeightedChoiceStrategy", () => {
const nextSelection2 = await stategy.nextCandidateSelection("parentQueue");
expect(nextSelection2).toEqual({ range: [0, 3], selectionId: expect.any(String) });
expect(nextSelection2).toEqual({
range: { offset: 6, count: 3 },
selectionId: expect.any(String),
});
});
it("should adjust the next filter range only if passed the maximum number of queues", async () => {
@@ -167,7 +173,10 @@ describe("SimpleWeightedChoiceStrategy", () => {
const nextSelection = await stategy.nextCandidateSelection("parentQueue");
expect(nextSelection).toEqual({ range: [0, 3], selectionId: expect.any(String) });
expect(nextSelection).toEqual({
range: { offset: 0, count: 3 },
selectionId: expect.any(String),
});
});
it("should adjust the next candidate range ONLY for the matching parent queue", async () => {
@@ -182,7 +191,7 @@ describe("SimpleWeightedChoiceStrategy", () => {
queue: "queue1",
age: 4497,
capacities: {
queue: { current: 10, limit: 10 },
queue: { current: 0, limit: 10 },
env: { current: 0, limit: 10 },
org: { current: 0, limit: 10 },
},
@@ -210,15 +219,21 @@ describe("SimpleWeightedChoiceStrategy", () => {
"selectionId"
);
expect(chosenQueue).toEqual({ abort: true });
expect(chosenQueue).toEqual("queue1");
const nextSelection = await stategy.nextCandidateSelection("parentQueue2");
expect(nextSelection).toEqual({ range: [0, 3], selectionId: expect.any(String) });
expect(nextSelection).toEqual({
range: { offset: 0, count: 3 },
selectionId: expect.any(String),
});
const nextSelection2 = await stategy.nextCandidateSelection("parentQueue");
expect(nextSelection2).toEqual({ range: [3, 6], selectionId: expect.any(String) });
expect(nextSelection2).toEqual({
range: { offset: 3, count: 3 },
selectionId: expect.any(String),
});
const chosenQueue2 = stategy.chooseQueue(
[
@@ -250,14 +265,52 @@ describe("SimpleWeightedChoiceStrategy", () => {
},
},
],
"parentQueue2",
"parentQueue",
"selectionId"
);
expect(chosenQueue2).toEqual("queue3");
expect(chosenQueue2).toEqual("queue2");
const nextSelection3 = await stategy.nextCandidateSelection("parentQueue2");
const nextSelection3 = await stategy.nextCandidateSelection("parentQueue");
expect(nextSelection3).toEqual({ range: [0, 3], selectionId: expect.any(String) });
expect(nextSelection3).toEqual({
range: { offset: 6, count: 3 },
selectionId: expect.any(String),
});
// Not passed 3 queues, so the range should be reset (we've reached the end)
const chosenQueue3 = stategy.chooseQueue(
[
{
queue: "queue1",
age: 4497,
capacities: {
queue: { current: 0, limit: 10 },
env: { current: 0, limit: 10 },
org: { current: 0, limit: 10 },
},
},
{
queue: "queue2",
age: 19670,
capacities: {
queue: { current: 0, limit: 10 },
env: { current: 0, limit: 10 },
org: { current: 0, limit: 10 },
},
},
],
"parentQueue",
"selectionId"
);
expect(chosenQueue3).toEqual("queue2");
const nextSelection4 = await stategy.nextCandidateSelection("parentQueue");
expect(nextSelection4).toEqual({
range: { offset: 0, count: 3 },
selectionId: expect.any(String),
});
});
});
+17 -3
View File
@@ -16,7 +16,21 @@ export const client = new TriggerClient({
});
```
## 2. Set your environment variables on your server
## 2. Include your Jobs in `sideEffects` if needed
If your `package.json` does not declare `sideEffects`, you can safely skip this step. Otherwise, include a glob pattern that matches your jobs so that they are not tree shaken away in your production bundle.
```json
// package.json
{
"name": "my-app",
"sideEffects": [
"./src/jobs/**/*.ts"
]
}
```
## 3. Set your environment variables on your server
Ensure that your `TRIGGER_API_KEY` (or whatever you've set it to) environment variable is set on your server, to the correct value from the "Environments & API Keys" page in your Trigger.dev dashboard.
@@ -31,11 +45,11 @@ The exact instructions will vary depending on where you deploy to. Here are the
- [Cloudflare Workers](https://developers.cloudflare.com/workers/platform/environment-variables/)
- [Netlify](https://docs.netlify.com/environment-variables/overview/)
## 3. Deploy your code as usual
## 4. Deploy your code as usual
Your Job code lives in your codebase, so you can deploy it as you normally would.
## 4. Connect the Endpoint for the first time
## 5. Connect the Endpoint for the first time
1. Go to the "Environments & API Keys" page in your Trigger.dev dashboard
![Go to the Environments & API Keys page ](/images/environments-link.png)
+71 -22
View File
@@ -1,8 +1,14 @@
{
"$schema": "https://mintlify.com/schema.json",
"name": "Trigger.dev",
"openapi": ["/openapi.yml", "/v3-openapi.json"],
"versions": ["v3 (Developer Preview)", "v2"],
"openapi": [
"/openapi.yml",
"/v3-openapi.json"
],
"versions": [
"v3 (Developer Preview)",
"v2"
],
"logo": {
"dark": "/logo/dark.png",
"light": "/logo/light.png",
@@ -90,7 +96,9 @@
{
"group": "",
"version": "v3 (Developer Preview)",
"pages": ["v3/introduction"]
"pages": [
"v3/introduction"
]
},
{
"group": "Getting Started",
@@ -113,7 +121,12 @@
"v3/apikeys",
{
"group": "Task types",
"pages": ["v3/tasks-regular", "v3/tasks-scheduled", "v3/tasks-zod", "v3/tasks-webhooks"]
"pages": [
"v3/tasks-regular",
"v3/tasks-scheduled",
"v3/tasks-zod",
"v3/tasks-webhooks"
]
},
"v3/trigger-config"
]
@@ -121,7 +134,10 @@
{
"group": "Development",
"version": "v3 (Developer Preview)",
"pages": ["v3/cli-dev", "v3/run-tests"]
"pages": [
"v3/cli-dev",
"v3/run-tests"
]
},
{
"group": "Deployment",
@@ -132,7 +148,9 @@
"v3/github-actions",
{
"group": "Deployment integrations",
"pages": ["v3/vercel-integration"]
"pages": [
"v3/vercel-integration"
]
}
]
},
@@ -208,7 +226,9 @@
},
{
"group": "Objects",
"pages": ["v3/reference-context"]
"pages": [
"v3/reference-context"
]
},
{
"group": "CLI",
@@ -226,9 +246,7 @@
{
"group": "Runs API",
"pages": [
"v3/management-start-run",
"v3/management-get-run",
"v3/management-get-runs",
"v3/management-retrieve-run",
"v3/management-replay-run",
"v3/management-cancel-run"
]
@@ -259,7 +277,11 @@
{
"group": "Open source",
"version": "v3 (Developer Preview)",
"pages": ["v3/github-repo", "v3/open-source-self-hosting", "v3/open-source-contributing"]
"pages": [
"v3/github-repo",
"v3/open-source-self-hosting",
"v3/open-source-contributing"
]
},
{
"group": "Help",
@@ -460,7 +482,10 @@
"pages": [
{
"group": "Airtable",
"pages": ["integrations/apis/airtable", "integrations/apis/airtable-tasks"]
"pages": [
"integrations/apis/airtable",
"integrations/apis/airtable-tasks"
]
},
{
"group": "GitHub",
@@ -486,16 +511,25 @@
},
{
"group": "Plain",
"pages": ["integrations/apis/plain", "integrations/apis/plain-tasks"]
"pages": [
"integrations/apis/plain",
"integrations/apis/plain-tasks"
]
},
"integrations/apis/replicate",
{
"group": "SendGrid",
"pages": ["integrations/apis/sendgrid", "integrations/apis/sendgrid-tasks"]
"pages": [
"integrations/apis/sendgrid",
"integrations/apis/sendgrid-tasks"
]
},
{
"group": "Resend",
"pages": ["integrations/apis/resend", "integrations/apis/resend-tasks"]
"pages": [
"integrations/apis/resend",
"integrations/apis/resend-tasks"
]
},
{
"group": "Shopify",
@@ -507,7 +541,10 @@
},
{
"group": "Slack",
"pages": ["integrations/apis/slack", "integrations/apis/slack-tasks"]
"pages": [
"integrations/apis/slack",
"integrations/apis/slack-tasks"
]
},
"integrations/apis/stripe",
{
@@ -532,7 +569,9 @@
"sdk/triggerclient/constructor",
{
"group": "Instance properties",
"pages": ["sdk/triggerclient/store"]
"pages": [
"sdk/triggerclient/store"
]
},
{
"group": "Instance methods",
@@ -595,7 +634,10 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
}
]
},
@@ -606,7 +648,10 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
}
]
},
@@ -618,7 +663,9 @@
},
{
"group": "HTTP Reference",
"pages": ["sdk/api-reference/events/create-an-event"]
"pages": [
"sdk/api-reference/events/create-an-event"
]
},
{
"group": "React SDK",
@@ -633,7 +680,9 @@
{
"group": "Overview",
"version": "v2",
"pages": ["examples/introduction"]
"pages": [
"examples/introduction"
]
}
],
"footerSocials": {
@@ -641,4 +690,4 @@
"github": "https://github.com/triggerdotdev",
"linkedin": "https://www.linkedin.com/company/triggerdotdev"
}
}
}
+191 -2
View File
@@ -493,7 +493,7 @@
"x-codeSamples": [
{
"lang": "typescript",
"source": "const handle = await runs.replay(existingRun.id);"
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nconst handle = await runs.replay(\"run_1234\");"
},
{
"lang": "sh",
@@ -601,7 +601,7 @@
"x-codeSamples": [
{
"lang": "typescript",
"source": "await runs.cancel(existingRun.id);"
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nawait runs.cancel(\"run_1234\");"
},
{
"lang": "sh",
@@ -609,6 +609,107 @@
}
]
}
},
"/api/v3/runs/{run_id}": {
"get": {
"description": "Retrieve a run",
"parameters": [
{
"in": "path",
"name": "run_id",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of an existing run. When you trigger a run you will get an id in the response."
}
],
"responses": {
"200": {
"description": "Successful request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RetrieveRunResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Invalid or missing run ID"
]
}
}
}
}
}
},
"401": {
"description": "Unauthorized request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Invalid or Missing API key"
]
}
}
}
}
}
},
"404": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Run not found"
]
}
}
}
}
}
}
},
"tags": [
"run"
],
"security": [
{
"bearerAuth": []
}
],
"operationId": "retrieve_run_v1",
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nawait runs.retrieve(\"run_1234\");"
},
{
"lang": "sh",
"source": "curl --request GET \\\n\t--url https://api.trigger.dev/api/v3/runs/{run_id} \\\n\t--header 'Authorization: Bearer <token>'"
}
]
}
}
},
"components": {
@@ -620,6 +721,94 @@
}
},
"schemas": {
"RetrieveRunResponse": {
"type": "object",
"required": [
"id",
"status",
"taskIdentifier",
"createdAt",
"updatedAt",
"attempts"
],
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"PENDING",
"EXECUTING",
"PAUSED",
"COMPLETED",
"FAILED",
"CANCELED"
]
},
"taskIdentifier": {
"type": "string"
},
"idempotencyKey": {
"type": "string"
},
"version": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"attempts": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"status",
"createdAt",
"updatedAt"
],
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"PENDING",
"EXECUTING",
"PAUSED",
"COMPLETED",
"FAILED",
"CANCELED"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"startedAt": {
"type": "string",
"format": "date-time"
},
"completedAt": {
"type": "string",
"format": "date-time"
}
}
}
}
}
},
"CreateScheduleOptions": {
"type": "object",
"properties": {
+34
View File
@@ -10,3 +10,37 @@ When you [trigger a task](/v3/triggering) from your backend code, you need to se
Each environment has its own secret key. You can find the value on the API keys page in the Trigger.dev dashboard:
![How to find your secret key](/images/v3/api-keys.png)
### Automatically Configuring the SDK
To automatically configure the SDK with your secret key, you can set the `TRIGGER_SECRET_KEY` environment variable. The SDK will automatically use this value when calling API methods (like `trigger`).
```bash
export TRIGGER_SECRET_KEY=tr_dev_…
```
You can do the same if you are self-hosting and need to change the default URL by using `TRIGGER_API_URL`.
```bash
export TRIGGER_API_URL=https://trigger.example.com
```
The default URL is `https://api.trigger.dev`.
### Manually Configuring the SDK
If you prefer to manually configure the SDK, you can call the `configure` method:
```ts
import { configure } from "@trigger.dev/sdk/v3";
import { myTask } from "./trigger/myTasks";
configure({
secretKey: "tr_dev_1234", // WARNING: Never actually hardcode your secret key like this
baseURL: "https://mytrigger.example.com", // Optional
});
async function triggerTask() {
await myTask.trigger({ userId: "1234" }); // This will use the secret key and base URL you configured
}
```
+7 -1
View File
@@ -29,6 +29,8 @@ This task will retry 10 times with exponential backoff.
One way to gain reliability is to break your work into smaller tasks and [trigger](/v3/triggering) them from each other. Each task can have its own retrying behavior:
```ts /trigger/multiple-tasks.ts
import { task } from "@trigger.dev/sdk/v3";
export const myTask = task({
id: "my-task",
retry: {
@@ -66,7 +68,7 @@ We provide some useful functions that you can use to retry smaller parts of a ta
You can retry a block of code that can throw an error, with the same retry settings as a task.
```ts /trigger/retry-on-throw.ts
import { task, logger, retry } from "@trigger.dev/sdk/v3"
import { task, logger, retry } from "@trigger.dev/sdk/v3";
export const retryOnThrow = task({
id: "retry-on-throw",
@@ -102,6 +104,8 @@ You can use `fetch`, `axios`, or any other library in your code.
But we do provide a convenient function to perform HTTP requests with conditional retrying based on the response:
```ts /trigger/retry-fetch.ts
import { task, logger, retry } from "@trigger.dev/sdk/v3";
export const taskWithFetchRetries = task({
id: "task-with-fetch-retries",
run: async ({ payload, ctx }) => {
@@ -195,6 +199,8 @@ In this complicated example:
- If we've run out of requests or tokens we retry at the time specified in the headers.
```ts
import { task } from "@trigger.dev/sdk/v3";
export const openaiTask = task({
id: "openai-task",
retry: {
+4
View File
@@ -0,0 +1,4 @@
---
title: "Retrieve run"
openapi: "v3-openapi GET /api/v3/runs/{run_id}"
---
+37
View File
@@ -1,5 +1,42 @@
# @trigger.dev/airtable
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "Trigger.dev integration for airtable",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"airtable": "^0.12.1",
"zod": "3.22.3"
},
+37
View File
@@ -1,5 +1,42 @@
# @trigger.dev/github
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -30,8 +30,8 @@
"@octokit/request-error": "^5.0.1",
"@octokit/webhooks": "^12.0.10",
"octokit": "^3.1.2",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"zod": "3.22.3"
},
"engines": {
+37
View File
@@ -1,5 +1,42 @@
# @trigger.dev/linear
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/linear",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "Trigger.dev integration for @linear/sdk",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@linear/sdk": "^8.0.0",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"zod": "3.22.3"
},
"engines": {
+37
View File
@@ -1,5 +1,42 @@
# @trigger.dev/slack
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "The official OpenAI integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -42,8 +42,8 @@
},
"dependencies": {
"openai": "^4.16.1",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29"
},
"engines": {
"node": ">=18.0.0"
+37
View File
@@ -1,5 +1,42 @@
# @trigger.dev/plain
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "The official Plain.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+37
View File
@@ -1,5 +1,42 @@
# @trigger.dev/replicate
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/replicate",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "Trigger.dev integration for replicate",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"replicate": "^0.18.1",
"zod": "3.22.3"
},
+37
View File
@@ -1,5 +1,42 @@
# @trigger.dev/resend
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "The official Resend.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"resend": "^2.1.0"
},
"engines": {
+37
View File
@@ -1,5 +1,42 @@
# @trigger.dev/sendgrid
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "Trigger.dev integration for @sendgrid/mail",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29"
},
"engines": {
"node": ">=16.8.0"
+38
View File
@@ -1,5 +1,43 @@
# @trigger.dev/shopify
## 3.0.0-beta.29
### Patch Changes
- 1fd26ff98: improved error messages when a shopify webhook fails to register
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/shopify",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "Trigger.dev integration for @shopify/shopify-api",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@shopify/shopify-api": "^8.0.2",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"zod": "3.22.3"
},
"engines": {
+23 -1
View File
@@ -63,8 +63,30 @@ export class Shopify implements TriggerIntegration {
throw `Can't create Shopify integration (${options.id}) as apiKey was undefined`;
}
if (Object.keys(options).includes("apiSecretKey") && !options.apiSecretKey) {
throw `Can't create Shopify integration (${options.id}) as apiSecretKey was undefined`;
}
if (Object.keys(options).includes("adminAccessToken") && !options.adminAccessToken) {
throw `Can't create Shopify integration (${options.id}) as adminAccessToken was undefined`;
}
if (Object.keys(options).includes("hostName") && !options.hostName) {
throw `Can't create Shopify integration (${options.id}) as hostName was undefined`;
}
this._options = options;
this._shopDomain = this._options.hostName.replace("http://", "").replace("https://", "");
// Extract the shop domain if user has entered the full URL
this._shopDomain = this._options.hostName
.replace(/^https?:\/\//, "") // Remove http:// or https://
.replace(/\/$/, ""); // Remove trailing slash if it exists (e.g. `example.myshopify.com/`)
// Regular expression to ensure the shopDomain is a valid `.myshopify.com` domain
const shopifyDomainPattern = /^[a-zA-Z0-9-]+\.myshopify\.com$/;
if (!shopifyDomainPattern.test(this._shopDomain)) {
throw `Can't create Shopify integration (${options.id}) because hostName should be a valid ".myshopify.com" domain, not a custom primary domain. For example: my-domain.myshopify.com`;
}
}
get authSource() {
+57 -24
View File
@@ -86,21 +86,36 @@ export function createWebhookEventSource(integration: Shopify) {
key: (params) => params.topic,
crud: {
create: async ({ io, ctx }) => {
const webhook = await io.integration.rest.Webhook.save("create-webhook", {
fromData: {
address: ctx.url,
topic: ctx.params.topic,
// fields: ctx.params.fields,
},
});
try {
const webhook = await io.integration.rest.Webhook.save("create-webhook", {
fromData: {
address: ctx.url,
topic: ctx.params.topic,
// fields: ctx.params.fields,
},
});
const clientSecret = await io.integration.runTask(
"get-client-secret",
async (client) => client.config.apiSecretKey
);
if (!webhook.id) {
throw new Error(
"Failed to create webhook. Ensure your Shopfiy client configuration is correct. Have you set the correct access scopes? Are you using the primary myshopify.com domain?"
);
}
await io.store.job.set("set-id", "webhook-id", webhook.id);
await io.store.job.set("set-secret", "webhook-secret", clientSecret);
const clientSecret = await io.integration.runTask(
"get-client-secret",
async (client) => client.config.apiSecretKey
);
await io.store.job.set("set-id", "webhook-id", webhook.id);
await io.store.job.set("set-secret", "webhook-secret", clientSecret);
} catch (error) {
if (error instanceof Error) {
await io.logger.error(`Failed to create webhook: ${error.message}`);
} else {
await io.logger.error("Failed to create webhook", { rawError: error });
}
throw error;
}
},
delete: async ({ io, ctx }) => {
const webhookId = await io.store.job.get<number>("get-webhook-id", "webhook-id");
@@ -109,23 +124,41 @@ export function createWebhookEventSource(integration: Shopify) {
throw new Error("Missing webhook ID for delete operation.");
}
await io.integration.rest.Webhook.delete("delete-webhook", {
id: webhookId,
});
try {
await io.integration.rest.Webhook.delete("delete-webhook", {
id: webhookId,
});
} catch (error) {
if (error instanceof Error) {
await io.logger.error(`Failed to delete webhook: ${error.message}`);
} else {
await io.logger.error("Failed to delete webhook", { rawError: error });
}
throw error;
}
await io.store.job.delete("delete-webhook-id", "webhook-id");
},
update: async ({ io, ctx }) => {
const webhookId = await io.store.job.get<number>("get-webhook-id", "webhook-id");
await io.integration.rest.Webhook.save("update-webhook", {
fromData: {
id: webhookId,
address: ctx.url,
topic: ctx.params.topic,
// fields: ctx.params.fields,
},
});
try {
await io.integration.rest.Webhook.save("update-webhook", {
fromData: {
id: webhookId,
address: ctx.url,
topic: ctx.params.topic,
// fields: ctx.params.fields,
},
});
} catch (error) {
if (error instanceof Error) {
await io.logger.error(`Failed to update webhook: ${error.message}`);
} else {
await io.logger.error("Failed to update webhook", { rawError: error });
}
throw error;
}
},
},
verify: async ({ request, client, ctx }) => {
+32
View File
@@ -1,5 +1,37 @@
# @trigger.dev/slack
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "3.0.0-beta.24",
"version": "3.0.0-beta.29",
"description": "The official Slack integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
},
"dependencies": {
"@slack/web-api": "^6.8.1",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"zod": "3.22.3"
},
"engines": {
+37
View File
@@ -1,5 +1,42 @@
# @trigger.dev/stripe
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.29
- @trigger.dev/sdk@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [6d9dfbc75]
- @trigger.dev/sdk@3.0.0-beta.28
- @trigger.dev/integration-kit@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [203e00208]
- @trigger.dev/sdk@3.0.0-beta.27
- @trigger.dev/integration-kit@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.26
- @trigger.dev/sdk@3.0.0-beta.26
## 3.0.0-beta.25
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.25
- @trigger.dev/sdk@3.0.0-beta.25
## 3.0.0-beta.24
### Patch Changes

Some files were not shown because too many files have changed in this diff Show More