Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86cacdfa33 | |||
| 53bdc66aa5 | |||
| 55a06701e2 | |||
| 5ce6f76e14 | |||
| 51202cde70 | |||
| 03e392a467 | |||
| 623ad20b44 | |||
| be744dd1f8 | |||
| 78cb8130f6 | |||
| 171f6b2330 |
@@ -0,0 +1,15 @@
|
||||
export function SideMenuRightClosedIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="12" y="4" width="1" height="12" fill="currentColor" />
|
||||
<rect x="2.5" y="3.5" width="15" height="13" rx="2.5" stroke="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/24/solid";
|
||||
import { DiscordIcon } from "@trigger.dev/companyicons";
|
||||
import { User } from "@trigger.dev/database";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { accountPath, personalAccessTokensPath, rootPath } from "~/utils/pathBuilder";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { SideMenuHeader } from "./SideMenuHeader";
|
||||
import { SideMenuItem } from "./SideMenuItem";
|
||||
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
|
||||
|
||||
export function AccountSideMenu({ user }: { user: User }) {
|
||||
const { v3Enabled } = useFeatures();
|
||||
@@ -20,7 +19,7 @@ export function AccountSideMenu({ user }: { user: User }) {
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className={cn("flex items-center justify-between border-b p-px transition")}>
|
||||
<div className={cn("flex h-10 items-center justify-between border-b p-1 transition")}>
|
||||
<LinkButton
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={ArrowLeftIcon}
|
||||
@@ -57,42 +56,7 @@ export function AccountSideMenu({ user }: { user: User }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 border-t border-grid-bright 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>
|
||||
}
|
||||
/>
|
||||
<HelpAndFeedback />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
CalendarDaysIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
EnvelopeIcon,
|
||||
LightBulbIcon,
|
||||
SignalIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
|
||||
import { Fragment, useState } from "react";
|
||||
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { StepContentContainer } from "../StepContentContainer";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
|
||||
import { Icon } from "../primitives/Icon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Popover, PopoverContent, PopoverSideMenuTrigger } from "../primitives/Popover";
|
||||
import { StepNumber } from "../primitives/StepNumber";
|
||||
import { MenuCount, SideMenuItem } from "./SideMenuItem";
|
||||
|
||||
export function HelpAndFeedback() {
|
||||
const [isHelpMenuOpen, setHelpMenuOpen] = useState(false);
|
||||
const currentPlan = useCurrentPlan();
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={(open) => setHelpMenuOpen(open)}>
|
||||
<PopoverSideMenuTrigger isOpen={isHelpMenuOpen} shortcut={{ key: "h" }}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ChatBubbleLeftEllipsisIcon className="size-4 text-success" />
|
||||
Help & Feedback
|
||||
</div>
|
||||
</PopoverSideMenuTrigger>
|
||||
<PopoverContent
|
||||
className="min-w-[14rem] divide-y divide-grid-bright overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
align="start"
|
||||
>
|
||||
<Fragment>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
name="Documentation"
|
||||
icon="docs"
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-green-500"
|
||||
activeIconColor="text-green-500"
|
||||
to="https://trigger.dev/docs"
|
||||
data-action="documentation"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
name="Status"
|
||||
icon={SignalIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-green-500"
|
||||
activeIconColor="text-green-500"
|
||||
to="https://status.trigger.dev/"
|
||||
data-action="status"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Suggest a feature"
|
||||
icon={LightBulbIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-sun-500"
|
||||
activeIconColor="text-sun-500"
|
||||
to="https://feedback.trigger.dev/"
|
||||
data-action="suggest-a-feature"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Changelog"
|
||||
icon="star"
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-sun-500"
|
||||
activeIconColor="text-sun-500"
|
||||
to="https://trigger.dev/changelog"
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<Paragraph className="pb-1 pl-1.5 pt-1.5 text-xs">Need help?</Paragraph>
|
||||
{currentPlan?.v3Subscription?.plan?.limits.support === "slack" && (
|
||||
<div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={SlackIcon}
|
||||
data-action="join-our-slack"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="text-text-bright">Join our Slack…</span>
|
||||
<MenuCount count="PRO" />
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>Join our Slack</DialogHeader>
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Icon icon={SlackIcon} className="h-10 w-10 min-w-[2.5rem]" />
|
||||
<Paragraph variant="base/bright">
|
||||
As a subscriber, you have access to a dedicated Slack channel for 1-to-1
|
||||
support with the Trigger.dev team.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<hr className="border-charcoal-800" />
|
||||
<div>
|
||||
<StepNumber stepNumber="1" title="Email us" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Send us an email to this address from your Trigger.dev account email
|
||||
address:
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value="priority-support@trigger.dev"
|
||||
className="my-2"
|
||||
/>
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Look out for an invite from Slack" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
As soon as we can, we'll setup a Slack Connect channel and say hello!
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)}
|
||||
<SideMenuItem
|
||||
name="Ask in our Discord"
|
||||
icon={DiscordIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
to="https://trigger.dev/discord"
|
||||
data-action="join our discord"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Book a 15 min call"
|
||||
icon={CalendarDaysIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-rose-500"
|
||||
activeIconColor="text-rose-500"
|
||||
to="https://cal.com/team/triggerdotdev/founders-call"
|
||||
data-action="book-a-call"
|
||||
target="_blank"
|
||||
/>
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={EnvelopeIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
data-action="contact-us"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
Contact us…
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,20 @@
|
||||
import {
|
||||
AcademicCapIcon,
|
||||
ArrowRightOnRectangleIcon,
|
||||
ArrowUpRightIcon,
|
||||
BeakerIcon,
|
||||
BellAlertIcon,
|
||||
CalendarDaysIcon,
|
||||
ChartBarIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
ClockIcon,
|
||||
CreditCardIcon,
|
||||
CursorArrowRaysIcon,
|
||||
EnvelopeIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
LightBulbIcon,
|
||||
RectangleStackIcon,
|
||||
ServerStackIcon,
|
||||
ShieldCheckIcon,
|
||||
SignalIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
@@ -64,17 +57,12 @@ import {
|
||||
v3TestPath,
|
||||
v3UsagePath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { ImpersonationBanner } from "../ImpersonationBanner";
|
||||
import { LogoIcon } from "../LogoIcon";
|
||||
import { StepContentContainer } from "../StepContentContainer";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { FreePlanUsage } from "../billing/v2/FreePlanUsage";
|
||||
import { Badge } from "../primitives/Badge";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
|
||||
import { Icon } from "../primitives/Icon";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
Popover,
|
||||
@@ -83,12 +71,11 @@ import {
|
||||
PopoverCustomTrigger,
|
||||
PopoverMenuItem,
|
||||
PopoverSectionHeader,
|
||||
PopoverSideMenuTrigger,
|
||||
} from "../primitives/Popover";
|
||||
import { StepNumber } from "../primitives/StepNumber";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
|
||||
import { SideMenuHeader } from "./SideMenuHeader";
|
||||
import { MenuCount, SideMenuItem } from "./SideMenuItem";
|
||||
import { SideMenuItem } from "./SideMenuItem";
|
||||
|
||||
type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
|
||||
type SideMenuProject = Pick<MatchedProject, "id" | "name" | "slug" | "version">;
|
||||
@@ -261,167 +248,6 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
);
|
||||
}
|
||||
|
||||
function HelpAndFeedback() {
|
||||
const [isHelpMenuOpen, setHelpMenuOpen] = useState(false);
|
||||
const currentPlan = useCurrentPlan();
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={(open) => setHelpMenuOpen(open)}>
|
||||
<PopoverSideMenuTrigger isOpen={isHelpMenuOpen} shortcut={{ key: "h" }}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ChatBubbleLeftEllipsisIcon className="size-4 text-success" />
|
||||
Help & Feedback
|
||||
</div>
|
||||
</PopoverSideMenuTrigger>
|
||||
<PopoverContent
|
||||
className="min-w-[14rem] divide-y divide-grid-bright overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
align="start"
|
||||
>
|
||||
<Fragment>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
name="Documentation"
|
||||
icon="docs"
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-green-500"
|
||||
activeIconColor="text-green-500"
|
||||
to="https://trigger.dev/docs"
|
||||
data-action="documentation"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
name="Status"
|
||||
icon={SignalIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-green-500"
|
||||
activeIconColor="text-green-500"
|
||||
to="https://status.trigger.dev/"
|
||||
data-action="status"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Suggest a feature"
|
||||
icon={LightBulbIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-sun-500"
|
||||
activeIconColor="text-sun-500"
|
||||
to="https://feedback.trigger.dev/"
|
||||
data-action="suggest-a-feature"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Changelog"
|
||||
icon="star"
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-sun-500"
|
||||
activeIconColor="text-sun-500"
|
||||
to="https://trigger.dev/changelog"
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<Paragraph className="pb-1 pl-1.5 pt-1.5 text-xs">Need help?</Paragraph>
|
||||
{currentPlan?.v3Subscription?.plan?.limits.support === "slack" && (
|
||||
<div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={SlackIcon}
|
||||
data-action="join-our-slack"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="text-text-bright">Join our Slack…</span>
|
||||
<MenuCount count="PRO" />
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>Join our Slack</DialogHeader>
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Icon icon={SlackIcon} className="h-10 w-10 min-w-[2.5rem]" />
|
||||
<Paragraph variant="base/bright">
|
||||
As a subscriber, you have access to a dedicated Slack channel for 1-to-1
|
||||
support with the Trigger.dev team.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<hr className="border-charcoal-800" />
|
||||
<div>
|
||||
<StepNumber stepNumber="1" title="Email us" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Send us an email to this address from your Trigger.dev account email
|
||||
address:
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value="priority-support@trigger.dev"
|
||||
className="my-2"
|
||||
/>
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Look out for an invite from Slack" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
As soon as we can, we'll setup a Slack Connect channel and say hello!
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)}
|
||||
<SideMenuItem
|
||||
name="Ask in our Discord"
|
||||
icon={DiscordIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
to="https://trigger.dev/discord"
|
||||
data-action="join our discord"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Book a 15 min call"
|
||||
icon={CalendarDaysIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-rose-500"
|
||||
activeIconColor="text-rose-500"
|
||||
to="https://cal.com/team/triggerdotdev/founders-call"
|
||||
data-action="book-a-call"
|
||||
target="_blank"
|
||||
/>
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={EnvelopeIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
data-action="contact-us"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
Contact us…
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectSelector({
|
||||
project,
|
||||
organizations,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const variants = {
|
||||
small: {
|
||||
size: "size-[1rem]",
|
||||
arrowHeadRight: "group-hover:translate-x-[3px]",
|
||||
arrowLineRight: "h-[1.5px] w-[7px] translate-x-1 top-[calc(50%-0.5px)]",
|
||||
arrowHeadLeft: "group-hover:translate-x-[3px]",
|
||||
arrowLineLeft: "h-[1.5px] w-[7px] translate-x-1 top-[calc(50%-0.5px)]",
|
||||
arrowHeadTopRight:
|
||||
"-translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]",
|
||||
},
|
||||
medium: {
|
||||
size: "size-[1.1rem]",
|
||||
arrowHeadRight: "group-hover:translate-x-[3px]",
|
||||
arrowLineRight: "h-[1.5px] w-[9px] translate-x-1 top-[calc(50%-1px)]",
|
||||
arrowHeadLeft: "group-hover:translate-x-[-3px]",
|
||||
arrowLineLeft: "h-[1.5px] w-[9px] translate-x-1 top-[calc(50%-1px)]",
|
||||
arrowHeadTopRight:
|
||||
"-translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]",
|
||||
},
|
||||
large: {
|
||||
size: "size-6",
|
||||
arrowHeadRight: "group-hover:translate-x-1",
|
||||
arrowLineRight: "h-[2.3px] w-[12px] translate-x-[6px] top-[calc(50%-1px)]",
|
||||
arrowHeadLeft: "group-hover:translate-x-1",
|
||||
arrowLineLeft: "h-[2.3px] w-[12px] translate-x-[6px] top-[calc(50%-1px)]",
|
||||
arrowHeadTopRight:
|
||||
"-translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]",
|
||||
},
|
||||
"extra-large": {
|
||||
size: "size-8",
|
||||
arrowHeadRight: "group-hover:translate-x-1",
|
||||
arrowLineRight: "h-[3px] w-[16px] translate-x-[8px] top-[calc(50%-1.5px)]",
|
||||
arrowHeadLeft: "group-hover:translate-x-1",
|
||||
arrowLineLeft: "h-[3px] w-[16px] translate-x-[8px] top-[calc(50%-1.5px)]",
|
||||
arrowHeadTopRight:
|
||||
"-translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]",
|
||||
},
|
||||
};
|
||||
|
||||
export const themes = {
|
||||
dark: {
|
||||
textStyle: "text-background-bright",
|
||||
arrowLine: "bg-background-bright",
|
||||
},
|
||||
dimmed: {
|
||||
textStyle: "text-text-dimmed",
|
||||
arrowLine: "bg-text-dimmed",
|
||||
},
|
||||
bright: {
|
||||
textStyle: "text-text-bright",
|
||||
arrowLine: "bg-text-bright",
|
||||
},
|
||||
primary: {
|
||||
textStyle: "text-text-dimmed group-hover:text-primary",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-primary",
|
||||
},
|
||||
blue: {
|
||||
textStyle: "text-text-dimmed group-hover:text-blue-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-blue-500",
|
||||
},
|
||||
rose: {
|
||||
textStyle: "text-text-dimmed group-hover:text-rose-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-rose-500",
|
||||
},
|
||||
amber: {
|
||||
textStyle: "text-text-dimmed group-hover:text-amber-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-amber-500",
|
||||
},
|
||||
apple: {
|
||||
textStyle: "text-text-dimmed group-hover:text-apple-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-apple-500",
|
||||
},
|
||||
lavender: {
|
||||
textStyle: "text-text-dimmed group-hover:text-lavender-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-lavender-500",
|
||||
},
|
||||
};
|
||||
|
||||
type Variants = keyof typeof variants;
|
||||
type Theme = keyof typeof themes;
|
||||
|
||||
type AnimatingArrowProps = {
|
||||
className?: string;
|
||||
variant?: Variants;
|
||||
theme?: Theme;
|
||||
direction?: "right" | "left" | "topRight";
|
||||
};
|
||||
|
||||
export function AnimatingArrow({
|
||||
className,
|
||||
variant = "medium",
|
||||
theme = "dimmed",
|
||||
direction = "right",
|
||||
}: AnimatingArrowProps) {
|
||||
const variantStyles = variants[variant];
|
||||
const themeStyles = themes[theme];
|
||||
|
||||
return (
|
||||
<span className={cn("relative -mr-1 ml-1 flex", variantStyles.size, className)}>
|
||||
{direction === "topRight" && (
|
||||
<>
|
||||
<svg
|
||||
className={cn(
|
||||
"absolute top-[5px] transition duration-200 ease-in-out",
|
||||
themeStyles.textStyle
|
||||
)}
|
||||
width="9"
|
||||
height="8"
|
||||
viewBox="0 0 9 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1.5 7L7.5 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
className={cn(
|
||||
"absolute top-[5px] transition duration-300 ease-in-out",
|
||||
themeStyles.textStyle,
|
||||
variantStyles.arrowHeadTopRight
|
||||
)}
|
||||
width="9"
|
||||
height="8"
|
||||
viewBox="0 0 9 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1 1H7.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<path d="M7.5 7L7.5 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<path d="M1 7.5L7.5 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
{direction === "right" && (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute rounded-full opacity-0 transition duration-300 ease-in-out group-hover:opacity-100",
|
||||
variantStyles.arrowLineRight,
|
||||
themeStyles.arrowLine
|
||||
)}
|
||||
/>
|
||||
<ChevronRightIcon
|
||||
className={cn(
|
||||
"absolute -translate-x-0.5 transition duration-300 ease-in-out",
|
||||
variantStyles.arrowHeadRight,
|
||||
variantStyles.size,
|
||||
themeStyles.textStyle
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{direction === "left" && (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute rounded-full opacity-0 transition duration-300 ease-in-out group-hover:opacity-100",
|
||||
variantStyles.arrowLineLeft,
|
||||
themeStyles.arrowLine
|
||||
)}
|
||||
/>
|
||||
<ChevronLeftIcon
|
||||
className={cn(
|
||||
"absolute translate-x-0.5 transition duration-300 ease-in-out",
|
||||
variantStyles.arrowHeadLeft,
|
||||
variantStyles.size,
|
||||
themeStyles.textStyle
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -174,6 +174,7 @@ export type ButtonContentPropsType = {
|
||||
className?: string;
|
||||
shortcut?: ShortcutDefinition;
|
||||
variant: keyof typeof variant;
|
||||
shortcutPosition?: "before-trailing-icon" | "after-trailing-icon";
|
||||
};
|
||||
|
||||
export function ButtonContent(props: ButtonContentPropsType) {
|
||||
@@ -237,6 +238,14 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
<>{text}</>
|
||||
))}
|
||||
|
||||
{shortcut && props.shortcutPosition === "before-trailing-icon" && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
variant={variation.shortcutVariant ?? "medium"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{TrailingIcon &&
|
||||
(typeof TrailingIcon === "string" ? (
|
||||
<NamedIcon
|
||||
@@ -258,13 +267,15 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{shortcut && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
variant={variation.shortcutVariant ?? "medium"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shortcut &&
|
||||
(!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
variant={variation.shortcutVariant ?? "medium"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+486
-176
@@ -4,14 +4,19 @@ import {
|
||||
ChatBubbleLeftRightIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
LightBulbIcon,
|
||||
UserPlusIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useRevalidator } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Link, useRevalidator, useSubmit } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { DiscordIcon } from "@trigger.dev/companyicons";
|
||||
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { TaskRunStatus } from "@trigger.dev/database";
|
||||
import { Fragment, Suspense, useEffect, useState } from "react";
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps } from "recharts";
|
||||
import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { InitCommandV3, TriggerDevStepV3, TriggerLoginStepV3 } from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
@@ -19,6 +24,7 @@ import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { AnimatingArrow } from "~/components/primitives/AnimatingArrow";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { formatDateTime } from "~/components/primitives/DateTime";
|
||||
@@ -28,6 +34,11 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { PopoverMenuItem } from "~/components/primitives/Popover";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "~/components/primitives/Resizable";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import {
|
||||
@@ -40,7 +51,13 @@ import {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import {
|
||||
Tooltip as TooltipPrimitive,
|
||||
SimpleTooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "~/components/primitives/Tooltip";
|
||||
import TooltipPortal from "~/components/primitives/TooltipPortal";
|
||||
import { TaskFunctionName } from "~/components/runs/v3/TaskPath";
|
||||
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
|
||||
@@ -57,12 +74,19 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
docsPath,
|
||||
inviteTeamMemberPath,
|
||||
ProjectParamSchema,
|
||||
v3RunsPath,
|
||||
v3TasksStreamingPath,
|
||||
v3TestPath,
|
||||
v3TestTaskPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import {
|
||||
getUsefulLinksPreference,
|
||||
setUsefulLinksPreference,
|
||||
uiPreferencesStorage,
|
||||
} from "~/services/preferences/uiPreferences.server";
|
||||
import { json } from "@remix-run/node";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -76,12 +100,15 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
projectSlug: projectParam,
|
||||
});
|
||||
|
||||
const usefulLinksPreference = await getUsefulLinksPreference(request);
|
||||
|
||||
return typeddefer({
|
||||
tasks,
|
||||
userHasTasks,
|
||||
activity,
|
||||
runningStats,
|
||||
durations,
|
||||
usefulLinksPreference,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -92,10 +119,26 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
}
|
||||
};
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const showUsefulLinks = formData.get("showUsefulLinks") === "true";
|
||||
|
||||
const session = await setUsefulLinksPreference(showUsefulLinks, request);
|
||||
|
||||
return json(
|
||||
{ success: true },
|
||||
{
|
||||
headers: {
|
||||
"Set-Cookie": await uiPreferencesStorage.commitSession(session),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const { tasks, userHasTasks, activity, runningStats, durations } =
|
||||
const { tasks, userHasTasks, activity, runningStats, durations, usefulLinksPreference } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<Task>({
|
||||
items: tasks,
|
||||
@@ -137,6 +180,16 @@ export default function Page() {
|
||||
// WARNING Don't put the revalidator in the useEffect deps array or bad things will happen
|
||||
}, [streamedEvents]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const [showUsefulLinks, setShowUsefulLinks] = useState(usefulLinksPreference ?? true);
|
||||
|
||||
// Create a submit handler to save the preference
|
||||
const submit = useSubmit();
|
||||
|
||||
const handleUsefulLinksToggle = (show: boolean) => {
|
||||
setShowUsefulLinks(show);
|
||||
submit({ showUsefulLinks: show.toString() }, { method: "post" });
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<NavBar>
|
||||
@@ -168,183 +221,213 @@ export default function Page() {
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
<PageBody scrollable={false}>
|
||||
<div className={cn("grid h-full grid-rows-1")}>
|
||||
{hasTasks ? (
|
||||
<div className="flex flex-col">
|
||||
{!userHasTasks && <UserHasNoTasks />}
|
||||
<div className="max-h-full overflow-hidden">
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="Search tasks"
|
||||
variant="tertiary"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<Table containerClassName="max-h-full mb-[2.5rem]">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Task ID</TableHeaderCell>
|
||||
<TableHeaderCell>Task</TableHeaderCell>
|
||||
<TableHeaderCell>Running</TableHeaderCell>
|
||||
<TableHeaderCell>Queued</TableHeaderCell>
|
||||
<TableHeaderCell>Activity (7d)</TableHeaderCell>
|
||||
<TableHeaderCell>Avg. duration</TableHeaderCell>
|
||||
<TableHeaderCell>Environments</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length > 0 ? (
|
||||
filteredItems.map((task) => {
|
||||
const path = v3RunsPath(organization, project, {
|
||||
tasks: [task.slug],
|
||||
});
|
||||
<ResizablePanelGroup orientation="horizontal" className="max-h-full">
|
||||
<ResizablePanel id="tasks-main" className="max-h-full">
|
||||
<div className={cn("grid h-full grid-rows-1")}>
|
||||
{hasTasks ? (
|
||||
<div className="flex min-w-0 max-w-full flex-col">
|
||||
{!userHasTasks && <UserHasNoTasks />}
|
||||
<div className="max-h-full overflow-hidden">
|
||||
<div className="flex items-center p-2">
|
||||
<Input
|
||||
placeholder="Search tasks"
|
||||
variant="tertiary"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
{!showUsefulLinks && (
|
||||
<Button
|
||||
variant="minimal/small"
|
||||
TrailingIcon={LightBulbIcon}
|
||||
onClick={() => handleUsefulLinksToggle(true)}
|
||||
className="px-2.5"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Table containerClassName="max-h-full mb-[2.5rem]">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Task ID</TableHeaderCell>
|
||||
<TableHeaderCell>Task</TableHeaderCell>
|
||||
<TableHeaderCell>Running</TableHeaderCell>
|
||||
<TableHeaderCell>Queued</TableHeaderCell>
|
||||
<TableHeaderCell>Activity (7d)</TableHeaderCell>
|
||||
<TableHeaderCell>Avg. duration</TableHeaderCell>
|
||||
<TableHeaderCell>Environments</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length > 0 ? (
|
||||
filteredItems.map((task) => {
|
||||
const path = v3RunsPath(organization, project, {
|
||||
tasks: [task.slug],
|
||||
});
|
||||
|
||||
const devYouEnvironment = task.environments.find(
|
||||
(e) => e.type === "DEVELOPMENT" && !e.userName
|
||||
);
|
||||
const firstDeployedEnvironment = task.environments
|
||||
.filter((e) => e.type !== "DEVELOPMENT")
|
||||
.at(0);
|
||||
const testEnvironment = devYouEnvironment ?? firstDeployedEnvironment;
|
||||
const devYouEnvironment = task.environments.find(
|
||||
(e) => e.type === "DEVELOPMENT" && !e.userName
|
||||
);
|
||||
const firstDeployedEnvironment = task.environments
|
||||
.filter((e) => e.type !== "DEVELOPMENT")
|
||||
.at(0);
|
||||
const testEnvironment = devYouEnvironment ?? firstDeployedEnvironment;
|
||||
|
||||
const testPath = testEnvironment
|
||||
? v3TestTaskPath(
|
||||
organization,
|
||||
project,
|
||||
{ taskIdentifier: task.slug },
|
||||
testEnvironment.slug
|
||||
)
|
||||
: v3TestPath(organization, project);
|
||||
const testPath = testEnvironment
|
||||
? v3TestTaskPath(
|
||||
organization,
|
||||
project,
|
||||
{ taskIdentifier: task.slug },
|
||||
testEnvironment.slug
|
||||
)
|
||||
: v3TestPath(organization, project);
|
||||
|
||||
return (
|
||||
<TableRow key={task.slug} className="group">
|
||||
<TableCell to={path}>
|
||||
<div className="flex items-center gap-2">
|
||||
<SimpleTooltip
|
||||
button={<TaskTriggerSourceIcon source={task.triggerSource} />}
|
||||
content={taskTriggerSourceDescription(task.triggerSource)}
|
||||
/>
|
||||
<span>{task.slug}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="py-0" actionClassName="py-0">
|
||||
<TaskFunctionName
|
||||
functionName={task.exportName}
|
||||
variant="extra-extra-small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<Suspense
|
||||
fallback={
|
||||
<>
|
||||
<Spinner color="muted" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TypedAwait resolve={runningStats}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return taskData?.running ?? "0";
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<Suspense fallback={<></>}>
|
||||
<TypedAwait resolve={runningStats}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return taskData?.queued ?? "0";
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0" actionClassName="py-0">
|
||||
<Suspense fallback={<TaskActivityBlankState />}>
|
||||
<TypedAwait resolve={activity}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return (
|
||||
<>
|
||||
{taskData !== undefined ? (
|
||||
<div className="h-6 w-[5.125rem] rounded-sm">
|
||||
<TaskActivityGraph activity={taskData} />
|
||||
</div>
|
||||
) : (
|
||||
<TaskActivityBlankState />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<Suspense fallback={<></>}>
|
||||
<TypedAwait resolve={durations}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return taskData
|
||||
? formatDurationMilliseconds(taskData * 1000, {
|
||||
style: "short",
|
||||
})
|
||||
: "–";
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<EnvironmentLabels environments={task.environments} />
|
||||
</TableCell>
|
||||
<TableCellMenu
|
||||
isSticky
|
||||
popoverContent={
|
||||
<>
|
||||
<PopoverMenuItem
|
||||
icon="runs"
|
||||
to={path}
|
||||
title="View runs"
|
||||
leadingIconClassName="text-teal-500"
|
||||
return (
|
||||
<TableRow key={task.slug} className="group">
|
||||
<TableCell to={path}>
|
||||
<div className="flex items-center gap-2">
|
||||
<SimpleTooltip
|
||||
button={<TaskTriggerSourceIcon source={task.triggerSource} />}
|
||||
content={taskTriggerSourceDescription(task.triggerSource)}
|
||||
/>
|
||||
<span>{task.slug}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="py-0" actionClassName="py-0">
|
||||
<TaskFunctionName
|
||||
functionName={task.exportName}
|
||||
variant="extra-extra-small"
|
||||
/>
|
||||
<PopoverMenuItem icon="beaker" to={testPath} title="Test task" />
|
||||
</>
|
||||
}
|
||||
hiddenButtons={
|
||||
<LinkButton
|
||||
variant="minimal/small"
|
||||
LeadingIcon={BeakerIcon}
|
||||
leadingIconClassName="text-text-bright"
|
||||
to={testPath}
|
||||
>
|
||||
Test
|
||||
</LinkButton>
|
||||
}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={8}>
|
||||
<Paragraph variant="small" className="flex items-center justify-center">
|
||||
No tasks match your filters
|
||||
</Paragraph>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<Suspense
|
||||
fallback={
|
||||
<>
|
||||
<Spinner color="muted" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TypedAwait resolve={runningStats}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return taskData?.running ?? "0";
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<Suspense fallback={<></>}>
|
||||
<TypedAwait resolve={runningStats}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return taskData?.queued ?? "0";
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0" actionClassName="py-0">
|
||||
<Suspense fallback={<TaskActivityBlankState />}>
|
||||
<TypedAwait resolve={activity}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return (
|
||||
<>
|
||||
{taskData !== undefined ? (
|
||||
<div className="h-6 w-[5.125rem] rounded-sm">
|
||||
<TaskActivityGraph activity={taskData} />
|
||||
</div>
|
||||
) : (
|
||||
<TaskActivityBlankState />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<Suspense fallback={<></>}>
|
||||
<TypedAwait resolve={durations}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return taskData
|
||||
? formatDurationMilliseconds(taskData * 1000, {
|
||||
style: "short",
|
||||
})
|
||||
: "–";
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<EnvironmentLabels environments={task.environments} />
|
||||
</TableCell>
|
||||
<TableCellMenu
|
||||
isSticky
|
||||
popoverContent={
|
||||
<>
|
||||
<PopoverMenuItem
|
||||
icon="runs"
|
||||
to={path}
|
||||
title="View runs"
|
||||
leadingIconClassName="text-teal-500"
|
||||
/>
|
||||
<PopoverMenuItem
|
||||
icon="beaker"
|
||||
to={testPath}
|
||||
title="Test task"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
hiddenButtons={
|
||||
<LinkButton
|
||||
variant="minimal/small"
|
||||
LeadingIcon={BeakerIcon}
|
||||
leadingIconClassName="text-text-bright"
|
||||
to={testPath}
|
||||
>
|
||||
Test
|
||||
</LinkButton>
|
||||
}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={8}>
|
||||
<Paragraph variant="small" className="flex items-center justify-center">
|
||||
No tasks match your filters
|
||||
</Paragraph>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<MainCenteredContainer className="max-w-prose">
|
||||
<CreateTaskInstructions />
|
||||
</MainCenteredContainer>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<MainCenteredContainer className="max-w-prose">
|
||||
<CreateTaskInstructions />
|
||||
</MainCenteredContainer>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
{hasTasks && showUsefulLinks ? (
|
||||
<>
|
||||
<ResizableHandle id="tasks-handle" />
|
||||
<ResizablePanel
|
||||
id="tasks-inspector"
|
||||
min="200px"
|
||||
default="400px"
|
||||
max="500px"
|
||||
className="w-full"
|
||||
>
|
||||
<HelpfulInfoHasTasks onClose={() => handleUsefulLinksToggle(false)} />
|
||||
</ResizablePanel>
|
||||
</>
|
||||
) : null}
|
||||
</ResizablePanelGroup>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
@@ -535,3 +618,230 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>)
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function HelpfulInfoHasTasks({ onClose }: { onClose: () => void }) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
|
||||
return (
|
||||
<div className="grid h-full max-h-full grid-rows-[auto_1fr] overflow-hidden bg-background-bright">
|
||||
<div className="overflow-y-scroll p-3 pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="mb-2 flex items-center justify-between gap-2 border-b border-grid-dimmed pb-2">
|
||||
<Header2 className="flex items-center gap-2">
|
||||
<LightBulbIcon className="size-4 text-sun-500" />
|
||||
Helpful next steps
|
||||
</Header2>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="minimal/small"
|
||||
TrailingIcon={ExitIcon}
|
||||
shortcut={{ key: "esc" }}
|
||||
shortcutPosition="before-trailing-icon"
|
||||
className="pl-[0.375rem]"
|
||||
/>
|
||||
</div>
|
||||
<LinkWithIcon
|
||||
to={v3TestPath(organization, project)}
|
||||
description="Test your tasks"
|
||||
icon={<BeakerIcon className="size-5 text-lime-500" />}
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={inviteTeamMemberPath(organization)}
|
||||
description="Invite team members"
|
||||
icon={<UserPlusIcon className="size-5 text-amber-500" />}
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to="https://trigger.dev/discord"
|
||||
description="Join our Discord for help and support"
|
||||
icon={<DiscordIcon className="size-5" />}
|
||||
isExternal
|
||||
/>
|
||||
<div className="mb-2 flex items-center gap-2 border-b border-grid-dimmed pb-2 pt-6">
|
||||
<Header2 className="flex items-center gap-2">
|
||||
<BookOpenIcon className="size-5 text-blue-500" />
|
||||
From the docs
|
||||
</Header2>
|
||||
</div>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/writing-tasks-introduction")}
|
||||
description="How to write a task"
|
||||
icon={<BookOpenIcon className="size-5 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/tasks/scheduled")}
|
||||
description="Scheduled tasks (cron)"
|
||||
icon={<BookOpenIcon className="size-5 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/triggering")}
|
||||
description="How to trigger a task"
|
||||
icon={<BookOpenIcon className="size-5 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/cli-dev")}
|
||||
description="Running the CLI"
|
||||
icon={<BookOpenIcon className="size-5 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/how-it-works")}
|
||||
description="How Trigger.dev works"
|
||||
icon={<BookOpenIcon className="size-5 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<div className="mb-2 flex items-center gap-2 border-b border-grid-dimmed pb-2 pt-6">
|
||||
<Header2 className="flex items-center gap-2">
|
||||
<TaskIcon className="size-4 text-blue-500" />
|
||||
Example tasks
|
||||
</Header2>
|
||||
</div>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/dall-e3-generate-image")}
|
||||
description="DALL·E 3 image generation"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/deepgram-transcribe-audio")}
|
||||
description="Deepgram audio transcription"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/fal-ai-image-to-cartoon")}
|
||||
description="Fal.ai image to cartoon"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/fal-ai-realtime")}
|
||||
description="Fal.ai with Realtime"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/ffmpeg-video-processing")}
|
||||
description="FFmpeg video processing"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/firecrawl-url-crawl")}
|
||||
description="Firecrawl URL crawl"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/libreoffice-pdf-conversion")}
|
||||
description="LibreOffice PDF conversion"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/open-ai-with-retrying")}
|
||||
description="OpenAI with retrying"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/pdf-to-image")}
|
||||
description="PDF to image"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/puppeteer")}
|
||||
description="Puppeteer"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/react-pdf")}
|
||||
description="React to PDF"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/resend-email-sequence")}
|
||||
description="Resend email sequence"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/scrape-hacker-news")}
|
||||
description="Scrape Hacker News"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/sentry-error-tracking")}
|
||||
description="Sentry error tracking"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/sharp-image-processing")}
|
||||
description="Sharp image processing"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/supabase-database-operations")}
|
||||
description="Supabase database operations"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/supabase-storage-upload")}
|
||||
description="Supabase Storage upload"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/vercel-ai-sdk")}
|
||||
description="Vercel AI SDK"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
<LinkWithIcon
|
||||
to={docsPath("/examples/vercel-sync-env-vars")}
|
||||
description="Vercel sync environment variables"
|
||||
icon={<TaskIcon className="size-4 text-text-dimmed" />}
|
||||
isExternal
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkWithIcon({
|
||||
to,
|
||||
description,
|
||||
icon,
|
||||
isExternal,
|
||||
}: {
|
||||
to: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
isExternal?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={isExternal ? "noreferrer" : undefined}
|
||||
className="group flex w-full items-center justify-between gap-2 rounded-md p-1 pr-3 transition hover:bg-charcoal-750"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid size-9 min-w-9 place-items-center rounded border border-transparent bg-charcoal-750 shadow transition group-hover:border-charcoal-650">
|
||||
{icon}
|
||||
</div>
|
||||
<Paragraph variant="base">{description}</Paragraph>
|
||||
</div>
|
||||
<AnimatingArrow direction={isExternal ? "topRight" : "right"} theme="dimmed" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-2
@@ -216,9 +216,10 @@ export default function Page() {
|
||||
<Header2 className={cn("whitespace-nowrap")}>{schedule.friendlyId}</Header2>
|
||||
<LinkButton
|
||||
to={`${v3SchedulesPath(organization, project)}${location.search}`}
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={ExitIcon}
|
||||
variant="minimal/small"
|
||||
TrailingIcon={ExitIcon}
|
||||
shortcut={{ key: "esc" }}
|
||||
shortcutPosition="before-trailing-icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-y-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createCookieSessionStorage } from "@remix-run/node";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
export const uiPreferencesStorage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "__ui_prefs",
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secrets: [env.SESSION_SECRET],
|
||||
secure: env.NODE_ENV === "production",
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 year
|
||||
},
|
||||
});
|
||||
|
||||
export function getUiPreferencesSession(request: Request) {
|
||||
return uiPreferencesStorage.getSession(request.headers.get("Cookie"));
|
||||
}
|
||||
|
||||
export async function getUsefulLinksPreference(request: Request): Promise<boolean | undefined> {
|
||||
const session = await getUiPreferencesSession(request);
|
||||
return session.get("showUsefulLinks");
|
||||
}
|
||||
|
||||
export async function setUsefulLinksPreference(show: boolean, request: Request) {
|
||||
const session = await getUiPreferencesSession(request);
|
||||
session.set("showUsefulLinks", show);
|
||||
return session;
|
||||
}
|
||||
Reference in New Issue
Block a user