diff --git a/apps/webapp/app/assets/icons/SideMenuRightClosed.tsx b/apps/webapp/app/assets/icons/SideMenuRightClosed.tsx new file mode 100644 index 0000000000..b120300c0c --- /dev/null +++ b/apps/webapp/app/assets/icons/SideMenuRightClosed.tsx @@ -0,0 +1,15 @@ +export function SideMenuRightClosedIcon({ className }: { className?: string }) { + return ( + + + + + ); +} diff --git a/apps/webapp/app/components/primitives/AnimatingArrow.tsx b/apps/webapp/app/components/primitives/AnimatingArrow.tsx new file mode 100644 index 0000000000..68dbf5c4d8 --- /dev/null +++ b/apps/webapp/app/components/primitives/AnimatingArrow.tsx @@ -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 ( + + {direction === "topRight" && ( + <> + + + + + + + + + + + )} + {direction === "right" && ( + <> + + + + )} + {direction === "left" && ( + <> + + + + )} + + ); +} diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index f3b1ba1ecc..7cd4a83f2d 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -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" && ( + + )} + {TrailingIcon && (typeof TrailingIcon === "string" ? ( ))} - {shortcut && ( - - )} + + {shortcut && + (!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") && ( + + )} ); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam._index/route.tsx index ef65ecfc8c..74d5ca966c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam._index/route.tsx @@ -4,14 +4,21 @@ import { ChatBubbleLeftRightIcon, ChevronDownIcon, ChevronUpIcon, + LightBulbIcon, + UserPlusIcon, + VideoCameraIcon, } from "@heroicons/react/20/solid"; -import { useRevalidator } from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { json } from "@remix-run/node"; +import { Link, useRevalidator, useSubmit } from "@remix-run/react"; +import { ActionFunctionArgs, LoaderFunctionArgs } 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,15 +26,22 @@ 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"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "~/components/primitives/Dialog"; import { Header1, Header2, Header3 } from "~/components/primitives/Headers"; import { Input } from "~/components/primitives/Input"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; 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 { @@ -53,10 +67,16 @@ import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useTextFilter } from "~/hooks/useTextFilter"; import { Task, TaskActivity, TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server"; +import { + getUsefulLinksPreference, + setUsefulLinksPreference, + uiPreferencesStorage, +} from "~/services/preferences/uiPreferences.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { docsPath, + inviteTeamMemberPath, ProjectParamSchema, v3RunsPath, v3TasksStreamingPath, @@ -76,12 +96,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 +115,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(); const { filterText, setFilterText, filteredItems } = useTextFilter({ items: tasks, @@ -137,6 +176,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 ( @@ -168,183 +217,213 @@ export default function Page() { -
- {hasTasks ? ( -
- {!userHasTasks && } -
-
- setFilterText(e.target.value)} - autoFocus - /> -
- - - - Task ID - Task - Running - Queued - Activity (7d) - Avg. duration - Environments - Go to page - - - - {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 testPath = testEnvironment - ? v3TestTaskPath( - organization, - project, - { taskIdentifier: task.slug }, - testEnvironment.slug - ) - : v3TestPath(organization, project); - - return ( - - -
- } - content={taskTriggerSourceDescription(task.triggerSource)} - /> - {task.slug} -
-
- - - - - - - - } - > - - {(data) => { - const taskData = data[task.slug]; - return taskData?.running ?? "0"; - }} - - - - - }> - - {(data) => { - const taskData = data[task.slug]; - return taskData?.queued ?? "0"; - }} - - - - - }> - - {(data) => { - const taskData = data[task.slug]; - return ( + + +
+ {hasTasks ? ( +
+ {!userHasTasks && } +
+
+ setFilterText(e.target.value)} + autoFocus + /> + {!showUsefulLinks && ( +
+
+ + + Task ID + Task + Running + Queued + Activity (7d) + Avg. duration + Environments + Go to page + + + + {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 testPath = testEnvironment + ? v3TestTaskPath( + organization, + project, + { taskIdentifier: task.slug }, + testEnvironment.slug + ) + : v3TestPath(organization, project); + + return ( + + +
+ } + content={taskTriggerSourceDescription(task.triggerSource)} + /> + {task.slug} +
+
+ + + + + - {taskData !== undefined ? ( -
- -
- ) : ( - - )} + - ); - }} - -
-
- - }> - - {(data) => { - const taskData = data[task.slug]; - return taskData - ? formatDurationMilliseconds(taskData * 1000, { - style: "short", - }) - : "–"; - }} - - - - - - - - - - - } - hiddenButtons={ - - Test - - } - /> -
- ); - }) - ) : ( - - - No tasks match your filters - - - )} -
-
-
+ } + > + + {(data) => { + const taskData = data[task.slug]; + return taskData?.running ?? "0"; + }} + + + + + }> + + {(data) => { + const taskData = data[task.slug]; + return taskData?.queued ?? "0"; + }} + + + + + }> + + {(data) => { + const taskData = data[task.slug]; + return ( + <> + {taskData !== undefined ? ( +
+ +
+ ) : ( + + )} + + ); + }} +
+
+
+ + }> + + {(data) => { + const taskData = data[task.slug]; + return taskData + ? formatDurationMilliseconds(taskData * 1000, { + style: "short", + }) + : "–"; + }} + + + + + + + + + + + } + hiddenButtons={ + + Test + + } + /> + + ); + }) + ) : ( + + + No tasks match your filters + + + )} + + +
+
+ ) : ( + + + + )} - ) : ( - - - - )} - + + {hasTasks && showUsefulLinks ? ( + <> + + + handleUsefulLinksToggle(false)} /> + + + ) : null} +
); @@ -537,3 +616,251 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps) return null; }; + +function HelpfulInfoHasTasks({ onClose }: { onClose: () => void }) { + const organization = useOrganization(); + const project = useProject(); + const [isVideoDialogOpen, setIsVideoDialogOpen] = useState(false); + + return ( +
+
+
+ + + Helpful next steps + +
+ } + /> + } + /> +
setIsVideoDialogOpen(true)} + className={cn( + "group flex w-full items-center justify-between gap-2 rounded-md p-1 pr-3 transition hover:bg-charcoal-750", + variants["withIcon"].container + )} + > +
+
+ +
+ + Watch a 14 min walkthrough video + +
+ +
+ } + isExternal + /> +
+ + + From the docs + +
+ + + + + +
+ + + Example tasks + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + Trigger.dev walkthrough + +
+