Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7df2f85a1f | |||
| f14180d13c | |||
| ab6b9514cd | |||
| 98345d67d9 | |||
| 38f5a90399 | |||
| 1b2635ae4a | |||
| 129f023d11 | |||
| 795e637dec | |||
| 5238c424fc | |||
| 1bbd7e6dc3 | |||
| 3bb82ed9a5 | |||
| ff4ff869ab | |||
| 1dcee2b338 | |||
| 0a798446c8 | |||
| 209942a63d | |||
| 57a9b35870 | |||
| a16b65f666 | |||
| 5871745a92 | |||
| 87b5dfbf11 | |||
| 5af2003516 | |||
| 85ce729bf2 | |||
| 5701d1da42 | |||
| bc61d83764 | |||
| 5bf125be0d | |||
| babe1c0e54 | |||
| 1224fceb18 | |||
| 8277f4d249 | |||
| 73cb8839a5 | |||
| 5d0a731cf6 | |||
| 25a152517e | |||
| d1092fcd2c |
@@ -0,0 +1,45 @@
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { Header3 } from "../primitives/Headers";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
RunPanelDivider,
|
||||
RunPanelIconProperty,
|
||||
RunPanelIconSection,
|
||||
} from "~/components/run/RunCard";
|
||||
import { Event } from "~/presenters/EventPresenter.server";
|
||||
|
||||
export function EventDetail({ event }: { event: Event }) {
|
||||
const { id, name, payload, context, timestamp, deliveredAt } = event;
|
||||
|
||||
return (
|
||||
<RunPanel selected={false}>
|
||||
<RunPanelBody>
|
||||
<RunPanelIconSection>
|
||||
<RunPanelIconProperty
|
||||
icon="calendar"
|
||||
label="Created"
|
||||
value={<DateTime date={timestamp} />}
|
||||
/>
|
||||
{deliveredAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Delivered"
|
||||
value={<DateTime date={deliveredAt} />}
|
||||
/>
|
||||
)}
|
||||
<RunPanelIconProperty icon="id" label="Event name" value={name} />
|
||||
<RunPanelIconProperty icon="account" label="Event ID" value={id} />
|
||||
</RunPanelIconSection>
|
||||
<RunPanelDivider />
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Header3>Payload</Header3>
|
||||
<CodeBlock code={payload} />
|
||||
<Header3>Context</Header3>
|
||||
<CodeBlock code={context} />
|
||||
</div>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { DirectionSchema, FilterableEnvironment } from "~/components/runs/RunStatuses";
|
||||
|
||||
export const EventListSearchSchema = z.object({
|
||||
cursor: z.string().optional(),
|
||||
direction: DirectionSchema.optional(),
|
||||
environment: FilterableEnvironment.optional(),
|
||||
from: z
|
||||
.string()
|
||||
.transform((value) => parseInt(value))
|
||||
.optional(),
|
||||
to: z
|
||||
.string()
|
||||
.transform((value) => parseInt(value))
|
||||
.optional(),
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../primitives/Select";
|
||||
import { EventListSearchSchema } from "./EventStatuses";
|
||||
import { environmentKeys, FilterableEnvironment } from "~/components/runs/RunStatuses";
|
||||
import { TimeFrameFilter } from "../runs/TimeFrameFilter";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
|
||||
export function EventsFilters() {
|
||||
const navigate = useNavigate();
|
||||
const location = useOptimisticLocation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const { environment, from, to } = EventListSearchSchema.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 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 handleEnvironmentChange = (value: FilterableEnvironment | "ALL") => {
|
||||
handleFilterChange("environment", value === "ALL" ? undefined : value);
|
||||
};
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
searchParams.delete("status");
|
||||
searchParams.delete("environment");
|
||||
searchParams.delete("from");
|
||||
searchParams.delete("to");
|
||||
navigate(`${location.pathname}?${searchParams.toString()}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between gap-x-2">
|
||||
<SelectGroup>
|
||||
<Select
|
||||
name="environment"
|
||||
value={environment ?? "ALL"}
|
||||
onValueChange={handleEnvironmentChange}
|
||||
>
|
||||
<SelectTrigger size="secondary/small" width="full">
|
||||
<SelectValue placeholder={"Select environment"} className="ml-2 p-0" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"ALL"}>
|
||||
<Paragraph variant="extra-small" className="pl-0.5">
|
||||
All environments
|
||||
</Paragraph>
|
||||
</SelectItem>
|
||||
{environmentKeys.map((env) => (
|
||||
<SelectItem key={env} value={env}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<EnvironmentLabel environment={{ type: env }} />
|
||||
<Paragraph variant="extra-small">environment</Paragraph>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
|
||||
<TimeFrameFilter from={from} to={to} onRangeChanged={handleTimeFrameChange} />
|
||||
|
||||
<Button variant="tertiary/small" onClick={() => clearFilters()} LeadingIcon={"close"}>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { StopIcon } from "@heroicons/react/24/outline";
|
||||
import { CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { RuntimeEnvironmentType, User } from "@trigger.dev/database";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellChevron,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "../primitives/Table";
|
||||
|
||||
type EventTableItem = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
environment: {
|
||||
type: RuntimeEnvironmentType;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
};
|
||||
createdAt: Date | null;
|
||||
isTest: boolean;
|
||||
deliverAt: Date | null;
|
||||
deliveredAt: Date | null;
|
||||
cancelledAt: Date | null;
|
||||
runs: number;
|
||||
};
|
||||
|
||||
type EventsTableProps = {
|
||||
total: number;
|
||||
hasFilters: boolean;
|
||||
events: EventTableItem[];
|
||||
isLoading?: boolean;
|
||||
eventsParentPath: string;
|
||||
currentUser: User;
|
||||
};
|
||||
|
||||
export function EventsTable({
|
||||
total,
|
||||
hasFilters,
|
||||
events,
|
||||
isLoading = false,
|
||||
eventsParentPath,
|
||||
currentUser,
|
||||
}: EventsTableProps) {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Event</TableHeaderCell>
|
||||
<TableHeaderCell>Env</TableHeaderCell>
|
||||
<TableHeaderCell>Received Time</TableHeaderCell>
|
||||
<TableHeaderCell>Delivery Time</TableHeaderCell>
|
||||
<TableHeaderCell>Delivered</TableHeaderCell>
|
||||
<TableHeaderCell>Canceled Time</TableHeaderCell>
|
||||
<TableHeaderCell>Test</TableHeaderCell>
|
||||
<TableHeaderCell>Runs</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
<span className="sr-only">Go to page</span>
|
||||
</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{total === 0 && !hasFilters ? (
|
||||
<TableBlankRow colSpan={9}>
|
||||
<NoEvents title="No events found" />
|
||||
</TableBlankRow>
|
||||
) : events.length === 0 ? (
|
||||
<TableBlankRow colSpan={9}>
|
||||
<NoEvents title="No events match your filters" />
|
||||
</TableBlankRow>
|
||||
) : (
|
||||
events.map((event) => {
|
||||
const path = `${eventsParentPath}/events/${event.id}`;
|
||||
const usernameForEnv =
|
||||
currentUser.id !== event.environment.userId ? event.environment.userName : undefined;
|
||||
|
||||
return (
|
||||
<TableRow key={event.id}>
|
||||
<TableCell to={path}>{typeof event.name === "string" ? event.name : "-"}</TableCell>
|
||||
<TableCell to={path}>
|
||||
<EnvironmentLabel environment={event.environment} userName={usernameForEnv} />
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{event.createdAt ? <DateTime date={event.createdAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{event.deliverAt ? <DateTime date={event.deliverAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{event.deliveredAt ? <DateTime date={event.deliveredAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{event.cancelledAt ? <DateTime date={event.cancelledAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{event.isTest ? (
|
||||
<CheckIcon className="h-4 w-4 text-slate-400" />
|
||||
) : (
|
||||
<StopIcon className="h-4 w-4 text-slate-850" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>{event.runs}</TableCell>
|
||||
<TableCellChevron to={path} isSticky />
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{isLoading && (
|
||||
<TableBlankRow
|
||||
colSpan={8}
|
||||
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-slate-900/90"
|
||||
>
|
||||
<Spinner /> <span className="text-dimmed">Loading…</span>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function NoEvents({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Paragraph className="w-auto">{title}</Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { User } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { accountPath, personalAccessTokensPath, rootPath } from "~/utils/pathBuilder";
|
||||
import { Header3 } from "../primitives/Headers";
|
||||
import { ArrowLeftIcon, ChevronLeftIcon } from "@heroicons/react/24/solid";
|
||||
import { SideMenuHeader } from "./SideMenuHeader";
|
||||
import { SideMenuItem } from "./SideMenuItem";
|
||||
import { DiscordIcon } from "@trigger.dev/companyicons";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||
import { useV3Enabled } from "~/root";
|
||||
|
||||
export function AccountSideMenu({ user }: { user: User }) {
|
||||
const v3Enabled = useV3Enabled();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-col gap-y-8 overflow-hidden border-r border-ui-border transition"
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div
|
||||
className={cn("flex items-center justify-between border-b bg-background p-px transition")}
|
||||
>
|
||||
<LinkButton
|
||||
variant="tertiary/medium"
|
||||
LeadingIcon={ArrowLeftIcon}
|
||||
to={rootPath()}
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
Account
|
||||
</LinkButton>
|
||||
</div>
|
||||
<div className="h-full overflow-hidden overflow-y-auto pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<div className="mb-6 flex flex-col gap-1 px-1">
|
||||
<SideMenuHeader title={user.name ?? user.displayName ?? user.email} />
|
||||
|
||||
<SideMenuItem
|
||||
name="Your profile"
|
||||
icon="account"
|
||||
iconColor="text-indigo-500"
|
||||
to={accountPath()}
|
||||
data-action="account"
|
||||
/>
|
||||
</div>
|
||||
{v3Enabled && (
|
||||
<div className="mb-1 flex flex-col gap-1 px-1">
|
||||
<SideMenuHeader title="Security" />
|
||||
<SideMenuItem
|
||||
name="Personal Access Tokens"
|
||||
icon={ShieldCheckIcon}
|
||||
iconColor="text-emerald-500"
|
||||
to={personalAccessTokensPath()}
|
||||
data-action="tokens"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 border-t border-border p-1">
|
||||
<SideMenuItem
|
||||
name="Join our Discord"
|
||||
icon={DiscordIcon}
|
||||
to="https://trigger.dev/discord"
|
||||
data-action="join our discord"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<SideMenuItem
|
||||
name="Documentation"
|
||||
icon="docs"
|
||||
to="https://trigger.dev/docs"
|
||||
data-action="documentation"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Changelog"
|
||||
icon="star"
|
||||
to="https://trigger.dev/changelog"
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon="log"
|
||||
data-action="help & feedback"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
Help & Feedback
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,16 +3,16 @@ import {
|
||||
ArrowRightIcon,
|
||||
ArrowRightOnRectangleIcon,
|
||||
ChartBarIcon,
|
||||
CursorArrowRaysIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { IconExclamationCircle } from "@tabler/icons-react";
|
||||
import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
|
||||
import { AnchorHTMLAttributes, Fragment, useEffect, useRef, useState } from "react";
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { MatchedOrganization } from "~/hooks/useOrganizations";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { MatchedProject } from "~/hooks/useProject";
|
||||
import { User } from "~/models/user.server";
|
||||
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
@@ -27,7 +27,9 @@ import {
|
||||
organizationIntegrationsPath,
|
||||
organizationPath,
|
||||
organizationTeamPath,
|
||||
personalAccessTokensPath,
|
||||
projectEnvironmentsPath,
|
||||
projectEventsPath,
|
||||
projectHttpEndpointsPath,
|
||||
projectPath,
|
||||
projectRunsPath,
|
||||
@@ -40,11 +42,10 @@ import { LogoIcon } from "../LogoIcon";
|
||||
import { StepContentContainer } from "../StepContentContainer";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { FreePlanUsage } from "../billing/FreePlanUsage";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
|
||||
import { Icon } from "../primitives/Icon";
|
||||
import { type IconNames } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
Popover,
|
||||
@@ -55,7 +56,9 @@ import {
|
||||
PopoverSectionHeader,
|
||||
} from "../primitives/Popover";
|
||||
import { StepNumber } from "../primitives/StepNumber";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { MenuCount, SideMenuItem } from "./SideMenuItem";
|
||||
import { SideMenuHeader } from "./SideMenuHeader";
|
||||
import { useV3Enabled } from "~/root";
|
||||
|
||||
type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
|
||||
type SideMenuProject = Pick<
|
||||
@@ -144,6 +147,12 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
data-action="triggers"
|
||||
hasWarning={project.hasInactiveExternalTriggers}
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Events"
|
||||
icon={CursorArrowRaysIcon}
|
||||
iconColor="text-sky-500"
|
||||
to={projectEventsPath(organization, project)}
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="HTTP endpoints"
|
||||
icon="http-endpoint"
|
||||
@@ -372,6 +381,7 @@ function ProjectSelector({
|
||||
function UserMenu({ user }: { user: SideMenuUser }) {
|
||||
const [isProfileMenuOpen, setProfileMenuOpen] = useState(false);
|
||||
const navigation = useNavigation();
|
||||
const v3Enabled = useV3Enabled();
|
||||
|
||||
useEffect(() => {
|
||||
setProfileMenuOpen(false);
|
||||
@@ -409,6 +419,14 @@ function UserMenu({ user }: { user: SideMenuUser }) {
|
||||
icon={UserProfilePhoto}
|
||||
leadingIconClassName="text-indigo-500"
|
||||
/>
|
||||
{v3Enabled && (
|
||||
<PopoverMenuItem
|
||||
to={personalAccessTokensPath()}
|
||||
title="Personal Access Tokens"
|
||||
icon={ShieldCheckIcon}
|
||||
leadingIconClassName="text-emerald-500"
|
||||
/>
|
||||
)}
|
||||
<PopoverMenuItem
|
||||
to={logoutPath()}
|
||||
title="Log out"
|
||||
@@ -421,99 +439,3 @@ function UserMenu({ user }: { user: SideMenuUser }) {
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function SideMenuHeader({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
const [isHeaderMenuOpen, setHeaderMenuOpen] = useState(false);
|
||||
const navigation = useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderMenuOpen(false);
|
||||
}, [navigation.location?.pathname]);
|
||||
|
||||
return (
|
||||
<div className="group flex items-center justify-between pl-1.5">
|
||||
<Paragraph
|
||||
variant="extra-extra-small/caps"
|
||||
className="cursor-default truncate text-slate-500"
|
||||
>
|
||||
{title}
|
||||
</Paragraph>
|
||||
<Popover onOpenChange={(open) => setHeaderMenuOpen(open)} open={isHeaderMenuOpen}>
|
||||
<PopoverCustomTrigger className="p-1">
|
||||
<EllipsisHorizontalIcon className="h-4 w-4 text-slate-500 transition group-hover:text-bright" />
|
||||
</PopoverCustomTrigger>
|
||||
<PopoverContent
|
||||
className="min-w-max overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
align="start"
|
||||
>
|
||||
<div className="flex flex-col gap-1 p-1">{children}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SideMenuItem({
|
||||
icon,
|
||||
iconColor,
|
||||
name,
|
||||
to,
|
||||
hasWarning,
|
||||
count,
|
||||
target,
|
||||
subItem = false,
|
||||
}: {
|
||||
icon?: IconNames | React.ComponentType<any>;
|
||||
iconColor?: string;
|
||||
name: string;
|
||||
to: string;
|
||||
hasWarning?: string | boolean;
|
||||
count?: number;
|
||||
target?: AnchorHTMLAttributes<HTMLAnchorElement>["target"];
|
||||
subItem?: boolean;
|
||||
}) {
|
||||
const pathName = usePathName();
|
||||
const isActive = pathName === to;
|
||||
|
||||
return (
|
||||
<LinkButton
|
||||
variant={subItem ? "small-menu-sub-item" : "small-menu-item"}
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
LeadingIcon={icon}
|
||||
leadingIconClassName={isActive ? iconColor : "text-dimmed"}
|
||||
to={to}
|
||||
target={target}
|
||||
className={cn(
|
||||
"text-bright group-hover:bg-slate-850",
|
||||
subItem ? "text-dimmed" : "",
|
||||
isActive ? "bg-slate-850 text-bright" : "group-hover:text-bright"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{name}
|
||||
<div className="flex items-center gap-1">
|
||||
{count !== undefined && count > 0 && <MenuCount count={count} />}
|
||||
{typeof hasWarning === "string" ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-1 border border-rose-500 bg-rose-500/20 backdrop-blur-xl">
|
||||
{hasWarning}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
hasWarning && <Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuCount({ count }: { count: number | string }) {
|
||||
return <div className="rounded-full bg-slate-900 px-2 py-1 text-xxs text-dimmed">{count}</div>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Popover, PopoverContent, PopoverCustomTrigger } from "../primitives/Popover";
|
||||
import { EllipsisHorizontalIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export function SideMenuHeader({ title, children }: { title: string; children?: React.ReactNode }) {
|
||||
const [isHeaderMenuOpen, setHeaderMenuOpen] = useState(false);
|
||||
const navigation = useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderMenuOpen(false);
|
||||
}, [navigation.location?.pathname]);
|
||||
|
||||
return (
|
||||
<div className="group flex items-center justify-between pl-1.5">
|
||||
<Paragraph
|
||||
variant="extra-extra-small/caps"
|
||||
className="cursor-default truncate text-slate-500"
|
||||
>
|
||||
{title}
|
||||
</Paragraph>
|
||||
{children !== undefined ? (
|
||||
<Popover onOpenChange={(open) => setHeaderMenuOpen(open)} open={isHeaderMenuOpen}>
|
||||
<PopoverCustomTrigger className="p-1">
|
||||
<EllipsisHorizontalIcon className="h-4 w-4 text-slate-500 transition group-hover:text-bright" />
|
||||
</PopoverCustomTrigger>
|
||||
<PopoverContent
|
||||
className="min-w-max overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
align="start"
|
||||
>
|
||||
<div className="flex flex-col gap-1 p-1">{children}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { AnchorHTMLAttributes } from "react";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { IconNames } from "../primitives/NamedIcon";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { Icon } from "../primitives/Icon";
|
||||
import { IconExclamationCircle } from "@tabler/icons-react";
|
||||
|
||||
export function SideMenuItem({
|
||||
icon,
|
||||
iconColor,
|
||||
name,
|
||||
to,
|
||||
hasWarning,
|
||||
count,
|
||||
target,
|
||||
subItem = false,
|
||||
}: {
|
||||
icon?: IconNames | React.ComponentType<any>;
|
||||
iconColor?: string;
|
||||
name: string;
|
||||
to: string;
|
||||
hasWarning?: string | boolean;
|
||||
count?: number;
|
||||
target?: AnchorHTMLAttributes<HTMLAnchorElement>["target"];
|
||||
subItem?: boolean;
|
||||
}) {
|
||||
const pathName = usePathName();
|
||||
const isActive = pathName === to;
|
||||
|
||||
return (
|
||||
<LinkButton
|
||||
variant={subItem ? "small-menu-sub-item" : "small-menu-item"}
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
LeadingIcon={icon}
|
||||
leadingIconClassName={isActive ? iconColor : "text-dimmed"}
|
||||
to={to}
|
||||
target={target}
|
||||
className={cn(
|
||||
"text-bright group-hover:bg-slate-850",
|
||||
subItem ? "text-dimmed" : "",
|
||||
isActive ? "bg-slate-850 text-bright" : "group-hover:text-bright"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{name}
|
||||
<div className="flex items-center gap-1">
|
||||
{count !== undefined && count > 0 && <MenuCount count={count} />}
|
||||
{typeof hasWarning === "string" ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-1 border border-rose-500 bg-rose-500/20 backdrop-blur-xl">
|
||||
{hasWarning}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
hasWarning && <Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function MenuCount({ count }: { count: number | string }) {
|
||||
return <div className="rounded-full bg-slate-900 px-2 py-1 text-xxs text-dimmed">{count}</div>;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const ClientTabs = TabsPrimitive.Root;
|
||||
|
||||
@@ -48,4 +49,47 @@ const ClientTabsContent = React.forwardRef<
|
||||
));
|
||||
ClientTabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export type TabsProps = {
|
||||
tabs: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
currentValue: string;
|
||||
className?: string;
|
||||
layoutId: string;
|
||||
};
|
||||
|
||||
export function ClientTabsWithUnderline({ className, tabs, currentValue, layoutId }: TabsProps) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
className={cn(`flex flex-row gap-x-6 border-b border-slate-700`, className)}
|
||||
>
|
||||
{tabs.map((tab, index) => {
|
||||
const isActive = currentValue === tab.value;
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className={cn(`group flex flex-col items-center`, className)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm transition duration-200",
|
||||
isActive ? "text-indigo-500" : "text-slate-200"
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</span>
|
||||
{isActive ? (
|
||||
<motion.div layoutId={layoutId} className="mt-1 h-0.5 w-full bg-indigo-500" />
|
||||
) : (
|
||||
<div className="mt-1 h-0.5 w-full bg-slate-500 opacity-0 transition duration-200 group-hover:opacity-100" />
|
||||
)}
|
||||
</TabsPrimitive.Trigger>
|
||||
);
|
||||
})}
|
||||
</TabsPrimitive.List>
|
||||
);
|
||||
}
|
||||
|
||||
export { ClientTabs, ClientTabsList, ClientTabsTrigger, ClientTabsContent };
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { CalendarDateTime, createCalendar } from "@internationalized/date";
|
||||
import { useDateField, useDateSegment } from "@react-aria/datepicker";
|
||||
import type { DateFieldState, DateSegment } from "@react-stately/datepicker";
|
||||
import { useDateFieldState } from "@react-stately/datepicker";
|
||||
import { Granularity } from "@react-types/datepicker";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { useLocales } from "./LocaleProvider";
|
||||
import { Button } from "./Buttons";
|
||||
|
||||
type DateFieldProps = {
|
||||
label?: string;
|
||||
defaultValue?: Date;
|
||||
minValue?: Date;
|
||||
maxValue?: Date;
|
||||
className?: string;
|
||||
fieldClassName?: string;
|
||||
granularity: Granularity;
|
||||
showGuide?: boolean;
|
||||
showNowButton?: boolean;
|
||||
showClearButton?: boolean;
|
||||
onValueChange?: (value: Date | undefined) => void;
|
||||
};
|
||||
|
||||
export function DateField({
|
||||
label,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
minValue,
|
||||
maxValue,
|
||||
granularity,
|
||||
className,
|
||||
fieldClassName,
|
||||
showGuide = false,
|
||||
showNowButton = false,
|
||||
showClearButton = false,
|
||||
}: DateFieldProps) {
|
||||
const [value, setValue] = useState<undefined | CalendarDateTime>(
|
||||
utcDateToCalendarDate(defaultValue)
|
||||
);
|
||||
|
||||
const state = useDateFieldState({
|
||||
value: value,
|
||||
onChange: (value) => {
|
||||
if (value) {
|
||||
setValue(value);
|
||||
onValueChange?.(value.toDate("utc"));
|
||||
}
|
||||
},
|
||||
minValue: utcDateToCalendarDate(minValue),
|
||||
maxValue: utcDateToCalendarDate(maxValue),
|
||||
shouldForceLeadingZeros: true,
|
||||
granularity,
|
||||
locale: "en-US",
|
||||
createCalendar: (name: string) => {
|
||||
return createCalendar(name);
|
||||
},
|
||||
});
|
||||
|
||||
//if the passed in value changes, we should update the date
|
||||
useEffect(() => {
|
||||
if (state.value === undefined && defaultValue === undefined) return;
|
||||
|
||||
const calendarDate = utcDateToCalendarDate(defaultValue);
|
||||
//unchanged
|
||||
if (state.value?.toDate("utc").getTime() === defaultValue?.getTime()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(calendarDate);
|
||||
}, [defaultValue]);
|
||||
|
||||
const ref = useRef<null | HTMLDivElement>(null);
|
||||
const { labelProps, fieldProps } = useDateField(
|
||||
{
|
||||
label,
|
||||
},
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
//render if reverse date order
|
||||
const yearSegment = state.segments.find((s) => s.type === "year")!;
|
||||
const monthSegment = state.segments.find((s) => s.type === "month")!;
|
||||
const daySegment = state.segments.find((s) => s.type === "day")!;
|
||||
const hourSegment = state.segments.find((s) => s.type === "hour")!;
|
||||
const minuteSegment = state.segments.find((s) => s.type === "minute")!;
|
||||
const secondSegment = state.segments.find((s) => s.type === "second")!;
|
||||
const dayPeriodSegment = state.segments.find((s) => s.type === "dayPeriod")!;
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col items-start ${className || ""}`}>
|
||||
<span {...labelProps} className="mb-1 ml-0.5 text-xs text-slate-300">
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div
|
||||
{...fieldProps}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex rounded-sm border border-slate-800 bg-midnight-900 p-0.5 px-1.5 transition-colors focus-within:border-slate-500 hover:border-slate-700 focus-within:hover:border-slate-500",
|
||||
fieldClassName
|
||||
)}
|
||||
>
|
||||
<DateSegment segment={yearSegment} state={state} />
|
||||
<DateSegment segment={literalSegment("/")} state={state} />
|
||||
<DateSegment segment={monthSegment} state={state} />
|
||||
<DateSegment segment={literalSegment("/")} state={state} />
|
||||
<DateSegment segment={daySegment} state={state} />
|
||||
<DateSegment segment={literalSegment(", ")} state={state} />
|
||||
<DateSegment segment={hourSegment} state={state} />
|
||||
<DateSegment segment={literalSegment(":")} state={state} />
|
||||
<DateSegment segment={minuteSegment} state={state} />
|
||||
<DateSegment segment={literalSegment(":")} state={state} />
|
||||
<DateSegment segment={secondSegment} state={state} />
|
||||
<DateSegment segment={literalSegment(" ")} state={state} />
|
||||
<DateSegment segment={dayPeriodSegment} state={state} />
|
||||
</div>
|
||||
{showNowButton && (
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
onClick={() => {
|
||||
const now = new Date();
|
||||
setValue(utcDateToCalendarDate(new Date()));
|
||||
onValueChange?.(now);
|
||||
}}
|
||||
>
|
||||
Now
|
||||
</Button>
|
||||
)}
|
||||
{showClearButton && (
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
LeadingIcon={"close"}
|
||||
onClick={() => {
|
||||
setValue(undefined);
|
||||
onValueChange?.(undefined);
|
||||
state.clearSegment("year");
|
||||
state.clearSegment("month");
|
||||
state.clearSegment("day");
|
||||
state.clearSegment("hour");
|
||||
state.clearSegment("minute");
|
||||
state.clearSegment("second");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showGuide && (
|
||||
<div className="mt-1 flex px-2">
|
||||
{state.segments.map((segment, i) => (
|
||||
<DateSegmentGuide key={i} segment={segment} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function utcDateToCalendarDate(date?: Date) {
|
||||
return date
|
||||
? new CalendarDateTime(
|
||||
date.getUTCFullYear(),
|
||||
date.getUTCMonth(),
|
||||
date.getUTCDate(),
|
||||
date.getUTCHours(),
|
||||
date.getUTCMinutes(),
|
||||
date.getUTCSeconds()
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
type DateSegmentProps = {
|
||||
segment: DateSegment;
|
||||
state: DateFieldState;
|
||||
};
|
||||
|
||||
function DateSegment({ segment, state }: DateSegmentProps) {
|
||||
const ref = useRef<null | HTMLDivElement>(null);
|
||||
const { segmentProps } = useDateSegment(segment, state, ref);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...segmentProps}
|
||||
ref={ref}
|
||||
style={{
|
||||
...segmentProps.style,
|
||||
minWidth: minWidthForSegment(segment),
|
||||
}}
|
||||
className={`group box-content rounded-sm px-0.5 text-right text-sm tabular-nums outline-none focus:bg-indigo-500 focus:text-white ${
|
||||
!segment.isEditable ? "text-slate-500" : "text-bright"
|
||||
}`}
|
||||
>
|
||||
{/* Always reserve space for the placeholder, to prevent layout shift when editing. */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="block text-center italic text-slate-500 group-focus:text-white"
|
||||
style={{
|
||||
visibility: segment.isPlaceholder ? undefined : "hidden",
|
||||
height: segment.isPlaceholder ? "" : 0,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{segment.placeholder}
|
||||
</span>
|
||||
{segment.isPlaceholder ? "" : segment.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function literalSegment(text: string): DateSegment {
|
||||
return {
|
||||
type: "literal",
|
||||
text,
|
||||
isPlaceholder: false,
|
||||
isEditable: false,
|
||||
placeholder: "",
|
||||
};
|
||||
}
|
||||
|
||||
function minWidthForSegment(segment: DateSegment) {
|
||||
if (segment.type === "literal") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return String(`${segment.maxValue}`).length + "ch";
|
||||
}
|
||||
|
||||
function DateSegmentGuide({ segment }: { segment: DateSegment }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minWidth: minWidthForSegment(segment),
|
||||
}}
|
||||
className={`group box-content rounded-sm px-0.5 text-right text-sm tabular-nums outline-none ${
|
||||
!segment.isEditable ? "text-slate-500" : "text-bright"
|
||||
}`}
|
||||
>
|
||||
<span className="block text-center italic text-slate-500">
|
||||
{segment.type !== "literal" ? segment.placeholder : segment.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import type { IconNamesOrString } from "./NamedIcon";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { Icon, RenderIcon } from "./Icon";
|
||||
|
||||
const variants = {
|
||||
large: {
|
||||
@@ -44,7 +45,7 @@ const variants = {
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
variant?: keyof typeof variants;
|
||||
icon?: IconNamesOrString;
|
||||
icon?: RenderIcon;
|
||||
shortcut?: string;
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
@@ -59,7 +60,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<div className={cn("relative", fullWidth ? "w-full" : "max-w-max")}>
|
||||
{icon && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center">
|
||||
<NamedIcon name={icon} className={cn(iconClassName, "text-dimmed")} />
|
||||
<Icon icon={icon} className={cn(iconClassName, "text-dimmed")} />
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
|
||||
@@ -21,7 +21,7 @@ const PopoverContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
avoidCollisions={true}
|
||||
className={cn(
|
||||
"z-50 min-w-max rounded-md border bg-midnight-850 p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 min-w-max rounded-md border border-slate-700 bg-midnight-850 p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -56,7 +56,7 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-max overflow-hidden rounded-md bg-popover text-bright shadow-md animate-in fade-in-40",
|
||||
"relative z-50 min-w-max overflow-hidden rounded-md border border-slate-700 bg-popover text-bright shadow-md animate-in fade-in-40",
|
||||
position === "popper" && "translate-y-1",
|
||||
className
|
||||
)}
|
||||
@@ -65,7 +65,7 @@ const SelectContent = React.forwardRef<
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"border border-slate-800 px-1 py-0",
|
||||
"px-1 py-0",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
|
||||
@@ -122,10 +122,26 @@ type TableCellProps = TableCellBasicProps & {
|
||||
to?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
hasAction?: boolean;
|
||||
isSticky?: boolean;
|
||||
};
|
||||
|
||||
const stickyStyles =
|
||||
"sticky right-0 z-10 w-[2.8rem] min-w-[2.8rem] bg-background before:absolute before:pointer-events-none before:-left-8 before:top-0 before:h-full before:min-w-[2rem] before:bg-gradient-to-r before:from-transparent before:to-background before:content-[''] group-hover:before:to-slate-900";
|
||||
|
||||
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
({ className, alignment = "left", children, colSpan, to, onClick, hasAction = false }, ref) => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
alignment = "left",
|
||||
children,
|
||||
colSpan,
|
||||
to,
|
||||
onClick,
|
||||
hasAction = false,
|
||||
isSticky = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
let alignmentClassName = "text-left";
|
||||
switch (alignment) {
|
||||
case "center":
|
||||
@@ -154,6 +170,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
? "cursor-pointer group-hover:bg-slate-900"
|
||||
: "px-4 py-3 align-middle",
|
||||
!to && !onClick && alignmentClassName,
|
||||
isSticky && stickyStyles,
|
||||
className
|
||||
)}
|
||||
colSpan={colSpan}
|
||||
@@ -174,9 +191,6 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
}
|
||||
);
|
||||
|
||||
const stickyStyles =
|
||||
"sticky right-0 z-10 w-[2.8rem] min-w-[2.8rem] bg-background before:absolute before:pointer-events-none before:-left-8 before:top-0 before:h-full before:min-w-[2rem] before:bg-gradient-to-r before:from-transparent before:to-background before:content-[''] group-hover:before:to-slate-900";
|
||||
|
||||
export const TableCellChevron = forwardRef<
|
||||
HTMLTableCellElement,
|
||||
{
|
||||
@@ -189,7 +203,8 @@ export const TableCellChevron = forwardRef<
|
||||
>(({ className, to, children, isSticky, onClick }, ref) => {
|
||||
return (
|
||||
<TableCell
|
||||
className={cn(isSticky && stickyStyles, className)}
|
||||
className={className}
|
||||
isSticky={isSticky}
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
@@ -213,7 +228,8 @@ export const TableCellMenu = forwardRef<
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<TableCell
|
||||
className={cn(isSticky && stickyStyles, className)}
|
||||
className={className}
|
||||
isSticky={isSticky}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
ExclamationTriangleIcon,
|
||||
NoSymbolIcon,
|
||||
PauseCircleIcon,
|
||||
XCircleIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../primitives/Select";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import {
|
||||
FilterableEnvironment,
|
||||
FilterableStatus,
|
||||
RunListSearchSchema,
|
||||
environmentKeys,
|
||||
statusKeys,
|
||||
} from "./RunStatuses";
|
||||
import { TimeFrameFilter } from "./TimeFrameFilter";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export function RunsFilters() {
|
||||
const navigate = useNavigate();
|
||||
const location = useOptimisticLocation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const { environment, status, from, to } = RunListSearchSchema.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: FilterableStatus | "ALL") => {
|
||||
handleFilterChange("status", value === "ALL" ? undefined : value);
|
||||
}, []);
|
||||
|
||||
const handleEnvironmentChange = useCallback((value: FilterableEnvironment | "ALL") => {
|
||||
handleFilterChange("environment", 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("status");
|
||||
searchParams.delete("environment");
|
||||
searchParams.delete("from");
|
||||
searchParams.delete("to");
|
||||
navigate(`${location.pathname}?${searchParams.toString()}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between gap-x-2">
|
||||
<SelectGroup>
|
||||
<Select
|
||||
name="environment"
|
||||
value={environment ?? "ALL"}
|
||||
onValueChange={handleEnvironmentChange}
|
||||
>
|
||||
<SelectTrigger size="secondary/small" width="full">
|
||||
<SelectValue placeholder={"Select environment"} className="ml-2 p-0" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"ALL"}>
|
||||
<Paragraph variant="extra-small" className="pl-0.5">
|
||||
All environments
|
||||
</Paragraph>
|
||||
</SelectItem>
|
||||
{environmentKeys.map((env) => (
|
||||
<SelectItem key={env} value={env}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<EnvironmentLabel environment={{ type: env }} />
|
||||
<Paragraph variant="extra-small">environment</Paragraph>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
|
||||
<SelectGroup>
|
||||
<Select name="status" value={status ?? "ALL"} onValueChange={handleStatusChange}>
|
||||
<SelectTrigger size="secondary/small" width="full">
|
||||
<SelectValue placeholder="Select status" className="ml-2 p-0" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"ALL"}>
|
||||
<Paragraph variant="extra-small" className="pl-0.5">
|
||||
All statuses
|
||||
</Paragraph>
|
||||
</SelectItem>
|
||||
{statusKeys.map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{
|
||||
<span className="flex items-center gap-1 text-xs">
|
||||
<FilterStatusIcon status={status} className="h-4 w-4" />
|
||||
<FilterStatusLabel status={status} />
|
||||
</span>
|
||||
}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
|
||||
<TimeFrameFilter from={from} to={to} onRangeChanged={handleTimeFrameChange} />
|
||||
|
||||
<Button variant="tertiary/small" onClick={() => clearFilters()} LeadingIcon={"close"}>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterStatusLabel({ status }: { status: FilterableStatus }) {
|
||||
return <span className={filterStatusClassNameColor(status)}>{filterStatusTitle(status)}</span>;
|
||||
}
|
||||
|
||||
export function FilterStatusIcon({
|
||||
status,
|
||||
className,
|
||||
}: {
|
||||
status: FilterableStatus;
|
||||
className: string;
|
||||
}) {
|
||||
switch (status) {
|
||||
case "COMPLETED":
|
||||
return <CheckCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "WAITING":
|
||||
return <ClockIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "QUEUED":
|
||||
return <PauseCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "IN_PROGRESS":
|
||||
return <Spinner className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "TIMEDOUT":
|
||||
return (
|
||||
<ExclamationTriangleIcon className={cn(filterStatusClassNameColor(status), className)} />
|
||||
);
|
||||
case "CANCELED":
|
||||
return <NoSymbolIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "FAILED":
|
||||
return <XCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function filterStatusTitle(status: FilterableStatus): string {
|
||||
switch (status) {
|
||||
case "QUEUED":
|
||||
return "Queued";
|
||||
case "IN_PROGRESS":
|
||||
return "In progress";
|
||||
case "WAITING":
|
||||
return "Waiting";
|
||||
case "COMPLETED":
|
||||
return "Completed";
|
||||
case "FAILED":
|
||||
return "Failed";
|
||||
case "CANCELED":
|
||||
return "Canceled";
|
||||
case "TIMEDOUT":
|
||||
return "Timed out";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function filterStatusClassNameColor(status: FilterableStatus): string {
|
||||
switch (status) {
|
||||
case "QUEUED":
|
||||
return "text-slate-500";
|
||||
case "IN_PROGRESS":
|
||||
return "text-blue-500";
|
||||
case "WAITING":
|
||||
return "text-blue-500";
|
||||
case "COMPLETED":
|
||||
return "text-green-500";
|
||||
case "FAILED":
|
||||
return "text-rose-500";
|
||||
case "CANCELED":
|
||||
return "text-slate-500";
|
||||
case "TIMEDOUT":
|
||||
return "text-amber-300";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type { JobRunStatus } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import { z } from "zod";
|
||||
|
||||
export function RunStatus({ status }: { status: JobRunStatus }) {
|
||||
return (
|
||||
@@ -127,3 +128,52 @@ export function runStatusClassNameColor(status: JobRunStatus): string {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const DirectionSchema = z.union([z.literal("forward"), z.literal("backward")]);
|
||||
export type Direction = z.infer<typeof DirectionSchema>;
|
||||
|
||||
export const FilterableStatus = z.union([
|
||||
z.literal("QUEUED"),
|
||||
z.literal("IN_PROGRESS"),
|
||||
z.literal("WAITING"),
|
||||
z.literal("COMPLETED"),
|
||||
z.literal("FAILED"),
|
||||
z.literal("TIMEDOUT"),
|
||||
z.literal("CANCELED"),
|
||||
]);
|
||||
export type FilterableStatus = z.infer<typeof FilterableStatus>;
|
||||
|
||||
export const FilterableEnvironment = z.union([
|
||||
z.literal("DEVELOPMENT"),
|
||||
z.literal("STAGING"),
|
||||
z.literal("PRODUCTION"),
|
||||
]);
|
||||
export type FilterableEnvironment = z.infer<typeof FilterableEnvironment>;
|
||||
export const environmentKeys: FilterableEnvironment[] = ["DEVELOPMENT", "STAGING", "PRODUCTION"];
|
||||
|
||||
export const RunListSearchSchema = z.object({
|
||||
cursor: z.string().optional(),
|
||||
direction: DirectionSchema.optional(),
|
||||
status: FilterableStatus.optional(),
|
||||
environment: FilterableEnvironment.optional(),
|
||||
from: z
|
||||
.string()
|
||||
.transform((value) => parseInt(value))
|
||||
.optional(),
|
||||
to: z
|
||||
.string()
|
||||
.transform((value) => parseInt(value))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const filterableStatuses: Record<FilterableStatus, JobRunStatus[]> = {
|
||||
QUEUED: ["QUEUED", "WAITING_TO_EXECUTE", "PENDING", "WAITING_ON_CONNECTIONS"],
|
||||
IN_PROGRESS: ["STARTED", "EXECUTING", "PREPROCESSING"],
|
||||
WAITING: ["WAITING_TO_CONTINUE"],
|
||||
COMPLETED: ["SUCCESS"],
|
||||
FAILED: ["FAILURE", "UNRESOLVED_AUTH", "INVALID_PAYLOAD", "ABORTED"],
|
||||
TIMEDOUT: ["TIMED_OUT"],
|
||||
CANCELED: ["CANCELED"],
|
||||
};
|
||||
|
||||
export const statusKeys: FilterableStatus[] = Object.keys(filterableStatuses) as FilterableStatus[];
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Calendar,
|
||||
CalendarDateTime,
|
||||
DateValue,
|
||||
getLocalTimeZone,
|
||||
today,
|
||||
} from "@internationalized/date";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { ClientTabs, ClientTabsContent, ClientTabsWithUnderline } from "../primitives/ClientTabs";
|
||||
import { formatDateTime } from "../primitives/DateTime";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover";
|
||||
import { DateField } from "../primitives/DateField";
|
||||
import { useLocales } from "../primitives/LocaleProvider";
|
||||
import { createCalendar } from "@internationalized/date";
|
||||
|
||||
type RunTimeFrameFilterProps = {
|
||||
from?: number;
|
||||
to?: number;
|
||||
onRangeChanged: (range: { from?: number; to?: number }) => void;
|
||||
};
|
||||
|
||||
type Mode = "absolute" | "relative";
|
||||
|
||||
export function TimeFrameFilter({ from, to, onRangeChanged }: RunTimeFrameFilterProps) {
|
||||
const [activeTab, setActiveTab] = useState<Mode>("absolute");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [relativeTimeSeconds, setRelativeTimeSeconds] = useState<number | undefined>();
|
||||
|
||||
const fromDate = from ? new Date(from) : undefined;
|
||||
const toDate = to ? new Date(to) : undefined;
|
||||
|
||||
const relativeTimeFrameChanged = useCallback((value: number) => {
|
||||
const to = new Date().getTime();
|
||||
const from = to - value;
|
||||
onRangeChanged({ from, to });
|
||||
setRelativeTimeSeconds(value);
|
||||
}, []);
|
||||
|
||||
const absoluteTimeFrameChanged = useCallback(({ from, to }: { from?: Date; to?: Date }) => {
|
||||
setRelativeTimeSeconds(undefined);
|
||||
const fromTime = from?.getTime();
|
||||
const toTime = to?.getTime();
|
||||
onRangeChanged({ from: fromTime, to: toTime });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={(open) => setIsOpen(open)} open={isOpen} modal>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
className="bg-slate-800 group-hover:bg-tertiary-foreground"
|
||||
>
|
||||
<Paragraph variant="extra-small" className="mr-2">
|
||||
{title(from, to, relativeTimeSeconds)}
|
||||
</Paragraph>
|
||||
<ChevronDownIcon className="h-4 w-4 text-bright" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent align="start" className="bg-popover p-2">
|
||||
<ClientTabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as Mode)}
|
||||
className="p-1"
|
||||
>
|
||||
<ClientTabsWithUnderline
|
||||
tabs={[
|
||||
{ label: "Absolute", value: "absolute" },
|
||||
{ label: "Relative", value: "relative" },
|
||||
]}
|
||||
currentValue={activeTab}
|
||||
layoutId={"time-tabs"}
|
||||
/>
|
||||
<ClientTabsContent value={"absolute"}>
|
||||
<AbsoluteTimeFrame
|
||||
from={fromDate}
|
||||
to={toDate}
|
||||
onValueChange={absoluteTimeFrameChanged}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"relative"}>
|
||||
<RelativeTimeFrame
|
||||
value={relativeTimeSeconds}
|
||||
onValueChange={relativeTimeFrameChanged}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function title(
|
||||
from: number | undefined,
|
||||
to: number | undefined,
|
||||
relativeTimeSeconds: number | undefined
|
||||
): string {
|
||||
if (!from && !to) {
|
||||
return "All time periods";
|
||||
}
|
||||
|
||||
if (relativeTimeSeconds !== undefined) {
|
||||
return timeFrameValues.find((t) => t.value === relativeTimeSeconds)?.label ?? "Timeframe";
|
||||
}
|
||||
|
||||
let fromString = from ? formatDateTime(new Date(from), "UTC", ["en-US"], false, true) : undefined;
|
||||
let toString = to ? formatDateTime(new Date(to), "UTC", ["en-US"], false, true) : undefined;
|
||||
if (from && !to) {
|
||||
return `From ${fromString} (UTC)`;
|
||||
}
|
||||
|
||||
if (!from && to) {
|
||||
return `To ${toString} (UTC)`;
|
||||
}
|
||||
|
||||
return `${fromString} - ${toString} (UTC)`;
|
||||
}
|
||||
|
||||
function RelativeTimeFrame({
|
||||
value,
|
||||
onValueChange,
|
||||
}: {
|
||||
value?: number;
|
||||
onValueChange: (value: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-1 pt-2">
|
||||
{timeFrameValues.map((timeframe) => (
|
||||
<Button
|
||||
key={timeframe.value}
|
||||
variant={value === timeframe.value ? "primary/small" : "tertiary/small"}
|
||||
className={cn(
|
||||
"w-full",
|
||||
value !== timeframe.value && "border border-slate-700 group-hover:bg-slate-700"
|
||||
)}
|
||||
onClick={() => {
|
||||
onValueChange(timeframe.value);
|
||||
}}
|
||||
>
|
||||
<Paragraph variant="extra-small">{timeframe.label}</Paragraph>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const timeFrameValues = [
|
||||
{
|
||||
label: "5 mins",
|
||||
value: 5 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "15 mins",
|
||||
value: 15 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "30 mins",
|
||||
value: 30 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "1 hour",
|
||||
value: 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "3 hours",
|
||||
value: 3 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "6 hours",
|
||||
value: 6 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "1 day",
|
||||
value: 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "3 days",
|
||||
value: 3 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "7 days",
|
||||
value: 7 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "10 days",
|
||||
value: 10 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "14 days",
|
||||
value: 14 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "30 days",
|
||||
value: 30 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
];
|
||||
|
||||
export type RelativeTimeFrameItem = (typeof timeFrameValues)[number];
|
||||
|
||||
function AbsoluteTimeFrame({
|
||||
from,
|
||||
to,
|
||||
onValueChange,
|
||||
}: {
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
onValueChange: (value: { from?: Date; to?: Date }) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 pt-2">
|
||||
<div className="flex flex-col justify-start gap-2">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<DateField
|
||||
label="From (UTC)"
|
||||
defaultValue={from}
|
||||
onValueChange={(value) => {
|
||||
onValueChange({ from: value, to: to });
|
||||
}}
|
||||
granularity="second"
|
||||
showNowButton
|
||||
showClearButton
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<DateField
|
||||
label="To (UTC)"
|
||||
defaultValue={to}
|
||||
onValueChange={(value) => {
|
||||
onValueChange({ from: from, to: value });
|
||||
}}
|
||||
granularity="second"
|
||||
showNowButton
|
||||
showClearButton
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -67,6 +67,9 @@ const EnvironmentSchema = z.object({
|
||||
|
||||
TUNNEL_HOST: z.string().optional(),
|
||||
TUNNEL_SECRET_KEY: z.string().optional(),
|
||||
|
||||
//v3
|
||||
V3_ENABLED: z.string().default("false"),
|
||||
});
|
||||
|
||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useLocation, useNavigation } from "@remix-run/react";
|
||||
|
||||
export function useOptimisticLocation() {
|
||||
const navigation = useNavigation();
|
||||
const location = useLocation();
|
||||
|
||||
if (navigation.state === "idle" || !navigation.location) {
|
||||
return location;
|
||||
}
|
||||
|
||||
return navigation.location;
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Direction, FilterableEnvironment } from "~/components/runs/RunStatuses";
|
||||
import { getUsername } from "~/utils/username";
|
||||
|
||||
type EventListOptions = {
|
||||
userId: string;
|
||||
organizationSlug: string;
|
||||
projectSlug: string;
|
||||
direction?: Direction;
|
||||
filterEnvironment?: FilterableEnvironment;
|
||||
cursor?: string;
|
||||
pageSize?: number;
|
||||
from?: number;
|
||||
to?: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
export type EventList = Awaited<ReturnType<EventListPresenter["call"]>>;
|
||||
|
||||
export class EventListPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
userId,
|
||||
organizationSlug,
|
||||
projectSlug,
|
||||
filterEnvironment,
|
||||
direction = "forward",
|
||||
cursor,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
from,
|
||||
to,
|
||||
}: EventListOptions) {
|
||||
const directionMultiplier = direction === "forward" ? 1 : -1;
|
||||
|
||||
// Find the organization that the user is a member of
|
||||
const organization = await this.#prismaClient.organization.findFirstOrThrow({
|
||||
where: {
|
||||
slug: organizationSlug,
|
||||
members: { some: { userId } },
|
||||
},
|
||||
});
|
||||
|
||||
// Find the project scoped to the organization
|
||||
const project = await this.#prismaClient.project.findFirstOrThrow({
|
||||
where: {
|
||||
slug: projectSlug,
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Find all runtimeEnvironments that the user has access to
|
||||
const environments = await this.#prismaClient.runtimeEnvironment.findMany({
|
||||
where: {
|
||||
projectId: project.id,
|
||||
},
|
||||
});
|
||||
|
||||
const events = await this.#prismaClient.eventRecord.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
deliverAt: true,
|
||||
deliveredAt: true,
|
||||
isTest: true,
|
||||
createdAt: true,
|
||||
cancelledAt: true,
|
||||
environment: {
|
||||
select: {
|
||||
type: true,
|
||||
slug: true,
|
||||
orgMember: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
displayName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
runs: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
internal: false,
|
||||
name: {
|
||||
notIn: ["trigger.scheduled", "dev.trigger.scheduled"],
|
||||
},
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
environmentId: {
|
||||
in: environments.map((environment) => environment.id),
|
||||
},
|
||||
environment: filterEnvironment ? { type: filterEnvironment } : undefined,
|
||||
createdAt: {
|
||||
gte: from ? new Date(from).toISOString() : undefined,
|
||||
lte: to ? new Date(to).toISOString() : undefined,
|
||||
},
|
||||
},
|
||||
orderBy: [{ id: "desc" }],
|
||||
//take an extra record to tell if there are more
|
||||
take: directionMultiplier * (pageSize + 1),
|
||||
//skip the cursor if there is one
|
||||
skip: cursor ? 1 : 0,
|
||||
cursor: cursor
|
||||
? {
|
||||
id: cursor,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const hasMore = events.length > pageSize;
|
||||
|
||||
//get cursors for next and previous pages
|
||||
let next: string | undefined;
|
||||
let previous: string | undefined;
|
||||
switch (direction) {
|
||||
case "forward":
|
||||
previous = cursor ? events.at(0)?.id : undefined;
|
||||
if (hasMore) {
|
||||
next = events[pageSize - 1]?.id;
|
||||
}
|
||||
break;
|
||||
case "backward":
|
||||
if (hasMore) {
|
||||
previous = events[1]?.id;
|
||||
next = events[pageSize]?.id;
|
||||
} else {
|
||||
next = events[pageSize - 1]?.id;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const eventsToReturn =
|
||||
direction === "backward" && hasMore
|
||||
? events.slice(1, pageSize + 1)
|
||||
: events.slice(0, pageSize);
|
||||
|
||||
return {
|
||||
events: eventsToReturn.map((event) => ({
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
deliverAt: event.deliverAt,
|
||||
deliveredAt: event.deliveredAt,
|
||||
createdAt: event.createdAt,
|
||||
cancelledAt: event.cancelledAt,
|
||||
isTest: event.isTest,
|
||||
environment: {
|
||||
type: event.environment.type,
|
||||
slug: event.environment.slug,
|
||||
userId: event.environment.orgMember?.user.id,
|
||||
userName: getUsername(event.environment.orgMember?.user),
|
||||
},
|
||||
runs: event.runs.length,
|
||||
})),
|
||||
pagination: {
|
||||
next,
|
||||
previous,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
|
||||
export type Event = NonNullable<Awaited<ReturnType<EventPresenter["call"]>>>;
|
||||
|
||||
export class EventPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
userId,
|
||||
projectSlug,
|
||||
organizationSlug,
|
||||
eventId,
|
||||
}: {
|
||||
userId: string;
|
||||
projectSlug: string;
|
||||
organizationSlug: string;
|
||||
eventId: string;
|
||||
}) {
|
||||
// Find the organization that the user is a member of
|
||||
const organization = await this.#prismaClient.organization.findFirstOrThrow({
|
||||
where: {
|
||||
slug: organizationSlug,
|
||||
members: { some: { userId } },
|
||||
},
|
||||
});
|
||||
|
||||
// Find the project scoped to the organization
|
||||
const project = await this.#prismaClient.project.findFirstOrThrow({
|
||||
where: {
|
||||
slug: projectSlug,
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
const event = await this.#prismaClient.eventRecord.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
payload: true,
|
||||
context: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
},
|
||||
where: {
|
||||
id: eventId,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Error("Could not find Event");
|
||||
}
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
timestamp: event.timestamp,
|
||||
payload: JSON.stringify(event.payload, null, 2),
|
||||
context: JSON.stringify(event.context, null, 2),
|
||||
deliveredAt: event.deliveredAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Direction,
|
||||
FilterableEnvironment,
|
||||
FilterableStatus,
|
||||
filterableStatuses,
|
||||
} from "~/components/runs/RunStatuses";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { DirectionSchema } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route";
|
||||
import { getUsername } from "~/utils/username";
|
||||
|
||||
export type Direction = z.infer<typeof DirectionSchema>;
|
||||
|
||||
type RunListOptions = {
|
||||
userId: string;
|
||||
eventId?: string;
|
||||
jobSlug?: string;
|
||||
organizationSlug: string;
|
||||
projectSlug: string;
|
||||
direction?: Direction;
|
||||
filterStatus?: JobRunStatus[];
|
||||
filterEnvironment?: RuntimeEnvironmentType;
|
||||
filterStatus?: FilterableStatus;
|
||||
filterEnvironment?: FilterableEnvironment;
|
||||
cursor?: string;
|
||||
pageSize?: number;
|
||||
from?: number;
|
||||
to?: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
@@ -31,6 +36,7 @@ export class RunListPresenter {
|
||||
|
||||
public async call({
|
||||
userId,
|
||||
eventId,
|
||||
jobSlug,
|
||||
organizationSlug,
|
||||
projectSlug,
|
||||
@@ -39,7 +45,11 @@ export class RunListPresenter {
|
||||
direction = "forward",
|
||||
cursor,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
from,
|
||||
to,
|
||||
}: RunListOptions) {
|
||||
const filterStatuses = filterStatus ? filterableStatuses[filterStatus] : undefined;
|
||||
|
||||
const directionMultiplier = direction === "forward" ? 1 : -1;
|
||||
|
||||
// Find the organization that the user is a member of
|
||||
@@ -74,6 +84,10 @@ export class RunListPresenter {
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const event = eventId
|
||||
? await this.#prismaClient.eventRecord.findUnique({ where: { id: eventId } })
|
||||
: undefined;
|
||||
|
||||
const runs = await this.#prismaClient.jobRun.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
@@ -114,14 +128,19 @@ export class RunListPresenter {
|
||||
},
|
||||
},
|
||||
where: {
|
||||
eventId: event?.id,
|
||||
jobId: job?.id,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
environmentId: {
|
||||
in: environments.map((environment) => environment.id),
|
||||
},
|
||||
status: filterStatus ? { in: filterStatus } : undefined,
|
||||
status: filterStatuses ? { in: filterStatuses } : undefined,
|
||||
environment: filterEnvironment ? { type: filterEnvironment } : undefined,
|
||||
startedAt: {
|
||||
gte: from ? new Date(from).toISOString() : undefined,
|
||||
lte: to ? new Date(to).toISOString() : undefined,
|
||||
},
|
||||
},
|
||||
orderBy: [{ id: "desc" }],
|
||||
//take an extra record to tell if there are more
|
||||
|
||||
@@ -2,7 +2,8 @@ import { TriggerSource, User } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { Direction, RunList, RunListPresenter } from "./RunListPresenter.server";
|
||||
import { RunList, RunListPresenter } from "./RunListPresenter.server";
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
|
||||
export class TriggerSourcePresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Direction } from "./RunListPresenter.server";
|
||||
|
||||
type RunListOptions = {
|
||||
userId: string;
|
||||
|
||||
@@ -2,9 +2,9 @@ import { User, Webhook } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { Direction } from "./RunListPresenter.server";
|
||||
import { organizationPath, projectPath } from "~/utils/pathBuilder";
|
||||
import { WebhookDeliveryListPresenter } from "./WebhookDeliveryListPresenter.server";
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
|
||||
export class WebhookDeliveryPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -2,8 +2,9 @@ import { User, Webhook } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { Direction, RunListPresenter } from "./RunListPresenter.server";
|
||||
import { RunListPresenter } from "./RunListPresenter.server";
|
||||
import { organizationPath, projectPath } from "~/utils/pathBuilder";
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
|
||||
export class WebhookSourcePresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { ErrorBoundary as HighlightErrorBoundary } from "@highlight-run/react";
|
||||
import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import type { ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
|
||||
import {
|
||||
Links,
|
||||
LiveReload,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useMatches,
|
||||
} from "@remix-run/react";
|
||||
import { metaV1 } from "@remix-run/v1-meta";
|
||||
import { TypedMetaFunction, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ExternalScripts } from "remix-utils/external-scripts";
|
||||
@@ -16,6 +24,7 @@ import { env } from "./env.server";
|
||||
import { featuresForRequest } from "./features.server";
|
||||
import { useHighlight } from "./hooks/useHighlight";
|
||||
import { usePostHog } from "./hooks/usePostHog";
|
||||
import { useTypedMatchesData } from "./hooks/useTypedMatchData";
|
||||
import { getUser } from "./services/session.server";
|
||||
import { appEnvTitleTag } from "./utils";
|
||||
|
||||
@@ -28,14 +37,24 @@ export const meta: TypedMetaFunction<typeof loader> = (args) => {
|
||||
title: `Trigger.dev${appEnvTitleTag(args.data?.appEnv)}`,
|
||||
charset: "utf-8",
|
||||
viewport: "width=1024, initial-scale=1",
|
||||
robots: args.data.features.isManagedCloud ? "index, follow" : "noindex, nofollow",
|
||||
});
|
||||
};
|
||||
|
||||
export function useV3Enabled() {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: "root",
|
||||
});
|
||||
|
||||
return routeMatch?.v3Enabled ?? false;
|
||||
}
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
const toastMessage = session.get("toastMessage") as ToastMessage;
|
||||
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
|
||||
const highlightProjectId = env.HIGHLIGHT_PROJECT_ID;
|
||||
const v3Enabled = env.V3_ENABLED === "true";
|
||||
const features = featuresForRequest(request);
|
||||
|
||||
return typedjson(
|
||||
@@ -47,6 +66,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
features,
|
||||
appEnv: env.APP_ENV,
|
||||
appOrigin: env.APP_ORIGIN,
|
||||
v3Enabled,
|
||||
},
|
||||
{ headers: { "Set-Cookie": await commitSession(session) } }
|
||||
);
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function Integrations() {
|
||||
</PageHeader>
|
||||
|
||||
<PageBody scrollable={false}>
|
||||
<div className="grid h-full max-w-full grid-cols-[2fr_3fr] gap-4 divide-x divide-slate-900 overflow-hidden">
|
||||
<div className="grid h-full max-w-full grid-cols-[2fr_3fr] divide-x divide-slate-900 overflow-hidden">
|
||||
<PossibleIntegrationsList
|
||||
options={options}
|
||||
organizationId={organization.id}
|
||||
@@ -142,7 +142,7 @@ function PossibleIntegrationsList({
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<div className="py-4 pl-4">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Header2 className="mb-2">Connect an API</Header2>
|
||||
<Switch
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { PageHeader, PageTitle, PageTitleRow } from "~/components/primitives/PageHeader";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { EventParamSchema, projectEventsPath, projectPath } from "~/utils/pathBuilder";
|
||||
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { EventDetail } from "~/components/event/EventDetail";
|
||||
import { EventPresenter } from "~/presenters/EventPresenter.server";
|
||||
import { useTypedMatchData } from "~/hooks/useTypedMatchData";
|
||||
import { Fragment } from "react";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { RunListSearchSchema } from "~/components/runs/RunStatuses";
|
||||
import { RunListPresenter } from "~/presenters/RunListPresenter.server";
|
||||
import { RunsTable } from "~/components/runs/RunsTable";
|
||||
import { RunsFilters } from "~/components/runs/RunFilters";
|
||||
import { ListPagination } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/ListPagination";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { eventParam, projectParam, organizationSlug } = EventParamSchema.parse(params);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const s = Object.fromEntries(url.searchParams.entries());
|
||||
const searchParams = RunListSearchSchema.parse(s);
|
||||
|
||||
const presenter = new EventPresenter();
|
||||
try {
|
||||
const event = await presenter.call({
|
||||
userId,
|
||||
projectSlug: projectParam,
|
||||
organizationSlug,
|
||||
eventId: eventParam,
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
const runsPresenter = new RunListPresenter();
|
||||
|
||||
const list = await runsPresenter.call({
|
||||
userId,
|
||||
filterEnvironment: searchParams.environment,
|
||||
filterStatus: searchParams.status,
|
||||
eventId: event.id,
|
||||
projectSlug: projectParam,
|
||||
organizationSlug,
|
||||
direction: searchParams.direction,
|
||||
cursor: searchParams.cursor,
|
||||
from: searchParams.from,
|
||||
to: searchParams.to,
|
||||
});
|
||||
|
||||
return typedjson({ event, list });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new Response(e instanceof Error ? e.message : JSON.stringify(e), { status: 404 });
|
||||
}
|
||||
};
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => {
|
||||
const eventData = useTypedMatchData<typeof loader>(match);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{eventData && eventData.event && (
|
||||
<BreadcrumbLink to={match.pathname} title={eventData.event.name} />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { event, list } = useTypedLoaderData<typeof loader>();
|
||||
const navigation = useNavigation();
|
||||
const isLoading = navigation.state !== "idle";
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitleRow>
|
||||
<PageTitle
|
||||
title={event.name}
|
||||
backButton={{
|
||||
to: projectEventsPath(organization, project),
|
||||
text: "Events",
|
||||
}}
|
||||
/>
|
||||
</PageTitleRow>
|
||||
</PageHeader>
|
||||
|
||||
<PageBody scrollable={false}>
|
||||
<div className="grid h-full grid-cols-2">
|
||||
<div className="overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<EventDetail event={event} />
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
<RunsFilters />
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<ListPagination list={list} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RunsTable
|
||||
total={list.runs.length}
|
||||
hasFilters={false}
|
||||
runs={list.runs}
|
||||
isLoading={isLoading}
|
||||
showJob={true}
|
||||
runsParentPath={projectPath(organization, project)}
|
||||
currentUser={user}
|
||||
/>
|
||||
<ListPagination list={list} className="mt-2 justify-end" />
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import {
|
||||
PageButtons,
|
||||
PageDescription,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
PageTitleRow,
|
||||
} from "~/components/primitives/PageHeader";
|
||||
import { EventsTable } from "~/components/events/EventsTable";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { EventListPresenter } from "~/presenters/EventListPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { ProjectParamSchema, docsPath, projectPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/ListPagination";
|
||||
import { EventListSearchSchema } from "~/components/events/EventStatuses";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { EventsFilters } from "~/components/events/EventsFilters";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam, organizationSlug } = ProjectParamSchema.parse(params);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const s = Object.fromEntries(url.searchParams.entries());
|
||||
const searchParams = EventListSearchSchema.parse(s);
|
||||
|
||||
const presenter = new EventListPresenter();
|
||||
const list = await presenter.call({
|
||||
userId,
|
||||
filterEnvironment: searchParams.environment,
|
||||
projectSlug: projectParam,
|
||||
organizationSlug,
|
||||
direction: searchParams.direction,
|
||||
cursor: searchParams.cursor,
|
||||
from: searchParams.from,
|
||||
to: searchParams.to,
|
||||
pageSize: 25,
|
||||
});
|
||||
|
||||
return typedjson({
|
||||
list,
|
||||
});
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { list } = useTypedLoaderData<typeof loader>();
|
||||
const navigation = useNavigation();
|
||||
const isLoading = navigation.state !== "idle";
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitleRow>
|
||||
<PageTitle title={`${project.name} events`} />
|
||||
<PageButtons>
|
||||
<LinkButton
|
||||
LeadingIcon={"docs"}
|
||||
to={docsPath("documentation/concepts/triggers/events")}
|
||||
variant="secondary/small"
|
||||
>
|
||||
Event documentation
|
||||
</LinkButton>
|
||||
</PageButtons>
|
||||
</PageTitleRow>
|
||||
<PageDescription>All events in this project</PageDescription>
|
||||
</PageHeader>
|
||||
|
||||
<PageBody scrollable={false}>
|
||||
<div className="h-full overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
<EventsFilters />
|
||||
<ListPagination list={list} />
|
||||
</div>
|
||||
<EventsTable
|
||||
total={list.events.length}
|
||||
hasFilters={false}
|
||||
events={list.events}
|
||||
isLoading={isLoading}
|
||||
eventsParentPath={projectPath(organization, project)}
|
||||
currentUser={user}
|
||||
/>
|
||||
<ListPagination list={list} className="mt-2 justify-end" />
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
|
||||
import { Handle } from "~/utils/handle";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={match.pathname} title="Events" />,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <Outlet />;
|
||||
}
|
||||
+9
-9
@@ -1,16 +1,16 @@
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Direction, RunList } from "~/presenters/RunListPresenter.server";
|
||||
import { WebhookDeliveryList } from "~/presenters/WebhookDeliveryListPresenter.server";
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function ListPagination({
|
||||
list,
|
||||
className,
|
||||
}: {
|
||||
list: RunList | WebhookDeliveryList;
|
||||
className?: string;
|
||||
}) {
|
||||
type List = {
|
||||
pagination: {
|
||||
next: string | undefined;
|
||||
previous: string | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export function ListPagination({ list, className }: { list: List; className?: string }) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1", className)}>
|
||||
<PreviousButton cursor={list.pagination.previous} />
|
||||
|
||||
+13
-10
@@ -20,13 +20,8 @@ import {
|
||||
organizationIntegrationsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "./ListPagination";
|
||||
|
||||
export const DirectionSchema = z.union([z.literal("forward"), z.literal("backward")]);
|
||||
|
||||
export const RunListSearchSchema = z.object({
|
||||
cursor: z.string().optional(),
|
||||
direction: DirectionSchema.optional(),
|
||||
});
|
||||
import { RunListSearchSchema } from "~/components/runs/RunStatuses";
|
||||
import { RunsFilters } from "~/components/runs/RunFilters";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -39,11 +34,15 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const presenter = new RunListPresenter();
|
||||
const list = await presenter.call({
|
||||
userId,
|
||||
filterEnvironment: searchParams.environment,
|
||||
filterStatus: searchParams.status,
|
||||
jobSlug: jobParam,
|
||||
projectSlug: projectParam,
|
||||
organizationSlug,
|
||||
direction: searchParams.direction,
|
||||
cursor: searchParams.cursor,
|
||||
from: searchParams.from,
|
||||
to: searchParams.to,
|
||||
});
|
||||
|
||||
return typedjson({
|
||||
@@ -73,10 +72,14 @@ export default function Page() {
|
||||
{(open) => (
|
||||
<div className={cn("grid h-fit gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-end gap-x-2">
|
||||
<HelpTrigger title="How do I run my Job?" />
|
||||
<ListPagination list={list} />
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
<RunsFilters />
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<HelpTrigger title="How do I run my Job?" />
|
||||
<ListPagination list={list} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RunsTable
|
||||
total={list.runs.length}
|
||||
hasFilters={false}
|
||||
|
||||
+7
-279
@@ -18,67 +18,8 @@ import { RunListPresenter } from "~/presenters/RunListPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { ProjectParamSchema, docsPath, projectPath } from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/ListPagination";
|
||||
import { RunListSearchSchema } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/primitives/Select";
|
||||
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
ExclamationTriangleIcon,
|
||||
PauseCircleIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { ChartBarIcon } from "@heroicons/react/20/solid";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { NoSymbolIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
// Filter -> status types
|
||||
const ExtendedJobRunStatus = {
|
||||
ALL: "ALL" as const,
|
||||
...JobRunStatus,
|
||||
} as const;
|
||||
type ExtendedJobRunStatusKey = keyof typeof ExtendedJobRunStatus;
|
||||
|
||||
type FilterableStatus =
|
||||
| "ALL"
|
||||
| "QUEUED"
|
||||
| "IN_PROGRESS"
|
||||
| "WAITING"
|
||||
| "COMPLETED"
|
||||
| "FAILED"
|
||||
| "CANCELED"
|
||||
| "TIMEDOUT";
|
||||
|
||||
const filterableStatuses: Record<FilterableStatus, ExtendedJobRunStatusKey[]> = {
|
||||
ALL: ["ALL"],
|
||||
QUEUED: ["QUEUED", "WAITING_TO_EXECUTE", "PENDING", "WAITING_ON_CONNECTIONS"],
|
||||
IN_PROGRESS: ["STARTED", "EXECUTING", "PREPROCESSING"],
|
||||
WAITING: ["WAITING_TO_CONTINUE"],
|
||||
COMPLETED: ["SUCCESS"],
|
||||
FAILED: ["FAILURE", "UNRESOLVED_AUTH", "INVALID_PAYLOAD", "ABORTED"],
|
||||
TIMEDOUT: ["TIMED_OUT"],
|
||||
CANCELED: ["CANCELED"],
|
||||
};
|
||||
|
||||
const statusKeys: FilterableStatus[] = Object.keys(filterableStatuses) as FilterableStatus[];
|
||||
|
||||
// Filter -> Environment types
|
||||
const ExtendedRuntimeEnvironment = {
|
||||
ALL: "ALL" as const,
|
||||
...RuntimeEnvironmentType,
|
||||
} as const;
|
||||
type ExtendedRuntimeEnvironmentType = keyof typeof ExtendedRuntimeEnvironment;
|
||||
const environmentKeys: ExtendedRuntimeEnvironmentType[] = Object.keys(
|
||||
ExtendedRuntimeEnvironment
|
||||
) as ExtendedRuntimeEnvironmentType[];
|
||||
import { RunListSearchSchema } from "~/components/runs/RunStatuses";
|
||||
import { RunsFilters } from "~/components/runs/RunFilters";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -88,34 +29,19 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const s = Object.fromEntries(url.searchParams.entries());
|
||||
const searchParams = RunListSearchSchema.parse(s);
|
||||
|
||||
const status = url.searchParams.get("status");
|
||||
const environment = url.searchParams.get("environment");
|
||||
|
||||
let filterStatus: JobRunStatus[] | undefined;
|
||||
if (status && status !== "ALL") {
|
||||
if (filterableStatuses.hasOwnProperty(status)) {
|
||||
filterStatus = filterableStatuses[status as FilterableStatus] as JobRunStatus[];
|
||||
}
|
||||
}
|
||||
|
||||
let filterEnvironment: RuntimeEnvironmentType | undefined;
|
||||
if (environment && environment !== "ALL") {
|
||||
if (environmentKeys.includes(environment)) {
|
||||
filterEnvironment = environment as RuntimeEnvironmentType;
|
||||
}
|
||||
}
|
||||
|
||||
const presenter = new RunListPresenter();
|
||||
|
||||
const list = await presenter.call({
|
||||
userId,
|
||||
filterEnvironment: filterEnvironment,
|
||||
filterStatus: filterStatus,
|
||||
filterEnvironment: searchParams.environment,
|
||||
filterStatus: searchParams.status,
|
||||
projectSlug: projectParam,
|
||||
organizationSlug,
|
||||
direction: searchParams.direction,
|
||||
cursor: searchParams.cursor,
|
||||
pageSize: 25,
|
||||
from: searchParams.from,
|
||||
to: searchParams.to,
|
||||
});
|
||||
|
||||
return typedjson({
|
||||
@@ -130,27 +56,6 @@ export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const url = new URLSearchParams(location.search);
|
||||
|
||||
const selectedEnvironment = url.get("environment") || ExtendedRuntimeEnvironment.ALL;
|
||||
const selectedStatus = url.get("status") || ExtendedJobRunStatus.ALL;
|
||||
|
||||
const handleFilterChange = (filterType: string, value: string) => {
|
||||
url.set(filterType, value);
|
||||
url.delete("cursor");
|
||||
url.delete("direction");
|
||||
navigate(`${location.pathname}?${url.toString()}`);
|
||||
};
|
||||
|
||||
const handleStatusChange = (value: FilterableStatus) => {
|
||||
handleFilterChange("status", value);
|
||||
};
|
||||
|
||||
const handleEnvironmentChange = (value: string) => {
|
||||
handleFilterChange("environment", value);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
@@ -173,67 +78,7 @@ export default function Page() {
|
||||
<PageBody scrollable={false}>
|
||||
<div className="h-full overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
<div className="flex flex-row justify-between gap-x-2">
|
||||
{/* environment filter */}
|
||||
<SelectGroup>
|
||||
<Select
|
||||
name="environment"
|
||||
value={selectedEnvironment}
|
||||
onValueChange={handleEnvironmentChange}
|
||||
>
|
||||
<SelectTrigger size="secondary/small" width="full">
|
||||
<SelectValue placeholder="Select environment" className="ml-2 p-0" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{environmentKeys.map((env) => (
|
||||
<SelectItem key={env} value={env}>
|
||||
<div className="flex gap-x-2">
|
||||
{env !== "ALL" && (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
|
||||
filterEnvironmentColorClassName(env)
|
||||
)}
|
||||
>
|
||||
{filterEnvironmentTitle(env)}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-4 items-center justify-center pl-1 text-xxs font-medium uppercase tracking-wider text-dimmed"
|
||||
)}
|
||||
>
|
||||
{env === "ALL" ? env + " Environments" : env}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
|
||||
{/* status filter */}
|
||||
<SelectGroup>
|
||||
<Select name="status" value={selectedStatus} onValueChange={handleStatusChange}>
|
||||
<SelectTrigger size="secondary/small" width="full">
|
||||
<SelectValue placeholder="Select environment" className="ml-2 p-0" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{statusKeys.map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{
|
||||
<span className="flex items-center gap-1 text-xxs font-medium uppercase tracking-wider">
|
||||
<FilterStatusIcon status={status} className="h-4 w-4" />
|
||||
<FilterStatusLabel status={status} />
|
||||
</span>
|
||||
}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
</div>
|
||||
|
||||
<RunsFilters />
|
||||
<ListPagination list={list} />
|
||||
</div>
|
||||
<RunsTable
|
||||
@@ -251,120 +96,3 @@ export default function Page() {
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function filterEnvironmentTitle(environment: ExtendedRuntimeEnvironmentType) {
|
||||
switch (environment) {
|
||||
case "ALL":
|
||||
return "All";
|
||||
case "PRODUCTION":
|
||||
return "Prod";
|
||||
case "STAGING":
|
||||
return "Staging";
|
||||
case "DEVELOPMENT":
|
||||
return "Dev";
|
||||
case "PREVIEW":
|
||||
return "Preview";
|
||||
}
|
||||
}
|
||||
|
||||
function filterEnvironmentColorClassName(environment: ExtendedRuntimeEnvironmentType) {
|
||||
switch (environment) {
|
||||
case "ALL":
|
||||
return "bg-indigo-500";
|
||||
case "PRODUCTION":
|
||||
return "bg-green-500";
|
||||
case "STAGING":
|
||||
return "bg-amber-500";
|
||||
case "DEVELOPMENT":
|
||||
return "bg-pink-500";
|
||||
case "PREVIEW":
|
||||
return "bg-yellow-500";
|
||||
}
|
||||
}
|
||||
|
||||
export function FilterStatusLabel({ status }: { status: FilterableStatus }) {
|
||||
return <span className={filterStatusClassNameColor(status)}>{filterStatusTitle(status)}</span>;
|
||||
}
|
||||
|
||||
export function FilterStatusIcon({
|
||||
status,
|
||||
className,
|
||||
}: {
|
||||
status: FilterableStatus;
|
||||
className: string;
|
||||
}) {
|
||||
switch (status) {
|
||||
case "ALL":
|
||||
return <span className="w-[0.0625rem]"></span>;
|
||||
case "COMPLETED":
|
||||
return <CheckCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "WAITING":
|
||||
return <ClockIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "QUEUED":
|
||||
return <PauseCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "IN_PROGRESS":
|
||||
return <Spinner className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "TIMEDOUT":
|
||||
return (
|
||||
<ExclamationTriangleIcon className={cn(filterStatusClassNameColor(status), className)} />
|
||||
);
|
||||
case "CANCELED":
|
||||
return <NoSymbolIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
case "FAILED":
|
||||
return <XCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function filterStatusTitle(status: FilterableStatus): string {
|
||||
switch (status) {
|
||||
case "ALL":
|
||||
return "All Status";
|
||||
case "QUEUED":
|
||||
return "Queued";
|
||||
case "IN_PROGRESS":
|
||||
return "In progress";
|
||||
case "WAITING":
|
||||
return "Waiting";
|
||||
case "COMPLETED":
|
||||
return "Completed";
|
||||
case "FAILED":
|
||||
return "Failed";
|
||||
case "CANCELED":
|
||||
return "Canceled";
|
||||
case "TIMEDOUT":
|
||||
return "Timed out";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function filterStatusClassNameColor(status: FilterableStatus): string {
|
||||
switch (status) {
|
||||
case "ALL":
|
||||
return "text-dimmed";
|
||||
case "QUEUED":
|
||||
return "text-slate-500";
|
||||
case "IN_PROGRESS":
|
||||
return "text-blue-500";
|
||||
case "WAITING":
|
||||
return "text-blue-500";
|
||||
case "COMPLETED":
|
||||
return "text-green-500";
|
||||
case "FAILED":
|
||||
return "text-rose-500";
|
||||
case "CANCELED":
|
||||
return "text-slate-500";
|
||||
case "TIMEDOUT":
|
||||
return "text-amber-300";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-13
@@ -1,11 +1,16 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
|
||||
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout, variantClasses } from "~/components/primitives/Callout";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
@@ -18,33 +23,26 @@ import {
|
||||
PageTitleRow,
|
||||
} from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { RunListSearchSchema } from "~/components/runs/RunStatuses";
|
||||
import { RunsTable } from "~/components/runs/RunsTable";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useTypedMatchData } from "~/hooks/useTypedMatchData";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { TriggerSourcePresenter } from "~/presenters/TriggerSourcePresenter.server";
|
||||
import { requireUser, requireUserId } from "~/services/session.server";
|
||||
import { ActivateSourceService } from "~/services/sources/activateSource.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import {
|
||||
TriggerSourceParamSchema,
|
||||
rootPath,
|
||||
projectTriggersPath,
|
||||
externalTriggerPath,
|
||||
externalTriggerRunsParentPath,
|
||||
projectTriggersPath,
|
||||
trimTrailingSlash,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/ListPagination";
|
||||
import { RunListSearchSchema } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { z } from "zod";
|
||||
import { ActivateSourceService } from "~/services/sources/activateSource.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
+11
-16
@@ -1,37 +1,32 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
|
||||
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
|
||||
import { Callout, variantClasses } from "~/components/primitives/Callout";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { RunListSearchSchema } from "~/components/runs/RunStatuses";
|
||||
import { RunsTable } from "~/components/runs/RunsTable";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { useTypedMatchData } from "~/hooks/useTypedMatchData";
|
||||
import { requireUser, requireUserId } from "~/services/session.server";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { WebhookSourcePresenter } from "~/presenters/WebhookSourcePresenter.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import {
|
||||
TriggerSourceParamSchema,
|
||||
projectTriggersPath,
|
||||
externalTriggerPath,
|
||||
projectWebhookTriggersPath,
|
||||
trimTrailingSlash,
|
||||
webhookTriggerRunsParentPath,
|
||||
projectWebhookTriggersPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/ListPagination";
|
||||
import { RunListSearchSchema } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { z } from "zod";
|
||||
import { ActivateSourceService } from "~/services/sources/activateSource.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { WebhookSourcePresenter } from "~/presenters/WebhookSourcePresenter.server";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
+3
-3
@@ -5,9 +5,12 @@ import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
|
||||
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { RunListSearchSchema } from "~/components/runs/RunStatuses";
|
||||
import { WebhookDeliveryRunsTable } from "~/components/runs/WebhookDeliveryRunsTable";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useTypedMatchData } from "~/hooks/useTypedMatchData";
|
||||
import { WebhookDeliveryPresenter } from "~/presenters/WebhookDeliveryPresenter.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import {
|
||||
@@ -19,9 +22,6 @@ import {
|
||||
webhookTriggerPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/ListPagination";
|
||||
import { RunListSearchSchema } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route";
|
||||
import { WebhookDeliveryPresenter } from "~/presenters/WebhookDeliveryPresenter.server";
|
||||
import { WebhookDeliveryRunsTable } from "~/components/runs/WebhookDeliveryRunsTable";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
+3
-8
@@ -2,7 +2,6 @@ import { Outlet } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import {
|
||||
PageHeader,
|
||||
PageInfoGroup,
|
||||
@@ -12,8 +11,10 @@ import {
|
||||
PageTitle,
|
||||
PageTitleRow,
|
||||
} from "~/components/primitives/PageHeader";
|
||||
import { RunListSearchSchema } from "~/components/runs/RunStatuses";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { WebhookSourcePresenter } from "~/presenters/WebhookSourcePresenter.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import {
|
||||
TriggerSourceParamSchema,
|
||||
@@ -21,8 +22,6 @@ import {
|
||||
webhookDeliveryPath,
|
||||
webhookTriggerPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { RunListSearchSchema } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route";
|
||||
import { WebhookSourcePresenter } from "~/presenters/WebhookSourcePresenter.server";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
@@ -77,11 +76,7 @@ export default function Page() {
|
||||
value={trigger.integration.slug}
|
||||
to={trigger.integrationLink}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon="webhook"
|
||||
label="HTTP Endpoint"
|
||||
to={trigger.httpEndpointLink}
|
||||
/>
|
||||
<PageInfoProperty icon="webhook" label="HTTP Endpoint" to={trigger.httpEndpointLink} />
|
||||
</PageInfoGroup>
|
||||
</PageInfoRow>
|
||||
<PageTabs
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { ActionFunction, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { UserProfilePhoto } from "~/components/UserProfilePhoto";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Checkbox } from "~/components/primitives/Checkbox";
|
||||
import { Fieldset } from "~/components/primitives/Fieldset";
|
||||
import { FormButtons } from "~/components/primitives/FormButtons";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
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 { PageHeader, PageTitle, PageTitleRow } from "~/components/primitives/PageHeader";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { updateUser } from "~/models/user.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { accountPath } from "~/utils/pathBuilder";
|
||||
|
||||
function createSchema(
|
||||
constraints: {
|
||||
isEmailUnique?: (email: string) => Promise<boolean>;
|
||||
} = {}
|
||||
) {
|
||||
return z.object({
|
||||
name: z
|
||||
.string({ required_error: "You must enter a name" })
|
||||
.min(2, "Your name must be at least 2 characters long")
|
||||
.max(50),
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.superRefine((email, ctx) => {
|
||||
if (constraints.isEmailUnique === undefined) {
|
||||
//client-side validation skips this
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: conform.VALIDATION_UNDEFINED,
|
||||
});
|
||||
} else {
|
||||
// Tell zod this is an async validation by returning the promise
|
||||
return constraints.isEmailUnique(email).then((isUnique) => {
|
||||
if (isUnique) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Email is already being used by a different account",
|
||||
});
|
||||
});
|
||||
}
|
||||
}),
|
||||
marketingEmails: z.preprocess((value) => value === "on", z.boolean()),
|
||||
});
|
||||
}
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const formSchema = createSchema({
|
||||
isEmailUnique: async (email) => {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (existingUser.id === userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const submission = await parse(formData, { schema: formSchema, async: true });
|
||||
|
||||
if (!submission.value || submission.intent !== "submit") {
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await updateUser({
|
||||
id: userId,
|
||||
name: submission.value.name,
|
||||
email: submission.value.email,
|
||||
marketingEmails: submission.value.marketingEmails,
|
||||
});
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
accountPath(),
|
||||
request,
|
||||
"Your account profile has been updated."
|
||||
);
|
||||
} catch (error: any) {
|
||||
return json({ errors: { body: error.message } }, { status: 400 });
|
||||
}
|
||||
};
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => {
|
||||
return <BreadcrumbLink to={match.pathname} title={"Profile"} />;
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const user = useUser();
|
||||
const lastSubmission = useActionData();
|
||||
|
||||
const [form, { name, email, marketingEmails }] = useForm({
|
||||
id: "account",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: createSchema() });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitleRow>
|
||||
<PageTitle title="Your profile" />
|
||||
</PageTitleRow>
|
||||
</PageHeader>
|
||||
|
||||
<PageBody>
|
||||
<Form method="post" {...form.props} className="max-w-md">
|
||||
<InputGroup className="mb-4">
|
||||
<Label htmlFor={name.id}>Profile picture</Label>
|
||||
<UserProfilePhoto className="h-24 w-24" />
|
||||
</InputGroup>
|
||||
<Fieldset>
|
||||
<InputGroup>
|
||||
<Label htmlFor={name.id}>Full name</Label>
|
||||
<Input
|
||||
{...conform.input(name, { type: "text" })}
|
||||
placeholder="Your full name"
|
||||
defaultValue={user?.name ?? ""}
|
||||
icon="account"
|
||||
/>
|
||||
<Hint>Your teammates will see this</Hint>
|
||||
<FormError id={name.errorId}>{name.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label htmlFor={email.id}>Email address</Label>
|
||||
<Input
|
||||
{...conform.input(email, { type: "text" })}
|
||||
placeholder="Your email"
|
||||
defaultValue={user?.email ?? ""}
|
||||
icon="envelope"
|
||||
/>
|
||||
<FormError id={email.errorId}>{email.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label>Notifications</Label>
|
||||
<Checkbox
|
||||
id="marketingEmails"
|
||||
{...conform.input(marketingEmails, { type: "checkbox" })}
|
||||
label="Receive product updates"
|
||||
variant="simple/small"
|
||||
defaultChecked={user.marketingEmails}
|
||||
/>
|
||||
<FormError id={marketingEmails.errorId}>{marketingEmails.error}</FormError>
|
||||
</InputGroup>
|
||||
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Button type="submit" variant={"primary/small"}>
|
||||
Update
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { title } from "process";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { ErrorIcon } from "~/assets/icons/ErrorIcon";
|
||||
import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Icon } from "~/components/primitives/Icon";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { createPersonalAccessTokenFromAuthorizationCode } from "~/services/personalAccessToken.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { rootPath } from "~/utils/pathBuilder";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
authorizationCode: z.string(),
|
||||
});
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const parsedParams = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
logger.info("Invalid params", { params });
|
||||
throw new Response(undefined, {
|
||||
status: 400,
|
||||
statusText: "Invalid params",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const personalAccessToken = await createPersonalAccessTokenFromAuthorizationCode(
|
||||
parsedParams.data.authorizationCode,
|
||||
userId
|
||||
);
|
||||
return typedjson({
|
||||
success: true as const,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Response) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return typedjson({
|
||||
success: false as const,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(JSON.stringify(error));
|
||||
throw new Response(undefined, {
|
||||
status: 400,
|
||||
statusText: "Something went wrong, if this problem persists please contact support.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const result = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
<MainCenteredContainer className="max-w-[22rem]">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
{result.success ? (
|
||||
<div>
|
||||
<Header1 className="mb-2 flex items-center gap-1">
|
||||
<Icon icon={CheckCircleIcon} className="h-6 w-6 text-emerald-500" /> Successfully
|
||||
authenticated
|
||||
</Header1>
|
||||
<Paragraph>Return to your terminal to continue.</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Header1 className="mb-2">Authentication failed</Header1>
|
||||
<Callout variant="error" className="my-2">
|
||||
{result.error}
|
||||
</Callout>
|
||||
<Paragraph spacing>
|
||||
There was a problem authenticating you, please try logging in with your CLI again.
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MainCenteredContainer>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||
import { ShieldExclamationIcon } from "@heroicons/react/24/solid";
|
||||
import { Form, useActionData, useFetcher } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { Fieldset } from "~/components/primitives/Fieldset";
|
||||
import { FormButtons } from "~/components/primitives/FormButtons";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
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 {
|
||||
PageButtons,
|
||||
PageDescription,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
PageTitleRow,
|
||||
} from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellMenu,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import {
|
||||
CreatedPersonalAccessToken,
|
||||
ObfuscatedPersonalAccessToken,
|
||||
createPersonalAccessToken,
|
||||
getValidPersonalAccessTokens,
|
||||
revokePersonalAccessToken,
|
||||
} from "~/services/personalAccessToken.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { personalAccessTokensPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
try {
|
||||
const personalAccessTokens = await getValidPersonalAccessTokens(userId);
|
||||
|
||||
return typedjson({
|
||||
personalAccessTokens,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Response) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
throw new Response(undefined, {
|
||||
status: 400,
|
||||
statusText: "Something went wrong, if this problem persists please contact support.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const CreateTokenSchema = z.discriminatedUnion("action", [
|
||||
z.object({
|
||||
action: z.literal("create"),
|
||||
tokenName: z
|
||||
.string({ required_error: "You must enter a name" })
|
||||
.min(2, "Your name must be at least 2 characters long")
|
||||
.max(50),
|
||||
}),
|
||||
z.object({
|
||||
action: z.literal("revoke"),
|
||||
tokenId: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const userId = await requireUserId(request);
|
||||
const formData = await request.formData();
|
||||
const submission = parse(formData, { schema: CreateTokenSchema });
|
||||
|
||||
if (!submission.value) {
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
switch (submission.value.action) {
|
||||
case "create": {
|
||||
try {
|
||||
const tokenResult = await createPersonalAccessToken({
|
||||
name: submission.value.tokenName,
|
||||
userId,
|
||||
});
|
||||
|
||||
return json({ ...submission, payload: { token: tokenResult } });
|
||||
} catch (error: any) {
|
||||
return json({ errors: { body: error.message } }, { status: 400 });
|
||||
}
|
||||
}
|
||||
case "revoke": {
|
||||
try {
|
||||
await revokePersonalAccessToken(submission.value.tokenId);
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
personalAccessTokensPath(),
|
||||
request,
|
||||
"Personal Access Token revoked"
|
||||
);
|
||||
} catch (error: any) {
|
||||
return json({ errors: { body: error.message } }, { status: 400 });
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return json({ errors: { body: "Invalid action" } }, { status: 400 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => {
|
||||
return <BreadcrumbLink to={match.pathname} title={"Personal Access Tokens"} />;
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { personalAccessTokens } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitleRow>
|
||||
<PageTitle title="Personal Access Tokens" />
|
||||
<PageButtons>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary/small">Create new token</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>Create a Personal Access Token</DialogHeader>
|
||||
<CreatePersonalAccessToken />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</PageButtons>
|
||||
</PageTitleRow>
|
||||
<PageDescription>Personal Access Tokens can be used with our CLI and API.</PageDescription>
|
||||
</PageHeader>
|
||||
|
||||
<PageBody>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Token</TableHeaderCell>
|
||||
<TableHeaderCell>Created</TableHeaderCell>
|
||||
<TableHeaderCell>Last accessed</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Delete</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{personalAccessTokens.length > 0 ? (
|
||||
personalAccessTokens.map((personalAccessToken) => {
|
||||
return (
|
||||
<TableRow key={personalAccessToken.id} className="group">
|
||||
<TableCell>{personalAccessToken.name}</TableCell>
|
||||
<TableCell>{personalAccessToken.obfuscatedToken}</TableCell>
|
||||
<TableCell>
|
||||
<DateTime date={personalAccessToken.createdAt} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{personalAccessToken.lastAccessedAt ? (
|
||||
<DateTime date={personalAccessToken.lastAccessedAt} />
|
||||
) : (
|
||||
"Never"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell alignment="right">
|
||||
<RevokePersonalAccessToken token={personalAccessToken} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={5}>
|
||||
<Paragraph variant="small" className="flex items-center justify-center">
|
||||
You have no Personal Access Tokens (that haven't been revoked).
|
||||
</Paragraph>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function CreatePersonalAccessToken() {
|
||||
const fetcher = useFetcher<typeof action>();
|
||||
const lastSubmission = fetcher.data as any;
|
||||
|
||||
const [form, { tokenName }] = useForm({
|
||||
id: "create-personal-access-token",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: CreateTokenSchema });
|
||||
},
|
||||
});
|
||||
|
||||
const token = lastSubmission?.payload?.token
|
||||
? (lastSubmission?.payload?.token as CreatedPersonalAccessToken)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="max-w-full overflow-x-hidden">
|
||||
{token ? (
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
<Header2>Successfully generated a new token</Header2>
|
||||
<Callout variant="success">
|
||||
Copy this access token and store it in a secure place - you will not be able to see it
|
||||
again.
|
||||
</Callout>
|
||||
<ClipboardField
|
||||
secure
|
||||
value={token.token}
|
||||
variant={"secondary/medium"}
|
||||
icon={<ShieldExclamationIcon className="h-5 w-5 text-emerald-500" />}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<fetcher.Form method="post" {...form.props}>
|
||||
<input type="hidden" name="action" value="create" />
|
||||
<Fieldset>
|
||||
<InputGroup>
|
||||
<Label htmlFor={tokenName.id}>Name</Label>
|
||||
<Input
|
||||
{...conform.input(tokenName, { type: "text" })}
|
||||
placeholder="The name of your Personal Access Token"
|
||||
defaultValue=""
|
||||
icon={ShieldCheckIcon}
|
||||
autoComplete="off"
|
||||
data-1p-ignore
|
||||
/>
|
||||
<Hint>
|
||||
This will help you to identify your token. Tokens called "cli" are automatically
|
||||
generated when you login with our CLI.
|
||||
</Hint>
|
||||
<FormError id={tokenName.errorId}>{tokenName.error}</FormError>
|
||||
</InputGroup>
|
||||
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Button type="submit" variant={"primary/small"}>
|
||||
Update
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Fieldset>
|
||||
</fetcher.Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RevokePersonalAccessToken({ token }: { token: ObfuscatedPersonalAccessToken }) {
|
||||
const lastSubmission = useActionData();
|
||||
|
||||
const [form, { tokenId }] = useForm({
|
||||
id: "revoke-personal-access-token",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: CreateTokenSchema });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="small-menu-item" LeadingIcon="trash-can" className="text-xs" />
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>Revoke Personal Access Token</DialogHeader>
|
||||
<div className="flex flex-col gap-3 pt-3">
|
||||
<Paragraph>
|
||||
Are you sure you want to revoke "{token.name}"? This can't be reversed.
|
||||
</Paragraph>
|
||||
<Form method="post" {...form.props}>
|
||||
<input type="hidden" name="action" value="revoke" />
|
||||
<input type="hidden" name="tokenId" value={token.id} />
|
||||
<Button type="submit" variant="danger/medium" fullWidth>
|
||||
Revoke token
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,185 +1,35 @@
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Fieldset } from "~/components/primitives/Fieldset";
|
||||
import { FormButtons } from "~/components/primitives/FormButtons";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
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 { Outlet } from "@remix-run/react";
|
||||
import { AppContainer } from "~/components/layout/AppLayout";
|
||||
import { AccountSideMenu } from "~/components/navigation/AccountSideMenu";
|
||||
import { Breadcrumb, BreadcrumbLink } from "~/components/navigation/Breadcrumb";
|
||||
import { PageNavigationIndicator } from "~/components/navigation/PageNavigationIndicator";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { z } from "zod";
|
||||
import { ActionFunction, json, redirect } from "@remix-run/server-runtime";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { accountPath, rootPath } from "~/utils/pathBuilder";
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { UserProfilePhoto } from "~/components/UserProfilePhoto";
|
||||
import { Checkbox } from "~/components/primitives/Checkbox";
|
||||
import { updateUser } from "~/models/user.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout";
|
||||
import { FormTitle } from "~/components/primitives/FormTitle";
|
||||
import { Handle } from "~/utils/handle";
|
||||
|
||||
function createSchema(
|
||||
constraints: {
|
||||
isEmailUnique?: (email: string) => Promise<boolean>;
|
||||
} = {}
|
||||
) {
|
||||
return z.object({
|
||||
name: z
|
||||
.string({ required_error: "You must enter a name" })
|
||||
.min(2, "Your name must be at least 2 characters long")
|
||||
.max(50),
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.superRefine((email, ctx) => {
|
||||
if (constraints.isEmailUnique === undefined) {
|
||||
//client-side validation skips this
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: conform.VALIDATION_UNDEFINED,
|
||||
});
|
||||
} else {
|
||||
// Tell zod this is an async validation by returning the promise
|
||||
return constraints.isEmailUnique(email).then((isUnique) => {
|
||||
if (isUnique) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Email is already being used by a different account",
|
||||
});
|
||||
});
|
||||
}
|
||||
}),
|
||||
marketingEmails: z.preprocess((value) => value === "on", z.boolean()),
|
||||
});
|
||||
}
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const formSchema = createSchema({
|
||||
isEmailUnique: async (email) => {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (existingUser.id === userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const submission = await parse(formData, { schema: formSchema, async: true });
|
||||
|
||||
if (!submission.value || submission.intent !== "submit") {
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await updateUser({
|
||||
id: userId,
|
||||
name: submission.value.name,
|
||||
email: submission.value.email,
|
||||
marketingEmails: submission.value.marketingEmails,
|
||||
});
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
accountPath(),
|
||||
request,
|
||||
"Your account profile has been updated."
|
||||
);
|
||||
} catch (error: any) {
|
||||
return json({ errors: { body: error.message } }, { status: 400 });
|
||||
}
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => {
|
||||
return <BreadcrumbLink to={match.pathname} title={"Account"} />;
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const user = useUser();
|
||||
const lastSubmission = useActionData();
|
||||
|
||||
const [form, { name, email, marketingEmails }] = useForm({
|
||||
id: "account",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: createSchema() });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
<MainCenteredContainer>
|
||||
<FormTitle LeadingIcon="user" title="Profile" />
|
||||
<Form method="post" {...form.props} className="max-w-md">
|
||||
<InputGroup className="mb-4">
|
||||
<Label htmlFor={name.id}>Profile picture</Label>
|
||||
<UserProfilePhoto className="h-24 w-24" />
|
||||
</InputGroup>
|
||||
<Fieldset>
|
||||
<InputGroup>
|
||||
<Label htmlFor={name.id}>Full name</Label>
|
||||
<Input
|
||||
{...conform.input(name, { type: "text" })}
|
||||
placeholder="Your full name"
|
||||
defaultValue={user?.name ?? ""}
|
||||
icon="account"
|
||||
/>
|
||||
<Hint>Your teammates will see this</Hint>
|
||||
<FormError id={name.errorId}>{name.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label htmlFor={email.id}>Email address</Label>
|
||||
<Input
|
||||
{...conform.input(email, { type: "text" })}
|
||||
placeholder="Your email"
|
||||
defaultValue={user?.email ?? ""}
|
||||
icon="envelope"
|
||||
/>
|
||||
<FormError id={email.errorId}>{email.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label>Notifications</Label>
|
||||
<Checkbox
|
||||
id="marketingEmails"
|
||||
{...conform.input(marketingEmails, { type: "checkbox" })}
|
||||
label="Receive product updates"
|
||||
variant="simple/small"
|
||||
defaultChecked={user.marketingEmails}
|
||||
/>
|
||||
<FormError id={marketingEmails.errorId}>{marketingEmails.error}</FormError>
|
||||
</InputGroup>
|
||||
<div className="grid grid-cols-[14rem_1fr] overflow-hidden">
|
||||
<AccountSideMenu user={user} />
|
||||
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Button type="submit" variant={"primary/small"}>
|
||||
Update
|
||||
</Button>
|
||||
}
|
||||
cancelButton={
|
||||
<LinkButton to={rootPath()} variant={"secondary/small"}>
|
||||
Cancel
|
||||
</LinkButton>
|
||||
}
|
||||
/>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</MainCenteredContainer>
|
||||
<div className="grid grid-rows-[2.25rem_1fr] overflow-hidden">
|
||||
<div className="flex w-full items-center justify-between border-b border-ui-border">
|
||||
<Breadcrumb />
|
||||
<div className="flex h-full items-center gap-4">
|
||||
<PageNavigationIndicator className="mr-2" />
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { CreateAuthorizationCodeResponse } from "@trigger.dev/core";
|
||||
import { env } from "~/env.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { createAuthorizationCode } from "~/services/personalAccessToken.server";
|
||||
|
||||
/** Used to create an AuthorizationCode, that can then be used to obtain a Personal Access Token by logging in with the provided URL */
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
logger.info("Creating AuthorizationCode", { url: request.url });
|
||||
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
//there is no authentication on this endpoint, anyone can create an AuthorizationCode.
|
||||
//they're only used to allow a user to login, when they'll then receive a Personal Access Token
|
||||
|
||||
try {
|
||||
const authorizationCode = await createAuthorizationCode();
|
||||
const responseJson: CreateAuthorizationCodeResponse = {
|
||||
authorizationCode: authorizationCode.code,
|
||||
url: `${env.APP_ORIGIN}/account/authorization-code/${authorizationCode.code}`,
|
||||
};
|
||||
|
||||
return json(responseJson);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Error creating AuthorizationCode", {
|
||||
url: request.url,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
return json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import {
|
||||
GetPersonalAccessTokenRequestSchema,
|
||||
GetPersonalAccessTokenResponse,
|
||||
} from "@trigger.dev/core";
|
||||
import { generateErrorMessage } from "zod-error";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { getPersonalAccessTokenFromAuthorizationCode } from "~/services/personalAccessToken.server";
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
logger.info("Getting PersonalAccessToken from AuthorizationCode", { url: request.url });
|
||||
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
//There is no authentication on this endpoint, anyone can create an AuthorizationCode.
|
||||
//But only a logged in user can create a PersonalAccessToken, so for a user who can't login to the app this will always fail.
|
||||
|
||||
// Now parse the request body
|
||||
const anyBody = await request.json();
|
||||
const body = GetPersonalAccessTokenRequestSchema.safeParse(anyBody);
|
||||
if (!body.success) {
|
||||
return json({ message: generateErrorMessage(body.error.issues) }, { status: 422 });
|
||||
}
|
||||
|
||||
try {
|
||||
const personalAccessToken = await getPersonalAccessTokenFromAuthorizationCode(
|
||||
body.data.authorizationCode
|
||||
);
|
||||
|
||||
const responseJson: GetPersonalAccessTokenResponse = {
|
||||
token: personalAccessToken.token,
|
||||
};
|
||||
return json(responseJson);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Error getting PersonalAccessToken from AuthorizationCode", {
|
||||
url: request.url,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
return json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { WhoAmIResponse } from "@trigger.dev/core";
|
||||
import { prisma } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
logger.info("whoami v2", { url: request.url });
|
||||
|
||||
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
where: {
|
||||
id: authenticationResult.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const result: WhoAmIResponse = {
|
||||
userId: authenticationResult.userId,
|
||||
email: user.email,
|
||||
};
|
||||
return json(result);
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
import { customAlphabet, nanoid } from "nanoid";
|
||||
import nodeCrypto from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { logger } from "./logger.server";
|
||||
import { PersonalAccessToken } from "@trigger.dev/database";
|
||||
|
||||
const tokenValueLength = 40;
|
||||
//lowercase only, removed 0 and l to avoid confusion
|
||||
const tokenGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", tokenValueLength);
|
||||
|
||||
type CreatePersonalAccessTokenOptions = {
|
||||
name: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
/** Returns obfuscated access tokens that aren't revoked */
|
||||
export async function getValidPersonalAccessTokens(userId: string) {
|
||||
const personalAccessTokens = await prisma.personalAccessToken.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
obfuscatedToken: true,
|
||||
createdAt: true,
|
||||
lastAccessedAt: true,
|
||||
},
|
||||
where: {
|
||||
userId,
|
||||
revokedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
return personalAccessTokens.map((pat) => ({
|
||||
id: pat.id,
|
||||
name: pat.name,
|
||||
obfuscatedToken: pat.obfuscatedToken,
|
||||
createdAt: pat.createdAt,
|
||||
lastAccessedAt: pat.lastAccessedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
export type ObfuscatedPersonalAccessToken = Awaited<
|
||||
ReturnType<typeof getValidPersonalAccessTokens>
|
||||
>[number];
|
||||
|
||||
/** Gets a PersonalAccessToken from an Auth Code, this only works within 10 mins of the auth code being created */
|
||||
export async function getPersonalAccessTokenFromAuthorizationCode(authorizationCode: string) {
|
||||
//only allow authorization codes that were created less than 10 mins ago
|
||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
||||
const code = await prisma.authorizationCode.findUnique({
|
||||
select: {
|
||||
personalAccessToken: true,
|
||||
},
|
||||
where: {
|
||||
code: authorizationCode,
|
||||
|
||||
createdAt: {
|
||||
gte: tenMinutesAgo,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!code) {
|
||||
throw new Error("Invalid authorization code, or code expired");
|
||||
}
|
||||
|
||||
//there's no PersonalAccessToken associated with this code
|
||||
if (!code.personalAccessToken) {
|
||||
return {
|
||||
token: null,
|
||||
};
|
||||
}
|
||||
|
||||
const decryptedToken = decryptPersonalAccessToken(code.personalAccessToken);
|
||||
return {
|
||||
token: {
|
||||
token: decryptedToken,
|
||||
obfuscatedToken: code.personalAccessToken.obfuscatedToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function revokePersonalAccessToken(tokenId: string) {
|
||||
await prisma.personalAccessToken.update({
|
||||
where: {
|
||||
id: tokenId,
|
||||
},
|
||||
data: {
|
||||
revokedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type PersonalAccessTokenAuthenticationResult = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const EncryptedSecretValueSchema = z.object({
|
||||
nonce: z.string(),
|
||||
ciphertext: z.string(),
|
||||
tag: z.string(),
|
||||
});
|
||||
|
||||
const AuthorizationHeaderSchema = z.string().regex(/^Bearer .+$/);
|
||||
|
||||
export async function authenticateApiRequestWithPersonalAccessToken(
|
||||
request: Request
|
||||
): Promise<PersonalAccessTokenAuthenticationResult | undefined> {
|
||||
const token = getPersonalAccessTokenFromRequest(request);
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
return authenticatePersonalAccessToken(token);
|
||||
}
|
||||
|
||||
function getPersonalAccessTokenFromRequest(request: Request) {
|
||||
const rawAuthorization = request.headers.get("Authorization");
|
||||
|
||||
const authorization = AuthorizationHeaderSchema.safeParse(rawAuthorization);
|
||||
if (!authorization.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const personalAccessToken = authorization.data.replace(/^Bearer /, "");
|
||||
return personalAccessToken;
|
||||
}
|
||||
|
||||
export async function authenticatePersonalAccessToken(
|
||||
token: string
|
||||
): Promise<PersonalAccessTokenAuthenticationResult | undefined> {
|
||||
if (!token.startsWith(tokenPrefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hashedToken = hashToken(token);
|
||||
|
||||
const personalAccessToken = await prisma.personalAccessToken.update({
|
||||
where: {
|
||||
hashedToken,
|
||||
revokedAt: null,
|
||||
},
|
||||
data: {
|
||||
lastAccessedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!personalAccessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decryptedToken = decryptPersonalAccessToken(personalAccessToken);
|
||||
|
||||
if (decryptedToken !== token) {
|
||||
logger.error(
|
||||
`PersonalAccessToken with id: ${personalAccessToken.id} was found in the database with hash ${hashedToken}, but the decrypted token did not match the provided token.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: personalAccessToken.userId,
|
||||
};
|
||||
}
|
||||
|
||||
export function createAuthorizationCode() {
|
||||
return prisma.authorizationCode.create({
|
||||
data: {
|
||||
code: nanoid(64),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Creates a PersonalAccessToken from an Auth Code, and return the token. We only ever return the unencrypted token once. */
|
||||
export async function createPersonalAccessTokenFromAuthorizationCode(
|
||||
authorizationCode: string,
|
||||
userId: string
|
||||
) {
|
||||
//only allow authorization codes that were created less than 10 mins ago
|
||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
||||
const code = await prisma.authorizationCode.findUnique({
|
||||
where: {
|
||||
code: authorizationCode,
|
||||
personalAccessTokenId: null,
|
||||
createdAt: {
|
||||
gte: tenMinutesAgo,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
throw new Error("Invalid authorization code, code already used, or code expired");
|
||||
}
|
||||
|
||||
const existingCliPersonalAccessToken = await prisma.personalAccessToken.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
name: "cli",
|
||||
},
|
||||
});
|
||||
|
||||
//we only allow you to have one CLI PAT at a time
|
||||
if (existingCliPersonalAccessToken) {
|
||||
await prisma.personalAccessToken.delete({
|
||||
where: {
|
||||
id: existingCliPersonalAccessToken.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const token = await createPersonalAccessToken({
|
||||
name: "cli",
|
||||
userId,
|
||||
});
|
||||
|
||||
await prisma.authorizationCode.update({
|
||||
where: {
|
||||
code: authorizationCode,
|
||||
},
|
||||
data: {
|
||||
personalAccessTokenId: token.id,
|
||||
},
|
||||
});
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/** Created a new PersonalAccessToken, and return the token. We only ever return the unencrypted token once. */
|
||||
export async function createPersonalAccessToken({
|
||||
name,
|
||||
userId,
|
||||
}: CreatePersonalAccessTokenOptions) {
|
||||
const token = createToken();
|
||||
const encryptedToken = encryptToken(token);
|
||||
|
||||
const personalAccessToken = await prisma.personalAccessToken.create({
|
||||
data: {
|
||||
name,
|
||||
userId,
|
||||
encryptedToken,
|
||||
obfuscatedToken: obfuscateToken(token),
|
||||
hashedToken: hashToken(token),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: personalAccessToken.id,
|
||||
name,
|
||||
userId,
|
||||
token,
|
||||
obfuscatedToken: personalAccessToken.obfuscatedToken,
|
||||
};
|
||||
}
|
||||
|
||||
export type CreatedPersonalAccessToken = Awaited<ReturnType<typeof createPersonalAccessToken>>;
|
||||
|
||||
const tokenPrefix = "tr_pat_";
|
||||
|
||||
/** Creates a PersonalAccessToken that starts with tr_pat_ */
|
||||
function createToken() {
|
||||
return `${tokenPrefix}${tokenGenerator()}`;
|
||||
}
|
||||
|
||||
/** Obfuscates all but the first and last 4 characters of the token, so it looks like tr_pat_bhbd•••••••••••••••••••fd4a */
|
||||
function obfuscateToken(token: string) {
|
||||
const withoutPrefix = token.replace(tokenPrefix, "");
|
||||
const obfuscated = `${withoutPrefix.slice(0, 4)}${"•".repeat(18)}${withoutPrefix.slice(-4)}`;
|
||||
return `${tokenPrefix}${obfuscated}`;
|
||||
}
|
||||
|
||||
function encryptToken(value: string) {
|
||||
const nonce = nodeCrypto.randomBytes(12);
|
||||
const cipher = nodeCrypto.createCipheriv("aes-256-gcm", env.ENCRYPTION_KEY, nonce);
|
||||
|
||||
let encrypted = cipher.update(value, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
const tag = cipher.getAuthTag().toString("hex");
|
||||
|
||||
return {
|
||||
nonce: nonce.toString("hex"),
|
||||
ciphertext: encrypted,
|
||||
tag,
|
||||
};
|
||||
}
|
||||
|
||||
function decryptPersonalAccessToken(personalAccessToken: PersonalAccessToken) {
|
||||
const encryptedData = EncryptedSecretValueSchema.safeParse(personalAccessToken.encryptedToken);
|
||||
if (!encryptedData.success) {
|
||||
throw new Error(
|
||||
`Unable to parse encrypted PersonalAccessToken with id: ${personalAccessToken.id}: ${encryptedData.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
const decryptedToken = decryptToken(
|
||||
encryptedData.data.nonce,
|
||||
encryptedData.data.ciphertext,
|
||||
encryptedData.data.tag
|
||||
);
|
||||
return decryptedToken;
|
||||
}
|
||||
|
||||
function decryptToken(nonce: string, ciphertext: string, tag: string): string {
|
||||
const decipher = nodeCrypto.createDecipheriv(
|
||||
"aes-256-gcm",
|
||||
env.ENCRYPTION_KEY,
|
||||
Buffer.from(nonce, "hex")
|
||||
);
|
||||
|
||||
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
||||
|
||||
let decrypted = decipher.update(ciphertext, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
function hashToken(token: string): string {
|
||||
const hash = nodeCrypto.createHash("sha256");
|
||||
hash.update(token);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
EventRecord,
|
||||
Integration,
|
||||
TriggerHttpEndpoint,
|
||||
TriggerSource,
|
||||
@@ -15,6 +16,7 @@ export type JobForPath = Pick<Job, "slug">;
|
||||
export type RunForPath = Pick<Job, "id">;
|
||||
export type IntegrationForPath = Pick<Integration, "slug">;
|
||||
export type TriggerForPath = Pick<TriggerSource, "id">;
|
||||
export type EventForPath = Pick<EventRecord, "id">;
|
||||
export type WebhookForPath = Pick<Webhook, "id">;
|
||||
export type HttpEndpointForPath = Pick<TriggerHttpEndpoint, "key">;
|
||||
|
||||
@@ -46,6 +48,10 @@ export const TriggerSourceParamSchema = ProjectParamSchema.extend({
|
||||
triggerParam: z.string(),
|
||||
});
|
||||
|
||||
export const EventParamSchema = ProjectParamSchema.extend({
|
||||
eventParam: z.string(),
|
||||
});
|
||||
|
||||
export const TriggerSourceRunParamsSchema = TriggerSourceParamSchema.extend({
|
||||
runParam: z.string(),
|
||||
});
|
||||
@@ -76,6 +82,10 @@ export function accountPath() {
|
||||
return `/account`;
|
||||
}
|
||||
|
||||
export function personalAccessTokensPath() {
|
||||
return `/account/tokens`;
|
||||
}
|
||||
|
||||
export function invitesPath() {
|
||||
return `/invites`;
|
||||
}
|
||||
@@ -202,6 +212,18 @@ export function projectTriggersPath(organization: OrgForPath, project: ProjectFo
|
||||
return `${projectPath(organization, project)}/triggers`;
|
||||
}
|
||||
|
||||
export function projectEventsPath(organization: OrgForPath, project: ProjectForPath) {
|
||||
return `${projectPath(organization, project)}/events`;
|
||||
}
|
||||
|
||||
export function projectEventPath(
|
||||
organization: OrgForPath,
|
||||
project: ProjectForPath,
|
||||
event: EventForPath
|
||||
) {
|
||||
return `${projectEventsPath(organization, project)}/${event.id}`;
|
||||
}
|
||||
|
||||
export function projectHttpEndpointsPath(organization: OrgForPath, project: ProjectForPath) {
|
||||
return `${projectPath(organization, project)}/http-endpoints`;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@highlight-run/node": "^3.1.0",
|
||||
"@highlight-run/react": "^3.2.0",
|
||||
"@internationalized/date": "^3.5.1",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.3",
|
||||
@@ -57,6 +58,9 @@
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.5",
|
||||
"@react-aria/datepicker": "^3.9.1",
|
||||
"@react-stately/datepicker": "^3.9.1",
|
||||
"@react-types/datepicker": "^3.7.1",
|
||||
"@remix-run/express": "2.1.0",
|
||||
"@remix-run/node": "2.1.0",
|
||||
"@remix-run/react": "2.1.0",
|
||||
@@ -66,8 +70,8 @@
|
||||
"@tabler/icons-react": "^2.39.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@team-plain/typescript-sdk": "^3.5.0",
|
||||
"@trigger.dev/companyicons": "^1.5.35",
|
||||
"@trigger.dev/billing": "^1.0.10",
|
||||
"@trigger.dev/companyicons": "^1.5.35",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/core-backend": "workspace:*",
|
||||
"@trigger.dev/database": "workspace:*",
|
||||
@@ -105,8 +109,10 @@
|
||||
"prismjs": "^1.29.0",
|
||||
"random-words": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-aria": "^3.31.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-stately": "^3.29.1",
|
||||
"react-use": "^17.4.0",
|
||||
"recharts": "^2.8.0",
|
||||
"remix-auth": "^3.6.0",
|
||||
@@ -172,6 +178,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||
"@typescript-eslint/parser": "^5.59.6",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"datepicker": "link:@types/@react-aria/datepicker",
|
||||
"esbuild": "^0.15.10",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@@ -193,4 +200,4 @@
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ You can add a step to the GitHub Action that deploys your app.
|
||||
```yaml .github/workflows/release.yml
|
||||
- name: 🚀 Refresh Trigger.dev Jobs
|
||||
env:
|
||||
DEPLOY_TEST_HOOK: ${{ secrets.TRIGGER_ENDPOINT_HOOK }}
|
||||
TRIGGER_ENDPOINT_HOOK: ${{ secrets.TRIGGER_ENDPOINT_HOOK }}
|
||||
run: |
|
||||
curl -X POST $TRIGGER_ENDPOINT_HOOK
|
||||
```
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
---
|
||||
title: "Deploy to Kubernetes"
|
||||
description: "Deploy self hosted version of [Trigger.dev](https://trigger.dev) to your kubernetes cluster using our helm chart"
|
||||
description: "Deploy [Trigger.dev](https://trigger.dev) to your kubernetes cluster using our helm chart"
|
||||
---
|
||||
|
||||
<Warning>WARNING: Kubernetes deploys are not officially supported yet, please only use these instructions as a general guide and starting point.</Warning>
|
||||
|
||||
**Prerequisites**
|
||||
- You have understanding of [Kubernetes](https://kubernetes.io/)
|
||||
- Installed [Helm package manager](https://helm.sh/) version v3.11.3 or greater
|
||||
- You have an understanding of [Kubernetes](https://kubernetes.io/)
|
||||
- You have [Helm](https://helm.sh/) version v3.11.3 or greater installed
|
||||
- You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster
|
||||
|
||||
By deploying Trigger.dev on Kubernetes, you can take advantage of its features to ensure that the application is fault-tolerant, highly available, and scalable.
|
||||
@@ -13,76 +16,85 @@ To make the installation process easier and more streamlined, we have created a
|
||||
Helm is a package manager for Kubernetes that simplifies the installation and management of Kubernetes applications.
|
||||
With our Helm chart, you can easily install Trigger.dev on Kubernetes, configure it to your liking, and scale it up or down as needed.
|
||||
|
||||
## Install Trigger.dev Helm repository
|
||||
## Get our Helm chart
|
||||
|
||||
As our charts aren't published for official use yet, you'll need a copy of the `helm-charts` dir:
|
||||
|
||||
```bash
|
||||
TODO: Add helm repo to artifact hub or cloudsmith
|
||||
git clone https://github.com/triggerdotdev/trigger.dev
|
||||
cd trigger.dev/helm-charts
|
||||
```
|
||||
|
||||
## Add Helm values
|
||||
## Add Helm values
|
||||
|
||||
Create a values.yaml file to configure various installation settings, such as the docker image tags and environment variables. To explore all configurable properties for your values file, [visit this page](https://github.com/triggerdotdev/trigger.dev/tree/main/helm-charts/).
|
||||
Create a `my-values.yaml` file to configure various installation settings, such as the docker image tags and environment variables. To explore all configurable parameters for your values file visit our [readme](https://github.com/triggerdotdev/trigger.dev/tree/main/helm-charts#parameters).
|
||||
|
||||
#### Set image tags
|
||||
### Set image tags
|
||||
|
||||
By default, the application will use the latest tag to retrieve the required Docker images, which may be appropriate for most cases.
|
||||
However, we recommend that you use a specific version of the Docker image to avoid unexpected changes to the application.
|
||||
|
||||
<Tip>
|
||||
To find the latest version number of Trigger.dev, follow the link below
|
||||
- [Trigger.dev image on github packaes](https://github.com/triggerdotdev/trigger.dev/pkgs/container/trigger.dev)
|
||||
You can find valid image tags on [GitHub Packages](https://github.com/triggerdotdev/trigger.dev/pkgs/container/trigger.dev).
|
||||
</Tip>
|
||||
|
||||
```yaml simple-values-example.yaml
|
||||
```yaml my-values.yaml
|
||||
trigger:
|
||||
name: trigger
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: ghcr.io/triggerdotdev/trigger.dev
|
||||
tag: "latest" # <--- frontend version
|
||||
tag: "latest" # <--- image tag
|
||||
pullPolicy: Always
|
||||
```
|
||||
|
||||
#### Configure environment variables
|
||||
### Configure environment variables
|
||||
|
||||
You can configure environment variables for trigger in your Helm values file under the property `envVars`. View configurable [environment variables](../configuration/envars).
|
||||
You can configure environment variables for trigger in your Helm values file under the property `trigger.env`. See examples for some of these values [here](https://github.com/triggerdotdev/trigger.dev/blob/main/.env.example).
|
||||
|
||||
Infisical requires the following backend environment variables to be defined: _`MAGIC_LINK_SECRET`_, _`SESSION_SECRET`_, _`ENCRYPTION_KEY`_, _`DIRECT_URL`_, and _`DATABASE_URL`_ .
|
||||
At a bare minimum, Trigger.dev requires the following environment variables to be defined:
|
||||
- `MAGIC_LINK_SECRET`
|
||||
- `SESSION_SECRET`
|
||||
- `ENCRYPTION_KEY`
|
||||
- `DIRECT_URL`
|
||||
- `DATABASE_URL`
|
||||
|
||||
However, when the above environment variables are not defined, the Helm chart
|
||||
will automatically generate these environment variables for you. The generated environment variables will be saved to a Kubernetes secret and will be preserved between upgrades or uninstalls.
|
||||
When the above environment variables are not defined, the Helm chart will automatically generate values for you. It will persist them in a secret which is preserved between upgrades or uninstalls. It is however strongly recommended to define your own values!
|
||||
|
||||
```yaml simple-values-example.yaml
|
||||
...
|
||||
envVars:
|
||||
ENCRYPTION_KEY: "b1ebe43a6a6e24b2aa8fa0707d3890e3"
|
||||
MAGIC_LINK_SECRET: "842727396bcee22da68518f959c5730b"
|
||||
```yaml my-values.yaml
|
||||
trigger:
|
||||
...
|
||||
env:
|
||||
ENCRYPTION_KEY: "b1ebe43a6a6e24b2aa8fa0707d3890e3"
|
||||
MAGIC_LINK_SECRET: "842727396bcee22da68518f959c5730b"
|
||||
...
|
||||
```
|
||||
#### Routing external traffic
|
||||
By default, Trigger.dev takes all traffic coming to your external load balancer's IP address and routes them Trigger.dev's services.
|
||||
Infisical uses Nginx to route external traffic. You can install Nginx along with Trigger by setting `ingress.enabled` to `true` in the Helm values file. View all [properties for ingress](https://github.com/triggerdotdev/trigger.dev/tree/main/helm-charts/).
|
||||
|
||||
```yaml simple-values-example.yaml
|
||||
### Routing external traffic
|
||||
By default, Trigger.dev takes all traffic coming to your external load balancer's IP address and routes them Trigger.dev's services.
|
||||
Trigger.dev uses Nginx to route external traffic. You can install Nginx along with Trigger by setting `ingress.enabled` to `true` in the Helm values file. View all [parameters for ingress](https://github.com/triggerdotdev/trigger.dev/tree/main/helm-charts#ingress-parameters).
|
||||
|
||||
```yaml my-values.yaml
|
||||
...
|
||||
ingress:
|
||||
nginx:
|
||||
enabled: true #<-- if you would like to install nginx along with Trigger.dev
|
||||
enabled: true # <-- if you would like to install nginx along with Trigger.dev
|
||||
```
|
||||
|
||||
#### Database
|
||||
Trigger.dev uses a SQL database as its persistence layer. With this Helm chart, you spin up a PostgreSQL instance powered by Bitnami along side other Trigger.dev services in your cluster.
|
||||
When persistence is enabled, the data will be stored as Kubernetes Persistence Volume. View all [properties for postgresql](https://github.com/triggerdotdev/trigger.dev/tree/main/helm-charts/).
|
||||
### Database
|
||||
With this Helm chart, you spin up a PostgreSQL instance powered by Bitnami alongside other Trigger.dev services in your cluster.
|
||||
When persistence is enabled, the data will be stored as a Kubernetes Persistence Volume. View all [parameters for postgres](https://github.com/triggerdotdev/trigger.dev/tree/main/helm-charts#postgres-parameters).
|
||||
|
||||
```yaml simple-values-example.yaml
|
||||
```yaml my-values.yaml
|
||||
postgresql:
|
||||
enabled: true
|
||||
persistence:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
#### Example helm values
|
||||
```yaml simple-values-example.yaml
|
||||
### Example values
|
||||
|
||||
```yaml my-values.yaml
|
||||
trigger:
|
||||
name: trigger
|
||||
replicaCount: 2
|
||||
@@ -90,19 +102,17 @@ trigger:
|
||||
repository: ghcr.io/triggerdotdev/trigger.dev
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
|
||||
envVars:
|
||||
ENCRYPTION_KEY: "b1ebe43a6a6e24b2aa8fa0707d3890e3"
|
||||
MAGIC_LINK_SECRET: "842727396bcee22da68518f959c5730b"
|
||||
env:
|
||||
ENCRYPTION_KEY: "b1ebe43a6a6e24b2aa8fa0707d3890e3"
|
||||
MAGIC_LINK_SECRET: "842727396bcee22da68518f959c5730b"
|
||||
|
||||
ingress:
|
||||
nginx:
|
||||
enabled: true #<-- if you would like to install nginx along with Infisical
|
||||
|
||||
enabled: true # <-- if you would like to install nginx along with Trigger.dev
|
||||
```
|
||||
|
||||
<Accordion title="Full helm values example">
|
||||
```yaml values.yaml
|
||||
```yaml my-values.yaml
|
||||
ingress:
|
||||
nginx:
|
||||
enabled: true
|
||||
@@ -122,13 +132,9 @@ ingress:
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
nodePort: ""
|
||||
|
||||
# View all environment variables TODO: Docs for all env vars
|
||||
envVars:
|
||||
DATABASE_URL: <>
|
||||
DIRECT_URL: <>
|
||||
ENCRYPTION_KEY: <>
|
||||
|
||||
env:
|
||||
ENCRYPTION_KEY: "b1ebe43a6a6e24b2aa8fa0707d3890e3"
|
||||
MAGIC_LINK_SECRET: "842727396bcee22da68518f959c5730b"
|
||||
|
||||
## Postgresql DB persistence
|
||||
postgresql:
|
||||
@@ -140,7 +146,7 @@ ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod" # <-- if you are setting up HTTPS
|
||||
hostName: app.yourdomain.com ## <- Replace with your own domain
|
||||
hostName: app.yourdomain.com ## <-- replace with your own domain
|
||||
trigger:
|
||||
path: /
|
||||
pathType: Prefix
|
||||
@@ -148,24 +154,48 @@ ingress:
|
||||
- secretName: echo-tls
|
||||
hosts:
|
||||
- app.yourdomain.com
|
||||
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
## Install the Helm chart
|
||||
## Install the Helm chart
|
||||
|
||||
By default, the helm chart will be installed on your default namespace. If you wish to install the Chart on a different namespace, you may specify
|
||||
that by adding the `--namespace <namespace-to-install-to>` to your `helm install` command.
|
||||
The following command will install our chart into the `trigger` namespace:
|
||||
|
||||
```bash
|
||||
## Installs to default namespace
|
||||
TODO: not published
|
||||
# with custom values read from my-values.yaml
|
||||
helm upgrade --install --atomic --namespace trigger --create-namespace trigger . --values my-values.yaml
|
||||
|
||||
# with default values
|
||||
helm upgrade --install --atomic --namespace trigger --create-namespace trigger .
|
||||
|
||||
# with inlined values
|
||||
helm upgrade --install --atomic --namespace trigger --create-namespace trigger . --set trigger.replicaCount=3
|
||||
```
|
||||
|
||||
To watch the pods coming up, simply run this from another terminal:
|
||||
|
||||
```bash
|
||||
kubectl --namespace trigger get pods -w
|
||||
```
|
||||
|
||||
## Access Trigger.dev
|
||||
Allow 3-5 minutes for the deployment to complete. Once done, you should now be able to access Trigger.dev on the IP address exposed via Ingress on your load balancer. If you are not sure what the IP address is run `kubectl get ingress` to view the external IP address exposing Trigger.dev.
|
||||
|
||||
|
||||
Once the deployment is ready, you should be able to access Trigger.dev on the IP address exposed via Ingress on your load balancer. If you are not sure what the IP address is run `kubectl get ingress` to view the external IP address exposing Trigger.dev.
|
||||
|
||||
<Info>
|
||||
Once installation is complete, you will have to create the first account. No default account is provided.
|
||||
</Info>
|
||||
|
||||
### Local access
|
||||
|
||||
Forward a local port to access the webapp directly from your device:
|
||||
|
||||
```bash
|
||||
kubectl --namespace trigger port-forward svc/trigger 2024:3000
|
||||
```
|
||||
|
||||
Log in via email at `http://localhost:2024` then check your logs for the magic link:
|
||||
|
||||
```bash
|
||||
kubectl --namespace trigger logs deployments/trigger
|
||||
```
|
||||
|
||||
+1
-1
@@ -436,7 +436,7 @@
|
||||
},
|
||||
"analytics": {
|
||||
"posthog": {
|
||||
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
|
||||
"apiKey": "phc_9aSDbJCaDUMdZdHxxMPTvcj7A9fsl3mCgM1RBPmPsl7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ client.defineJob({
|
||||
}
|
||||
},
|
||||
{ name: "On Error" },
|
||||
onError: (error, task) => {
|
||||
(error, task) => {
|
||||
//retry the task in 5 minutes
|
||||
return {
|
||||
retryAt: new Date(Date.now() + 5 * 60 * 1000),
|
||||
|
||||
@@ -21,3 +21,4 @@
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
node_modules/
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# Trigger.dev Helm Chart
|
||||
|
||||
> WARNING: Kubernetes deploys are not officially supported yet, please only use these instructions as a general guide and starting point.
|
||||
|
||||
## Installation
|
||||
|
||||
As our charts aren't published for official use yet, you'll need a copy of the `helm-charts` dir and run the following commands within it:
|
||||
|
||||
```bash
|
||||
# with access to your cluster, e.g. KUBECONFIG correctly set
|
||||
helm upgrade --install --atomic --namespace trigger --create-namespace trigger .
|
||||
|
||||
# watch the deployment
|
||||
kubectl --namespace trigger get deployments -w
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
### Common parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------ | ------------------------- | ----- |
|
||||
| `nameOverride` | Override release name | `""` |
|
||||
| `fullnameOverride` | Override release fullname | `""` |
|
||||
|
||||
### Trigger.dev parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| --------------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
|
||||
| `trigger.name` | | `trigger` |
|
||||
| `trigger.fullnameOverride` | trigger fullnameOverride | `""` |
|
||||
| `trigger.podAnnotations` | trigger pod annotations | `{}` |
|
||||
| `trigger.deploymentAnnotations` | trigger deployment annotations | `{}` |
|
||||
| `trigger.replicaCount` | trigger replica count | `2` |
|
||||
| `trigger.image.repository` | trigger image repository | `ghcr.io/triggerdotdev/trigger.dev` |
|
||||
| `trigger.image.tag` | trigger image tag | `latest` |
|
||||
| `trigger.image.pullPolicy` | trigger image pullPolicy | `Always` |
|
||||
| `trigger.resources.limits.memory` | container memory limit [(docs)](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) | `800Mi` |
|
||||
| `trigger.resources.requests.cpu` | container CPU requests [(docs)](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) | `250m` |
|
||||
| `trigger.affinity` | Backend pod affinity | `{}` |
|
||||
| `trigger.kubeSecretRef` | trigger secret resource reference name | `""` |
|
||||
| `trigger.service.annotations` | trigger service annotations | `{}` |
|
||||
| `trigger.service.type` | trigger service type | `ClusterIP` |
|
||||
| `trigger.service.nodePort` | trigger service nodePort (used if above type is `NodePort`) | `""` |
|
||||
|
||||
### Postgres parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------- |
|
||||
| `postgresql.enabled` | Enable Postgres | `true` |
|
||||
| `postgresql.name` | Name used to build variables (deprecated) | `postgresql` |
|
||||
| `postgresql.nameOverride` | Name override | `postgresql` |
|
||||
| `postgresql.fullnameOverride` | Fullname override | `postgresql` |
|
||||
| `postgresql.global.postgresql.auth.postgresPassword` | Password for the "postgres" admin user (overrides `auth.postgresPassword`) | `password` |
|
||||
| `postgresql.global.postgresql.auth.username` | Name for a custom user to create (overrides `auth.username`) | `postgres` |
|
||||
| `postgresql.global.postgresql.auth.password` | Password for the custom user to create (overrides `auth.password`) | `password` |
|
||||
| `postgresql.global.postgresql.auth.database` | Name for a custom database to create (overrides `auth.database`) | `trigger` |
|
||||
| `postgresql.global.postgresql.auth.existingSecret` | Name of existing secret to use for PostgreSQL credentials (overrides `auth.existingSecret`). | `""` |
|
||||
| `postgresql.global.postgresql.auth.secretKeys.adminPasswordKey` | Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.adminPasswordKey`). Only used when `postgresql.global.postgresql.auth.existingSecret` is set. | `""` |
|
||||
| `postgresql.global.postgresql.auth.secretKeys.userPasswordKey` | Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.userPasswordKey`). Only used when `postgresql.global.postgresql.auth.existingSecret` is set. | `""` |
|
||||
| `postgresql.global.postgresql.auth.secretKeys.replicationPasswordKey` | Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.replicationPasswordKey`). Only used when `postgresql.global.postgresql.auth.existingSecret` is set. | `""` |
|
||||
| `postgresql.global.postgresql.service.ports.postgresql` | PostgreSQL service port (overrides `service.ports.postgresql`) | `5432` |
|
||||
| `postgresql.image.registry` | PostgreSQL image registry | `docker.io` |
|
||||
| `postgresql.image.repository` | PostgreSQL image repository | `bitnami/postgresql` |
|
||||
| `postgresql.image.tag` | PostgreSQL image tag (immutable tags are recommended) | `14.10.0-debian-11-r21` |
|
||||
| `postgresql.image.digest` | PostgreSQL image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag | `""` |
|
||||
| `postgresql.image.pullPolicy` | PostgreSQL image pull policy | `IfNotPresent` |
|
||||
| `postgresql.image.pullSecrets` | Specify image pull secrets | `[]` |
|
||||
| `postgresql.image.debug` | Specify if debug values should be set | `false` |
|
||||
| `postgresql.architecture` | PostgreSQL architecture (`standalone` or `replication`) | `standalone` |
|
||||
| `postgresql.containerPorts.postgresql` | PostgreSQL container port | `5432` |
|
||||
| `postgresql.postgresqlDataDir` | PostgreSQL data dir | `/bitnami/postgresql/data` |
|
||||
| `postgresql.postgresqlSharedPreloadLibraries` | Shared preload libraries (comma-separated list) | `pgaudit` |
|
||||
|
||||
### PostgreSQL Primary parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------ | ------------------- |
|
||||
| `postgresql.primary.livenessProbe.enabled` | Enable livenessProbe on PostgreSQL Primary containers | `true` |
|
||||
| `postgresql.primary.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `30` |
|
||||
| `postgresql.primary.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `10` |
|
||||
| `postgresql.primary.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `5` |
|
||||
| `postgresql.primary.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `6` |
|
||||
| `postgresql.primary.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` |
|
||||
| `postgresql.primary.readinessProbe.enabled` | Enable readinessProbe on PostgreSQL Primary containers | `true` |
|
||||
| `postgresql.primary.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `5` |
|
||||
| `postgresql.primary.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `10` |
|
||||
| `postgresql.primary.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `5` |
|
||||
| `postgresql.primary.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `6` |
|
||||
| `postgresql.primary.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` |
|
||||
| `postgresql.primary.startupProbe.enabled` | Enable startupProbe on PostgreSQL Primary containers | `false` |
|
||||
| `postgresql.primary.startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `30` |
|
||||
| `postgresql.primary.startupProbe.periodSeconds` | Period seconds for startupProbe | `10` |
|
||||
| `postgresql.primary.startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` |
|
||||
| `postgresql.primary.startupProbe.failureThreshold` | Failure threshold for startupProbe | `15` |
|
||||
| `postgresql.primary.startupProbe.successThreshold` | Success threshold for startupProbe | `1` |
|
||||
| `postgresql.primary.persistence.enabled` | Enable PostgreSQL Primary data persistence using PVC | `true` |
|
||||
| `postgresql.primary.persistence.existingClaim` | Name of an existing PVC to use | `""` |
|
||||
| `postgresql.primary.persistence.accessModes` | PVC Access Mode for PostgreSQL volume | `["ReadWriteOnce"]` |
|
||||
| `postgresql.primary.persistence.size` | PVC Storage Request for PostgreSQL volume | `8Gi` |
|
||||
|
||||
### Ingress parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| -------------------------- | ------------------------------------------------------------------------ | -------- |
|
||||
| `ingress.enabled` | Enable ingress | `true` |
|
||||
| `ingress.ingressClassName` | Ingress class name | `nginx` |
|
||||
| `ingress.nginx.enabled` | Ingress controller | `false` |
|
||||
| `ingress.annotations` | Ingress annotations | `{}` |
|
||||
| `ingress.hostName` | Ingress hostname (your custom domain name, e.g. `infisical.example.org`) | `""` |
|
||||
| `ingress.tls` | Ingress TLS hosts (matching above hostName) | `[]` |
|
||||
| `ingress.trigger.path` | Trigger.dev ingress path | `/` |
|
||||
| `ingress.trigger.pathType` | Trigger.dev ingress path type | `Prefix` |
|
||||
|
||||
## Generating docs
|
||||
|
||||
This chart aims to be compliant with the [Readme Generator For Helm](https://github.com/bitnami/readme-generator-for-helm) to easily create and maintain the parameters tables above.
|
||||
|
||||
To update the docs, just run: `pnpm generate-docs`
|
||||
@@ -0,0 +1,14 @@
|
||||
trigger:
|
||||
name: trigger
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: ghcr.io/triggerdotdev/trigger.dev
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
env:
|
||||
ENCRYPTION_KEY: "b1ebe43a6a6e24b2aa8fa0707d3890e3"
|
||||
MAGIC_LINK_SECRET: "842727396bcee22da68518f959c5730b"
|
||||
|
||||
ingress:
|
||||
nginx:
|
||||
enabled: false #<-- if you would like to install nginx along with Trigger.dev
|
||||
Generated
+203
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"name": "helm-charts",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "helm-charts",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@bitnami/readme-generator-for-helm": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bitnami/readme-generator-for-helm": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@bitnami/readme-generator-for-helm/-/readme-generator-for-helm-2.6.0.tgz",
|
||||
"integrity": "sha512-LcByNCryaC2OJExL9rnhyFJ18+vrZu1gVoN2Z7j/HI42EjV4kLgT4G1KEPNnrKbls9HvozBqMG+sKZIDh0McFg==",
|
||||
"dependencies": {
|
||||
"commander": "^7.1.0",
|
||||
"dot-object": "^2.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-table": "^2.0.0",
|
||||
"yaml": "^2.0.0-3"
|
||||
},
|
||||
"bin": {
|
||||
"readme-generator": "bin/index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/dot-object": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz",
|
||||
"integrity": "sha512-7FXnyyCLFawNYJ+NhkqyP9Wd2yzuo+7n9pGiYpkmXCTYa8Ci2U0eUNDVg5OuO5Pm6aFXI2SWN8/N/w7SJWu1WA==",
|
||||
"dependencies": {
|
||||
"commander": "^4.0.0",
|
||||
"glob": "^7.1.5"
|
||||
},
|
||||
"bin": {
|
||||
"dot-object": "bin/dot-object"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-object/node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/markdown-table": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz",
|
||||
"integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==",
|
||||
"dependencies": {
|
||||
"repeat-string": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/repeat-string": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
|
||||
"integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
|
||||
"integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"readme-generator-for-helm": {
|
||||
"version": "2.6.1",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"commander": "^7.1.0",
|
||||
"dot-object": "^2.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-table": "^2.0.0",
|
||||
"yaml": "^2.0.0-3"
|
||||
},
|
||||
"bin": {
|
||||
"readme-generator": "bin/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"jest": "^29.2.1",
|
||||
"temp": "^0.9.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "helm-charts",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"generate-docs": "readme-generator --readme README.md --values values.yaml"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@bitnami/readme-generator-for-helm": "^2.6.0"
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ stringData:
|
||||
"DATABASE_URL" (include "trigger.postgresql.connectionString" .) }}
|
||||
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace (include "trigger.name" .)) | default dict }}
|
||||
{{- $secretData := (get $secretObj "data") | default dict }}
|
||||
{{ range $key, $value := .Values.envVars }}
|
||||
{{ range $key, $value := .Values.trigger.env }}
|
||||
{{- $default := get $requiredVars $key -}}
|
||||
{{- $current := get $secretData $key | b64dec -}}
|
||||
{{- $v := $value | default ($current | default $default) -}}
|
||||
|
||||
+97
-69
@@ -10,7 +10,7 @@ nameOverride: ""
|
||||
##
|
||||
fullnameOverride: ""
|
||||
|
||||
## @section trigger -- main app
|
||||
## @section Trigger.dev parameters
|
||||
##
|
||||
trigger:
|
||||
## @param trigger.name
|
||||
@@ -39,8 +39,8 @@ trigger:
|
||||
## @param trigger.image.pullPolicy trigger image pullPolicy
|
||||
##
|
||||
pullPolicy: Always
|
||||
## @param trigger.resources.limits.memory container memory limit [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
|
||||
## @param trigger.resources.requests.cpu container CPU request [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
|
||||
## @param trigger.resources.limits.memory container memory limit [(docs)](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
|
||||
## @param trigger.resources.requests.cpu container CPU requests [(docs)](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
|
||||
##
|
||||
resources:
|
||||
limits:
|
||||
@@ -65,27 +65,27 @@ trigger:
|
||||
## @param trigger.service.nodePort trigger service nodePort (used if above type is `NodePort`)
|
||||
##
|
||||
nodePort: ""
|
||||
## @skip trigger.env
|
||||
##
|
||||
env:
|
||||
ENCRYPTION_KEY: ""
|
||||
MAGIC_LINK_SECRET: ""
|
||||
SESSION_SECRET: ""
|
||||
LOGIN_ORIGIN: ""
|
||||
APP_ORIGIN: ""
|
||||
DIRECT_URL: ""
|
||||
DATABASE_URL: ""
|
||||
FROM_EMAIL: ""
|
||||
REPLY_TO_EMAIL: ""
|
||||
RESEND_API_KEY: ""
|
||||
AUTH_GITHUB_CLIENT_ID: ""
|
||||
AUTH_GITHUB_CLIENT_SECRET: ""
|
||||
|
||||
## trigger environment variables configuration
|
||||
envVars:
|
||||
ENCRYPTION_KEY: ""
|
||||
MAGIC_LINK_SECRET: ""
|
||||
SESSION_SECRET: ""
|
||||
LOGIN_ORIGIN: ""
|
||||
APP_ORIGIN: ""
|
||||
DIRECT_URL: ""
|
||||
DATABASE_URL: ""
|
||||
FROM_EMAIL: ""
|
||||
REPLY_TO_EMAIL: ""
|
||||
RESEND_API_KEY: ""
|
||||
AUTH_GITHUB_CLIENT_ID: ""
|
||||
AUTH_GITHUB_CLIENT_SECRET: ""
|
||||
|
||||
## @section Postgresql(®) parameters
|
||||
## Documentation : https://github.com/bitnami/charts/tree/main/bitnami/postgresql-ha
|
||||
## @section Postgres parameters
|
||||
## Documentation: https://github.com/bitnami/charts/tree/main/bitnami/postgresql-ha
|
||||
##
|
||||
postgresql:
|
||||
## @param postgresql.enabled Enable Postgresql(®)
|
||||
## @param postgresql.enabled Enable Postgres
|
||||
##
|
||||
enabled: true
|
||||
## @param postgresql.name Name used to build variables (deprecated)
|
||||
@@ -94,27 +94,32 @@ postgresql:
|
||||
## @param postgresql.nameOverride Name override
|
||||
##
|
||||
nameOverride: "postgresql"
|
||||
## @param fullnameOverride String to fully override common.names.fullname template
|
||||
## @param postgresql.fullnameOverride Fullname override
|
||||
##
|
||||
fullnameOverride: "postgresql"
|
||||
|
||||
global:
|
||||
postgresql:
|
||||
## @param global.postgresql.auth.postgresPassword Password for the "postgres" admin user (overrides `auth.postgresPassword`)
|
||||
## @param global.postgresql.auth.username Name for a custom user to create (overrides `auth.username`)
|
||||
## @param global.postgresql.auth.password Password for the custom user to create (overrides `auth.password`)
|
||||
## @param global.postgresql.auth.database Name for a custom database to create (overrides `auth.database`)
|
||||
## @param global.postgresql.auth.existingSecret Name of existing secret to use for PostgreSQL credentials (overrides `auth.existingSecret`).
|
||||
## @param global.postgresql.auth.secretKeys.adminPasswordKey Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.adminPasswordKey`). Only used when `global.postgresql.auth.existingSecret` is set.
|
||||
## @param global.postgresql.auth.secretKeys.userPasswordKey Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.userPasswordKey`). Only used when `global.postgresql.auth.existingSecret` is set.
|
||||
## @param global.postgresql.auth.secretKeys.replicationPasswordKey Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.replicationPasswordKey`). Only used when `global.postgresql.auth.existingSecret` is set.
|
||||
## @param postgresql.global.postgresql.auth.postgresPassword Password for the "postgres" admin user (overrides `auth.postgresPassword`)
|
||||
## @param postgresql.global.postgresql.auth.username Name for a custom user to create (overrides `auth.username`)
|
||||
## @param postgresql.global.postgresql.auth.password Password for the custom user to create (overrides `auth.password`)
|
||||
## @param postgresql.global.postgresql.auth.database Name for a custom database to create (overrides `auth.database`)
|
||||
## @param postgresql.global.postgresql.auth.existingSecret Name of existing secret to use for PostgreSQL credentials (overrides `auth.existingSecret`).
|
||||
## @param postgresql.global.postgresql.auth.secretKeys.adminPasswordKey Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.adminPasswordKey`). Only used when `postgresql.global.postgresql.auth.existingSecret` is set.
|
||||
## @param postgresql.global.postgresql.auth.secretKeys.userPasswordKey Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.userPasswordKey`). Only used when `postgresql.global.postgresql.auth.existingSecret` is set.
|
||||
## @param postgresql.global.postgresql.auth.secretKeys.replicationPasswordKey Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.replicationPasswordKey`). Only used when `postgresql.global.postgresql.auth.existingSecret` is set.
|
||||
##
|
||||
auth:
|
||||
postgresPassword: "password"
|
||||
username: "postgres"
|
||||
password: "password"
|
||||
database: "trigger"
|
||||
## @param global.postgresql.service.ports.postgresql PostgreSQL service port (overrides `service.ports.postgresql`)
|
||||
existingSecret: ""
|
||||
secretKeys:
|
||||
adminPasswordKey: ""
|
||||
userPasswordKey: ""
|
||||
replicationPasswordKey: ""
|
||||
## @param postgresql.global.postgresql.service.ports.postgresql PostgreSQL service port (overrides `service.ports.postgresql`)
|
||||
##
|
||||
service:
|
||||
ports:
|
||||
@@ -122,30 +127,51 @@ postgresql:
|
||||
|
||||
## Bitnami PostgreSQL image version
|
||||
## ref: https://hub.docker.com/r/bitnami/postgresql/tags/
|
||||
## @param image.registry PostgreSQL image registry
|
||||
## @param image.repository PostgreSQL image repository
|
||||
## @param image.tag PostgreSQL image tag (immutable tags are recommended)
|
||||
## @param image.digest PostgreSQL image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag
|
||||
## @param image.pullPolicy PostgreSQL image pull policy
|
||||
## @param image.pullSecrets Specify image pull secrets
|
||||
## @param image.debug Specify if debug values should be set
|
||||
## @param postgresql.image.registry PostgreSQL image registry
|
||||
## @param postgresql.image.repository PostgreSQL image repository
|
||||
## @param postgresql.image.tag PostgreSQL image tag (immutable tags are recommended)
|
||||
## @param postgresql.image.digest PostgreSQL image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag
|
||||
## @param postgresql.image.pullPolicy PostgreSQL image pull policy
|
||||
## @param postgresql.image.pullSecrets Specify image pull secrets
|
||||
## @param postgresql.image.debug Specify if debug values should be set
|
||||
##
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: bitnami/postgresql
|
||||
tag: 16.0.0-debian-11-r13
|
||||
tag: 14.10.0-debian-11-r21
|
||||
digest: ""
|
||||
## Specify a imagePullPolicy
|
||||
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
|
||||
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
|
||||
##
|
||||
pullPolicy: IfNotPresent
|
||||
## Optionally specify an array of imagePullSecrets.
|
||||
## Secrets must be manually created in the namespace.
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
## Example:
|
||||
## pullSecrets:
|
||||
## - myRegistryKeySecretName
|
||||
##
|
||||
pullSecrets: []
|
||||
## Set to true if you would like to see extra information on logs
|
||||
##
|
||||
debug: false
|
||||
|
||||
## @param postgresql.architecture PostgreSQL architecture (`standalone` or `replication`)
|
||||
##
|
||||
architecture: standalone
|
||||
## Replication configuration
|
||||
## Ignored if `architecture` is `standalone`
|
||||
## Ignored if `postgresql.architecture` is `standalone`
|
||||
##
|
||||
## @param containerPorts.postgresql PostgreSQL container port
|
||||
## @param postgresql.containerPorts.postgresql PostgreSQL container port
|
||||
##
|
||||
containerPorts:
|
||||
postgresql: 5432
|
||||
|
||||
## @param postgresql.postgresqlDataDir PostgreSQL data dir
|
||||
##
|
||||
postgresqlDataDir: /bitnami/postgresql/data
|
||||
## @param postgresqlSharedPreloadLibraries Shared preload libraries (comma-separated list)
|
||||
## @param postgresql.postgresqlSharedPreloadLibraries Shared preload libraries (comma-separated list)
|
||||
##
|
||||
postgresqlSharedPreloadLibraries: "pgaudit"
|
||||
## @section PostgreSQL Primary parameters
|
||||
@@ -153,12 +179,12 @@ postgresql:
|
||||
primary:
|
||||
## Configure extra options for PostgreSQL Primary containers' liveness, readiness and startup probes
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes
|
||||
## @param primary.livenessProbe.enabled Enable livenessProbe on PostgreSQL Primary containers
|
||||
## @param primary.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe
|
||||
## @param primary.livenessProbe.periodSeconds Period seconds for livenessProbe
|
||||
## @param primary.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe
|
||||
## @param primary.livenessProbe.failureThreshold Failure threshold for livenessProbe
|
||||
## @param primary.livenessProbe.successThreshold Success threshold for livenessProbe
|
||||
## @param postgresql.primary.livenessProbe.enabled Enable livenessProbe on PostgreSQL Primary containers
|
||||
## @param postgresql.primary.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe
|
||||
## @param postgresql.primary.livenessProbe.periodSeconds Period seconds for livenessProbe
|
||||
## @param postgresql.primary.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe
|
||||
## @param postgresql.primary.livenessProbe.failureThreshold Failure threshold for livenessProbe
|
||||
## @param postgresql.primary.livenessProbe.successThreshold Success threshold for livenessProbe
|
||||
##
|
||||
livenessProbe:
|
||||
enabled: true
|
||||
@@ -167,12 +193,12 @@ postgresql:
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
successThreshold: 1
|
||||
## @param primary.readinessProbe.enabled Enable readinessProbe on PostgreSQL Primary containers
|
||||
## @param primary.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe
|
||||
## @param primary.readinessProbe.periodSeconds Period seconds for readinessProbe
|
||||
## @param primary.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe
|
||||
## @param primary.readinessProbe.failureThreshold Failure threshold for readinessProbe
|
||||
## @param primary.readinessProbe.successThreshold Success threshold for readinessProbe
|
||||
## @param postgresql.primary.readinessProbe.enabled Enable readinessProbe on PostgreSQL Primary containers
|
||||
## @param postgresql.primary.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe
|
||||
## @param postgresql.primary.readinessProbe.periodSeconds Period seconds for readinessProbe
|
||||
## @param postgresql.primary.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe
|
||||
## @param postgresql.primary.readinessProbe.failureThreshold Failure threshold for readinessProbe
|
||||
## @param postgresql.primary.readinessProbe.successThreshold Success threshold for readinessProbe
|
||||
##
|
||||
readinessProbe:
|
||||
enabled: true
|
||||
@@ -181,12 +207,12 @@ postgresql:
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
successThreshold: 1
|
||||
## @param primary.startupProbe.enabled Enable startupProbe on PostgreSQL Primary containers
|
||||
## @param primary.startupProbe.initialDelaySeconds Initial delay seconds for startupProbe
|
||||
## @param primary.startupProbe.periodSeconds Period seconds for startupProbe
|
||||
## @param primary.startupProbe.timeoutSeconds Timeout seconds for startupProbe
|
||||
## @param primary.startupProbe.failureThreshold Failure threshold for startupProbe
|
||||
## @param primary.startupProbe.successThreshold Success threshold for startupProbe
|
||||
## @param postgresql.primary.startupProbe.enabled Enable startupProbe on PostgreSQL Primary containers
|
||||
## @param postgresql.primary.startupProbe.initialDelaySeconds Initial delay seconds for startupProbe
|
||||
## @param postgresql.primary.startupProbe.periodSeconds Period seconds for startupProbe
|
||||
## @param postgresql.primary.startupProbe.timeoutSeconds Timeout seconds for startupProbe
|
||||
## @param postgresql.primary.startupProbe.failureThreshold Failure threshold for startupProbe
|
||||
## @param postgresql.primary.startupProbe.successThreshold Success threshold for startupProbe
|
||||
##
|
||||
startupProbe:
|
||||
enabled: false
|
||||
@@ -196,21 +222,22 @@ postgresql:
|
||||
failureThreshold: 15
|
||||
successThreshold: 1
|
||||
persistence:
|
||||
## @param primary.persistence.enabled Enable PostgreSQL Primary data persistence using PVC
|
||||
## @param postgresql.primary.persistence.enabled Enable PostgreSQL Primary data persistence using PVC
|
||||
##
|
||||
enabled: true
|
||||
## @param primary.persistence.existingClaim Name of an existing PVC to use
|
||||
## @param postgresql.primary.persistence.existingClaim Name of an existing PVC to use
|
||||
##
|
||||
existingClaim: ""
|
||||
## @param primary.persistence.accessModes PVC Access Mode for PostgreSQL volume
|
||||
## @param postgresql.primary.persistence.accessModes PVC Access Mode for PostgreSQL volume
|
||||
##
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
## @param primary.persistence.size PVC Storage Request for PostgreSQL volume
|
||||
## @param postgresql.primary.persistence.size PVC Storage Request for PostgreSQL volume
|
||||
##
|
||||
size: 8Gi
|
||||
|
||||
## @section Ingress parameters
|
||||
## Documentation: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
##
|
||||
ingress:
|
||||
## @param ingress.enabled Enable ingress
|
||||
@@ -233,11 +260,6 @@ ingress:
|
||||
## Replace with your own domain
|
||||
##
|
||||
hostName: ""
|
||||
## @skip ingress.frontend
|
||||
##
|
||||
trigger:
|
||||
path: /
|
||||
pathType: Prefix
|
||||
## @param ingress.tls Ingress TLS hosts (matching above hostName)
|
||||
## Replace with your own domain
|
||||
##
|
||||
@@ -246,3 +268,9 @@ ingress:
|
||||
# - secretName: letsencrypt-nginx
|
||||
# hosts:
|
||||
# - infisical.local
|
||||
## @param ingress.trigger.path Trigger.dev ingress path
|
||||
## @param ingress.trigger.pathType Trigger.dev ingress path type
|
||||
##
|
||||
trigger:
|
||||
path: /
|
||||
pathType: Prefix
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/airtable
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/airtable",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"airtable": "^0.12.1",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/github
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/github",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/linear
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/linear",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/openai",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.9"
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/plain
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/plain",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"@team-plain/typescript-sdk": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/replicate
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/replicate",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"replicate": "^0.18.1",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/resend
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/resend",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"resend": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/sendgrid
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/sendgrid",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.9"
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/shopify
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/shopify",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/slack",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"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:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# @trigger.dev/stripe
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5af20035: Fix SubtleCryptoProvider webhook validation
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/stripe",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"description": "Trigger.dev integration for stripe",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -25,8 +25,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"stripe": "^12.14.0",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1151,7 +1151,7 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger) {
|
||||
const stripeClient = new StripeClient("", { apiVersion: "2022-11-15" });
|
||||
|
||||
try {
|
||||
const event = stripeClient.webhooks.constructEvent(rawBody, signature, source.secret);
|
||||
const event = await stripeClient.webhooks.constructEventAsync(rawBody, signature, source.secret);
|
||||
|
||||
return {
|
||||
events: [
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/supabase
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/supabase",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"description": "Trigger.dev integration for @supabase/supabase-js",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.26.0",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"supabase-management-js": "^1.0.0",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @trigger.dev/typeform
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
- @trigger.dev/integration-kit@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.3.11
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
- @trigger.dev/integration-kit@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/typeform",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"description": "The official Typeform integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -24,8 +24,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.9",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.3.12",
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12",
|
||||
"@typeform/api-client": "^1.8.0",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/astro
|
||||
|
||||
## 2.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [129f023d]
|
||||
- Updated dependencies [38f5a903]
|
||||
- Updated dependencies [ff4ff869]
|
||||
- @trigger.dev/sdk@2.3.12
|
||||
|
||||
## 2.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@2.3.11
|
||||
|
||||
## 2.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8277f4d2]
|
||||
- Updated dependencies [73cb8839]
|
||||
- @trigger.dev/sdk@2.3.10
|
||||
|
||||
## 2.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@trigger.dev/astro",
|
||||
"description": "An Astro-native integration for Trigger.dev background jobs platform",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
@@ -20,7 +20,7 @@
|
||||
"build:tsup": "tsup"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@trigger.dev/sdk": "workspace:^2.3.9"
|
||||
"@trigger.dev/sdk": "workspace:^2.3.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^3.0.12",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# trigger.dev
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/core@2.3.12
|
||||
@@ -0,0 +1,28 @@
|
||||
# Running the CLI from source
|
||||
|
||||
1. Run the CLI and watch for changes
|
||||
|
||||
```sh
|
||||
cd packages/cli-v3
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
2. In the job-catalog folder you can use the CLI
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
pnpm exec trigger-v3-cli
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If you want to use it in a new folder, you need to first add it as a dev dependency in package.json:
|
||||
|
||||
```json
|
||||
//...
|
||||
"devDependencies": {
|
||||
"trigger.dev": "workspace:*",
|
||||
//...
|
||||
}
|
||||
//...
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Trigger.dev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Trigger.dev CLI
|
||||
|
||||
A CLI that allows you to create, run locally and deploy Trigger.dev background tasks.
|
||||
|
||||
Note: this only works with Trigger.dev v3 projects and later. For older projects use the [@trigger.dev/cli](https://www.npmjs.com/package/@trigger.dev/cli) package.
|
||||
|
||||
Trigger.dev is an open source platform that makes it easy to create event-driven background tasks directly in your existing project.
|
||||
|
||||
## Usage
|
||||
|
||||
### Login
|
||||
|
||||
Logs that machine into Trigger.dev by creating a new Personal Access Token and storing it on the local machine. Once you're logged in you can perform the other actions below.
|
||||
|
||||
```sh
|
||||
npx trigger.dev@latest login
|
||||
```
|
||||
|
||||
| Option | Short option | Description |
|
||||
| ----------- | ------------ | -------------------------------------------------------------------- |
|
||||
| `--api-url` | `-a` | Set the API URL for Trigger.dev, defaults to https://api.trigger.dev |
|
||||
|
||||
### Update
|
||||
|
||||
Will update all of your @trigger.dev packages in your package.json to the latest version.
|
||||
|
||||
```sh
|
||||
npx trigger.dev@latest update
|
||||
```
|
||||
|
||||
You can pass the path to the folder that your package.json file lives in:
|
||||
|
||||
```sh
|
||||
npx trigger.dev@latest update ./myapp
|
||||
```
|
||||
|
||||
| Option | Short option | Description |
|
||||
| ------ | ------------ | ---------------------------------------------------------- |
|
||||
| `--to` | `-t` | The version to update to (ex: 2.1.4), defaults to "latest" |
|
||||
|
||||
### Who Am I?
|
||||
|
||||
Shows the current user that is logged in.
|
||||
|
||||
```sh
|
||||
npx trigger.dev@latest whoami
|
||||
```
|
||||
|
||||
| Option | Short option | Description |
|
||||
| ----------- | ------------ | -------------------------------------------------------------------- |
|
||||
| `--api-url` | `-a` | Set the API URL for Trigger.dev, defaults to https://api.trigger.dev |
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "trigger.dev",
|
||||
"version": "1.0.1",
|
||||
"description": "A Command-Line Interface for Trigger.dev (v3) projects",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/triggerdotdev/trigger.dev.git",
|
||||
"directory": "packages/cli-v3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"typescript",
|
||||
"trigger.dev",
|
||||
"workflows",
|
||||
"orchestration",
|
||||
"events",
|
||||
"webhooks",
|
||||
"integrations",
|
||||
"apis",
|
||||
"jobs",
|
||||
"background jobs",
|
||||
"nextjs"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
"bin": {
|
||||
"trigger-v3-cli": "./dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@types/gradient-string": "^1.1.2",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "16",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/ws": "^8.5.3",
|
||||
"open": "^10.0.3",
|
||||
"p-retry": "^6.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"tsup": "^6.5.0",
|
||||
"type-fest": "^3.6.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vitest": "^0.34.4",
|
||||
"xdg-app-paths": "^8.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc",
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"clean": "rimraf dist",
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@types/degit": "^2.8.3",
|
||||
"chalk": "^5.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"cli-table3": "^0.6.3",
|
||||
"commander": "^9.4.1",
|
||||
"degit": "^2.8.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"execa": "^7.0.0",
|
||||
"gradient-string": "^2.0.2",
|
||||
"liquidjs": "^10.9.2",
|
||||
"mock-fs": "^5.2.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"node-fetch": "^3.3.0",
|
||||
"npm-check-updates": "^16.12.2",
|
||||
"posthog-node": "^3.1.1",
|
||||
"proxy-agent": "^6.3.0",
|
||||
"simple-git": "^3.19.0",
|
||||
"update-check": "^1.5.4",
|
||||
"url": "^0.11.1",
|
||||
"ws": "^8.11.0",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CreateAuthorizationCodeResponseSchema,
|
||||
GetPersonalAccessTokenResponseSchema,
|
||||
WhoAmIResponseSchema,
|
||||
} from "@trigger.dev/core";
|
||||
|
||||
export class ApiClient {
|
||||
constructor(private readonly apiURL: string) {
|
||||
this.apiURL = apiURL;
|
||||
}
|
||||
|
||||
async createAuthorizationCode() {
|
||||
return zodfetch(
|
||||
CreateAuthorizationCodeResponseSchema,
|
||||
`${this.apiURL}/api/v1/authorization-code`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getPersonalAccessToken(authorizationCode: string) {
|
||||
return zodfetch(GetPersonalAccessTokenResponseSchema, `${this.apiURL}/api/v1/token`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
authorizationCode,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async whoAmI({ accessToken }: { accessToken: string }) {
|
||||
return zodfetch(WhoAmIResponseSchema, `${this.apiURL}/api/v2/whoami`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type ApiResult<TSuccessResult> =
|
||||
| { success: true; data: TSuccessResult }
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
async function zodfetch<TResponseBody extends any>(
|
||||
schema: z.Schema<TResponseBody>,
|
||||
url: string,
|
||||
requestInit?: RequestInit
|
||||
): Promise<ApiResult<TResponseBody>> {
|
||||
try {
|
||||
const response = await fetch(url, requestInit);
|
||||
|
||||
if ((!requestInit || requestInit.method === "GET") && response.status === 404) {
|
||||
return {
|
||||
success: false,
|
||||
error: `404: ${response.statusText}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status >= 400 && response.status < 500) {
|
||||
const body = await response.json();
|
||||
if (!body.error) {
|
||||
return { success: false, error: "Something went wrong" };
|
||||
}
|
||||
|
||||
return { success: false, error: body.error };
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to fetch ${url}, got status code ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const jsonBody = await response.json();
|
||||
const parsedResult = schema.safeParse(jsonBody);
|
||||
|
||||
if (parsedResult.success) {
|
||||
return { success: true, data: parsedResult.data };
|
||||
}
|
||||
|
||||
if ("error" in jsonBody) {
|
||||
return {
|
||||
success: false,
|
||||
error: typeof jsonBody.error === "string" ? jsonBody.error : JSON.stringify(jsonBody.error),
|
||||
};
|
||||
}
|
||||
|
||||
return { success: false, error: parsedResult.error.message };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : JSON.stringify(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Command } from "commander";
|
||||
import { devCommand } from "../commands/dev";
|
||||
import { updateCommand } from "../commands/update";
|
||||
import { whoamiCommand } from "../commands/whoami.js";
|
||||
import { COMMAND_NAME } from "../consts";
|
||||
import { getVersion } from "../utilities/getVersion";
|
||||
import { printInitialBanner } from "../utilities/initialBanner";
|
||||
import { login, loginCommand } from "../commands/login";
|
||||
import { z } from "zod";
|
||||
|
||||
export const program = new Command();
|
||||
|
||||
export const ApiUrlOptionsSchema = z.object({
|
||||
apiUrl: z.string(),
|
||||
});
|
||||
|
||||
program
|
||||
.name(COMMAND_NAME)
|
||||
.description("Create, run locally and deploy Trigger.dev background tasks.")
|
||||
.version(getVersion(), "-v, --version", "Display the version number");
|
||||
|
||||
program
|
||||
.command("login")
|
||||
.description("Login with Trigger.dev so you can perform authenticated actions")
|
||||
.option(
|
||||
"-a, --api-url <value>",
|
||||
"Override the API URL, defaults to https://api.trigger.dev",
|
||||
"https://api.trigger.dev"
|
||||
)
|
||||
.version(getVersion(), "-v, --version", "Display the version number")
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await printInitialBanner(false);
|
||||
await loginCommand(options);
|
||||
//todo login command
|
||||
} catch (e) {
|
||||
//todo error reporting
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
//todo update for the new version
|
||||
//todo add usage instructions to the README
|
||||
program
|
||||
.command("dev")
|
||||
.description("Run your Trigger.dev tasks locally")
|
||||
.argument("[path]", "The path to the project", ".")
|
||||
.option("-p, --port <port>", "Override the local port your server is on")
|
||||
.option("-H, --hostname <hostname>", "Override the hostname on which the application is served")
|
||||
.option("-e, --env-file <name>", "Override the name of the env file to load")
|
||||
.option(
|
||||
"-i, --client-id <name>",
|
||||
"The ID of the client to use for this project. Will use the value from the package.json file if not provided."
|
||||
)
|
||||
.version(getVersion(), "-v, --version", "Display the version number")
|
||||
.action(async (path, options) => {
|
||||
try {
|
||||
await printInitialBanner();
|
||||
await devCommand(path, options);
|
||||
} catch (e) {
|
||||
//todo error reporting
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("update")
|
||||
.description(
|
||||
"Updates all @trigger.dev/* packages to their latest compatible versions or the specified version"
|
||||
)
|
||||
.argument("[path]", "The path to the directory that contains the package.json file", ".")
|
||||
.option("-t, --to <version tag>", "The version to update to (ex: 2.1.4)", "latest")
|
||||
.action(async (path, options) => {
|
||||
await printInitialBanner(false);
|
||||
await updateCommand(path, options);
|
||||
});
|
||||
|
||||
program
|
||||
.command("whoami")
|
||||
.description("display the current logged in user and project details")
|
||||
.option(
|
||||
"-a, --api-url <value>",
|
||||
"Override the API URL, defaults to https://cloud.trigger.dev",
|
||||
"https://cloud.trigger.dev"
|
||||
)
|
||||
.version(getVersion(), "-v, --version", "Display the version number")
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await printInitialBanner();
|
||||
await whoamiCommand(options);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import childProcess from "child_process";
|
||||
import util from "util";
|
||||
import { z } from "zod";
|
||||
import { telemetryClient } from "../telemetry/telemetry";
|
||||
import { logger } from "../utilities/logger";
|
||||
import { resolvePath } from "../utilities/parseNameAndPath";
|
||||
import { RequireKeys } from "../utilities/requiredKeys";
|
||||
|
||||
const asyncExecFile = util.promisify(childProcess.execFile);
|
||||
|
||||
export const DevCommandOptionsSchema = z.object({
|
||||
port: z.coerce.number().optional(),
|
||||
hostname: z.string().optional(),
|
||||
envFile: z.string().optional(),
|
||||
clientId: z.string().optional(),
|
||||
});
|
||||
|
||||
export type DevCommandOptions = z.infer<typeof DevCommandOptionsSchema>;
|
||||
type ResolvedOptions = RequireKeys<DevCommandOptions, "envFile">;
|
||||
|
||||
const formattedDate = new Intl.DateTimeFormat("en", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
});
|
||||
|
||||
export async function devCommand(path: string, anyOptions: any) {
|
||||
telemetryClient.dev.started(path, anyOptions);
|
||||
|
||||
const result = DevCommandOptionsSchema.safeParse(anyOptions);
|
||||
if (!result.success) {
|
||||
logger.error(result.error.message);
|
||||
|
||||
return;
|
||||
}
|
||||
const options = result.data;
|
||||
|
||||
const resolvedPath = resolvePath(path);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { intro, log, outro, select, spinner } from "@clack/prompts";
|
||||
import open from "open";
|
||||
import pRetry, { AbortError } from "p-retry";
|
||||
import { ApiClient } from "../apiClient";
|
||||
import { ApiUrlOptionsSchema } from "../cli";
|
||||
import { chalkLink } from "../utilities/colors";
|
||||
import { readAuthConfigFile, writeAuthConfigFile } from "../utilities/configFiles";
|
||||
import { logger } from "../utilities/logger";
|
||||
import { whoAmI } from "./whoami";
|
||||
|
||||
export async function loginCommand(options: any) {
|
||||
const result = ApiUrlOptionsSchema.safeParse(options);
|
||||
if (!result.success) {
|
||||
logger.error(result.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
return login(result.data.apiUrl);
|
||||
}
|
||||
|
||||
export type LoginResult =
|
||||
| {
|
||||
success: true;
|
||||
accessToken: string;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export async function login(apiUrl: string): Promise<LoginResult> {
|
||||
const apiClient = new ApiClient(apiUrl);
|
||||
|
||||
intro("Logging in to Trigger.dev");
|
||||
|
||||
const existingAccessToken = readAuthConfigFile()?.accessToken;
|
||||
if (existingAccessToken) {
|
||||
const whoAmiI = await whoAmI(apiUrl);
|
||||
|
||||
const continueOption = await select({
|
||||
message: "You are already logged in.",
|
||||
options: [
|
||||
{
|
||||
value: false,
|
||||
label: "Exit",
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
label: "Login with a different account",
|
||||
},
|
||||
],
|
||||
initialValue: false,
|
||||
});
|
||||
|
||||
if (continueOption !== true) {
|
||||
outro("Already logged in");
|
||||
return {
|
||||
success: true,
|
||||
accessToken: existingAccessToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//generate authorization code
|
||||
const createAuthCodeSpinner = spinner();
|
||||
createAuthCodeSpinner.start("Creating authorition code");
|
||||
const authorizationCodeResult = await apiClient.createAuthorizationCode();
|
||||
if (!authorizationCodeResult.success) {
|
||||
createAuthCodeSpinner.stop(
|
||||
`Failed to create authorization code\n${authorizationCodeResult.error}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: authorizationCodeResult.error,
|
||||
};
|
||||
}
|
||||
createAuthCodeSpinner.stop("Created authorization code");
|
||||
|
||||
//Link the user to the authorization code
|
||||
log.step(
|
||||
`Please visit the following URL to login:\n${chalkLink(authorizationCodeResult.data.url)}`
|
||||
);
|
||||
await open(authorizationCodeResult.data.url);
|
||||
|
||||
//poll for personal access token (we need to poll for it)
|
||||
const getPersonalAccessTokenSpinner = spinner();
|
||||
getPersonalAccessTokenSpinner.start("Waiting for you to login");
|
||||
try {
|
||||
const indexResult = await pRetry(
|
||||
() => getPersonalAccessToken(apiClient, authorizationCodeResult.data.authorizationCode),
|
||||
{
|
||||
//this means we're polling, same distance between each attempt
|
||||
factor: 1,
|
||||
retries: 60,
|
||||
minTimeout: 1000,
|
||||
}
|
||||
);
|
||||
|
||||
getPersonalAccessTokenSpinner.stop(`Logged in with token ${indexResult.obfuscatedToken}`);
|
||||
|
||||
writeAuthConfigFile({ accessToken: indexResult.token });
|
||||
|
||||
outro("Logged in successfully");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
accessToken: indexResult.token,
|
||||
};
|
||||
} catch (e) {
|
||||
getPersonalAccessTokenSpinner.stop(`Failed to get access token`);
|
||||
if (e instanceof AbortError) {
|
||||
log.error(e.message);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: e instanceof Error ? e.message : JSON.stringify(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getPersonalAccessToken(apiClient: ApiClient, authorizationCode: string) {
|
||||
const token = await apiClient.getPersonalAccessToken(authorizationCode);
|
||||
|
||||
if (!token.success) {
|
||||
throw new AbortError(token.error);
|
||||
}
|
||||
|
||||
if (!token.data.token) {
|
||||
throw new Error("No token found yet");
|
||||
}
|
||||
|
||||
return {
|
||||
token: token.data.token.token,
|
||||
obfuscatedToken: token.data.token.obfuscatedToken,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { spinner, confirm } from "@clack/prompts";
|
||||
import path from "path";
|
||||
import { run, RunOptions } from "npm-check-updates";
|
||||
import { installDependencies } from "../utilities/installDependencies";
|
||||
import { readJSONFileSync, writeJSONFile } from "../utilities/fileSystem.js";
|
||||
import { logger } from "../utilities/logger.js";
|
||||
import { z } from "zod";
|
||||
import { chalkError, chalkSuccess } from "../utilities/colors";
|
||||
|
||||
export const UpdateCommandOptionsSchema = z.object({
|
||||
to: z.string().optional(),
|
||||
});
|
||||
|
||||
export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptionsSchema>;
|
||||
|
||||
type NcuRunOptionTarget = "latest" | `@${string}`;
|
||||
|
||||
export async function updateCommand(projectPath: string, anyOptions: any) {
|
||||
const loadingSpinner = spinner();
|
||||
loadingSpinner.start("Checking settings");
|
||||
|
||||
const parseRes = UpdateCommandOptionsSchema.safeParse(anyOptions);
|
||||
if (!parseRes.success) {
|
||||
loadingSpinner.stop(chalkError(parseRes.error.message));
|
||||
return;
|
||||
}
|
||||
const options = parseRes.data;
|
||||
|
||||
const triggerDevPackage = "@trigger.dev";
|
||||
const packageJSONPath = path.join(projectPath, "package.json");
|
||||
const packageData = readJSONFileSync(packageJSONPath);
|
||||
if (!packageData) {
|
||||
loadingSpinner.stop(chalkError("Couldn't load package.json"));
|
||||
return;
|
||||
}
|
||||
|
||||
loadingSpinner.message("Checking for updates");
|
||||
|
||||
const packageMaps: { [k: string]: { type: string; version: string } } = {};
|
||||
const packageDependencies = packageData.dependencies || {};
|
||||
const packageDevDependencies = packageData.devDependencies || {};
|
||||
Object.keys(packageDependencies).forEach((i) => {
|
||||
packageMaps[i] = { type: "dependencies", version: packageDependencies[i] };
|
||||
});
|
||||
Object.keys(packageDevDependencies).forEach((i) => {
|
||||
packageMaps[i] = {
|
||||
type: "devDependencies",
|
||||
version: packageDevDependencies[i],
|
||||
};
|
||||
});
|
||||
|
||||
const targetVersion = getTargetVersion(options.to);
|
||||
|
||||
// Use npm-check-updates to get updated dependency versions
|
||||
const ncuOptions: RunOptions = {
|
||||
packageData,
|
||||
upgrade: true,
|
||||
jsonUpgraded: true,
|
||||
target: targetVersion,
|
||||
};
|
||||
|
||||
// Can either give a json like package.json or just with deps and their new versions
|
||||
const updatedDependencies: { [k: string]: any } | void = await run(ncuOptions);
|
||||
|
||||
if (!updatedDependencies) {
|
||||
loadingSpinner.stop(chalkError("Couldn't update dependencies"));
|
||||
return;
|
||||
}
|
||||
|
||||
const ifUpdatedDependenciesIsPackageJSON =
|
||||
updatedDependencies.hasOwnProperty("dependencies") ||
|
||||
updatedDependencies.hasOwnProperty("devDependencies");
|
||||
|
||||
const dependencies = updatedDependencies.dependencies || {};
|
||||
const devDependencies = updatedDependencies.devDependencies || {};
|
||||
|
||||
const allDependencies = ifUpdatedDependenciesIsPackageJSON
|
||||
? Object.keys({ ...dependencies, ...devDependencies })
|
||||
: Object.keys(updatedDependencies);
|
||||
|
||||
const triggerPackages = allDependencies.filter((pkg) => pkg.startsWith(triggerDevPackage));
|
||||
|
||||
// If there are no @trigger.dev packages
|
||||
if (triggerPackages.length === 0) {
|
||||
loadingSpinner.stop(chalkSuccess(`All @trigger.dev/* packages are already up to date.`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter the packages with null and what don't match what
|
||||
// they are installed with so that they can be updated
|
||||
const packagesToUpdate = triggerPackages.filter((pkg: string) => updatedDependencies[pkg]);
|
||||
|
||||
// If no packages require any updation
|
||||
if (packagesToUpdate.length === 0) {
|
||||
loadingSpinner.stop(chalkSuccess(`All @trigger.dev/* packages are already up to date.`));
|
||||
return;
|
||||
}
|
||||
|
||||
let applyUpdates = targetVersion !== "latest";
|
||||
|
||||
if (targetVersion === "latest") {
|
||||
applyUpdates = await hasUserConfirmed(packagesToUpdate, packageMaps, updatedDependencies);
|
||||
}
|
||||
|
||||
if (applyUpdates) {
|
||||
const newPackageJSON = packageData;
|
||||
packagesToUpdate.forEach((packageName) => {
|
||||
const tmp = packageMaps[packageName];
|
||||
if (tmp) {
|
||||
newPackageJSON[tmp.type][packageName] = updatedDependencies[packageName];
|
||||
}
|
||||
});
|
||||
await writeJSONFile(packageJSONPath, newPackageJSON);
|
||||
await installDependencies(projectPath);
|
||||
}
|
||||
}
|
||||
|
||||
// expects a version number, or latest.
|
||||
// if version number is specified, prepend it with '@' for ncu.
|
||||
function getTargetVersion(toVersion?: string): NcuRunOptionTarget {
|
||||
if (!toVersion) {
|
||||
return "latest";
|
||||
}
|
||||
return toVersion === "latest" ? "latest" : `@${toVersion}`;
|
||||
}
|
||||
|
||||
async function hasUserConfirmed(
|
||||
packagesToUpdate: string[],
|
||||
packageMaps: { [x: string]: { type: string; version: string } },
|
||||
updatedDependencies: { [x: string]: any }
|
||||
): Promise<boolean> {
|
||||
// Inform the user of the dependencies that can be updated
|
||||
console.log("\nNewer versions found for the following packages:");
|
||||
console.table(
|
||||
packagesToUpdate.map((i) => ({
|
||||
name: i,
|
||||
old: packageMaps[i]?.version,
|
||||
new: updatedDependencies[i],
|
||||
}))
|
||||
);
|
||||
|
||||
// Ask the user if they want to update the dependencies
|
||||
const shouldContinue = await confirm({
|
||||
message: "Do you want to update these packages in package.json and re-install dependencies?",
|
||||
});
|
||||
|
||||
return shouldContinue as boolean;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { note, spinner } from "@clack/prompts";
|
||||
import { ApiUrlOptionsSchema } from "../cli";
|
||||
import { logger } from "../utilities/logger";
|
||||
import { resolvePath } from "../utilities/parseNameAndPath";
|
||||
import { readAuthConfigFile } from "../utilities/configFiles";
|
||||
import { login } from "./login";
|
||||
import { ApiClient } from "../apiClient";
|
||||
|
||||
type WhoAmIResult =
|
||||
| {
|
||||
success: true;
|
||||
data: {
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export async function whoamiCommand(options: any): Promise<WhoAmIResult> {
|
||||
const result = ApiUrlOptionsSchema.safeParse(options);
|
||||
if (!result.success) {
|
||||
logger.error(result.error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: result.error.message,
|
||||
};
|
||||
}
|
||||
|
||||
return whoAmI(result.data.apiUrl);
|
||||
}
|
||||
|
||||
export async function whoAmI(apiUrl: string): Promise<WhoAmIResult> {
|
||||
const loadingSpinner = spinner();
|
||||
loadingSpinner.start("Checking your account details");
|
||||
|
||||
if (!readAuthConfigFile()?.accessToken) {
|
||||
loadingSpinner.stop("You must login.");
|
||||
const loginResult = await login(apiUrl);
|
||||
if (!loginResult.success) {
|
||||
logger.error(loginResult.error);
|
||||
return {
|
||||
success: false,
|
||||
error: loginResult.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const accessToken = readAuthConfigFile()?.accessToken;
|
||||
if (!accessToken) {
|
||||
logger.error("No access token after login… this should never happen");
|
||||
return {
|
||||
success: false,
|
||||
error: "No access token after login… this should never happen",
|
||||
};
|
||||
}
|
||||
|
||||
const apiClient = new ApiClient(apiUrl);
|
||||
const userData = await apiClient.whoAmI({ accessToken });
|
||||
|
||||
if (!userData.success) {
|
||||
loadingSpinner.stop("Error getting your account details");
|
||||
logger.error(userData.error);
|
||||
return {
|
||||
success: false,
|
||||
error: userData.error,
|
||||
};
|
||||
}
|
||||
|
||||
loadingSpinner.stop("Retrieved your account details");
|
||||
|
||||
note(
|
||||
`User ID: ${userData.data.userId}
|
||||
Email: ${userData.data.email}`,
|
||||
"Account details"
|
||||
);
|
||||
|
||||
return userData;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// With the move to TSUP as a build tool, this keeps path routes in other files (installers, loaders, etc) in check more easily.
|
||||
// Path is in relation to a single index.js file inside ./dist
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const distPath = path.dirname(__filename);
|
||||
|
||||
export const PKG_ROOT = path.join(distPath, "../");
|
||||
export const COMMAND_NAME = "trigger.dev";
|
||||
export const CLOUD_WEB_URL = "https://cloud.trigger.dev";
|
||||
export const CLOUD_API_URL = "https://api.trigger.dev";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user