Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/animated-resizable-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@trigger.dev/core": patch
"@trigger.dev/sdk": patch
---

Feat(webapp): animated resizable panel

Adds animated open/close transitions to Resizable panels using react-window-splitter built-in animation hooks. Includes new exports: RESIZABLE_PANEL_ANIMATION, collapsibleHandleClassName(), and useFrozenValue(). Converts inspector/detail side panels from conditionally-mounted to always-mounted collapsible panels across multiple routes (batches, runs, schedules, deployments, logs, waitpoints, bulk-actions).
9 changes: 6 additions & 3 deletions apps/webapp/app/components/GitMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export function GitMetadataBranch({
<LinkButton
variant="minimal/small"
LeadingIcon={<GitBranchIcon className="size-4" />}
leadingIconClassName="group-hover/table-row:text-text-bright"
iconSpacing="gap-x-1"
to={git.branchUrl}
className="pl-1"
className="pl-1 duration-0 [&_span]:duration-0 [&_span]:group-hover/table-row:text-text-bright"
>
{git.branchName}
</LinkButton>
Expand All @@ -49,8 +50,9 @@ export function GitMetadataCommit({
variant="minimal/small"
to={git.commitUrl}
LeadingIcon={<GitCommitIcon className="size-4" />}
leadingIconClassName="group-hover/table-row:text-text-bright"
iconSpacing="gap-x-1"
className="pl-1"
className="pl-1 duration-0 [&_span]:duration-0 [&_span]:group-hover/table-row:text-text-bright"
>
{`${git.shortSha} / ${git.commitMessage}`}
</LinkButton>
Expand All @@ -74,8 +76,9 @@ export function GitMetadataPullRequest({
variant="minimal/small"
to={git.pullRequestUrl}
LeadingIcon={<GitPullRequestIcon className="size-4" />}
leadingIconClassName="group-hover/table-row:text-text-bright"
iconSpacing="gap-x-1"
className="pl-1"
className="pl-1 duration-0 [&_span]:duration-0 [&_span]:group-hover/table-row:text-text-bright"
>
#{git.pullRequestNumber} {git.pullRequestTitle}
</LinkButton>
Expand Down
4 changes: 2 additions & 2 deletions apps/webapp/app/components/logs/LogsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ export function LogsTable({
>
<DateTimeAccurate date={log.triggeredTimestamp} hour12={false} />
</TableCell>
<TableCell className="min-w-24" onClick={handleRowClick} hasAction>
<TruncatedCopyableValue value={log.runId} />
<TableCell className="min-w-24 cursor-pointer" onClick={handleRowClick}>
<span className="font-mono text-xs">{log.runId}</span>
</TableCell>
<TableCell className="min-w-32" onClick={handleRowClick} hasAction>
<span className="font-mono text-xs">{log.taskIdentifier}</span>
Expand Down
28 changes: 26 additions & 2 deletions apps/webapp/app/components/primitives/Resizable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React from "react";
import React, { useRef } from "react";
import { PanelGroup, Panel, PanelResizer } from "react-window-splitter";
import { cn } from "~/utils/cn";

Expand Down Expand Up @@ -69,6 +69,30 @@ const ResizableHandle = ({
</PanelResizer>
);

export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
const RESIZABLE_PANEL_ANIMATION = {
easing: "ease-in-out" as const,
duration: 200,
};

const COLLAPSIBLE_HANDLE_CLASSNAME = "transition-opacity duration-200";

function collapsibleHandleClassName(show: boolean) {
return cn(COLLAPSIBLE_HANDLE_CLASSNAME, !show && "pointer-events-none opacity-0");
}

function useFrozenValue<T>(value: T | null | undefined): T | null | undefined {
const ref = useRef(value);
if (value != null) ref.current = value;
return ref.current;
}

export {
RESIZABLE_PANEL_ANIMATION,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
collapsibleHandleClassName,
useFrozenValue,
};

export type ResizableSnapshot = React.ComponentProps<typeof PanelGroup>["snapshot"];
12 changes: 12 additions & 0 deletions apps/webapp/app/components/primitives/TreeView/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ export function useTree<TData, TFilterValue>({
concreteStateFromInput({ tree, selectedId, collapsedIds, filter })
);

//sync external selectedId prop into internal state
useEffect(() => {
const internalSelectedId = selectedIdFromState(state.nodes);
if (selectedId !== internalSelectedId) {
if (selectedId === undefined) {
dispatch({ type: "DESELECT_ALL_NODES" });
} else {
dispatch({ type: "SELECT_NODE", payload: { id: selectedId, scrollToNode: false, scrollToNodeFn } });
}
}
}, [selectedId]);

//fire onSelectedIdChanged()
useEffect(() => {
const selectedId = selectedIdFromState(state.nodes);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArrowRightIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { tryCatch } from "@trigger.dev/core";
import { motion } from "framer-motion";
Expand All @@ -12,17 +12,14 @@ import { DateTime } from "~/components/primitives/DateTime";
import { Header2, Header3 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import * as Property from "~/components/primitives/PropertyTable";
import {
BatchStatusCombo,
descriptionForBatchStatus,
} from "~/components/runs/v3/BatchStatus";
import { BatchStatusCombo, descriptionForBatchStatus } from "~/components/runs/v3/BatchStatus";
import { useAutoRevalidate } from "~/hooks/useAutoRevalidate";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { BatchPresenter, type BatchPresenterData } from "~/presenters/v3/BatchPresenter.server";
import { BatchPresenter } from "~/presenters/v3/BatchPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { formatNumber } from "~/utils/numberFormatter";
Expand All @@ -35,8 +32,7 @@ const BatchParamSchema = EnvironmentParamSchema.extend({
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);

const { organizationSlug, projectParam, envParam, batchParam } =
BatchParamSchema.parse(params);
const { organizationSlug, projectParam, envParam, batchParam } = BatchParamSchema.parse(params);

const project = await findProjectBySlug(organizationSlug, projectParam, userId);
if (!project) {
Expand Down Expand Up @@ -85,7 +81,8 @@ export default function Page() {
disabled: batch.hasFinished,
});

const showProgressMeter = batch.isV2 && (batch.status === "PROCESSING" || batch.status === "PARTIAL_FAILED");
const showProgressMeter =
batch.isV2 && (batch.status === "PROCESSING" || batch.status === "PARTIAL_FAILED");

return (
<div className="grid h-full max-h-full grid-rows-[2.5rem_2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
Expand Down Expand Up @@ -141,9 +138,7 @@ export default function Page() {
</Property.Item>
<Property.Item>
<Property.Label>Version</Property.Label>
<Property.Value>
{batch.isV2 ? "v2 (Run Engine)" : "v1 (Legacy)"}
</Property.Value>
<Property.Value>{batch.isV2 ? "v2 (Run Engine)" : "v1 (Legacy)"}</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Total runs</Property.Label>
Expand Down Expand Up @@ -243,11 +238,11 @@ export default function Page() {
{/* Footer */}
<div className="flex items-center justify-end gap-2 border-t border-grid-dimmed px-2">
<LinkButton
variant="tertiary/medium"
variant="secondary/medium"
to={v3BatchRunsPath(organization, project, environment, batch)}
LeadingIcon={RunsIcon}
leadingIconClassName="text-indigo-500"
TrailingIcon={ArrowRightIcon}
trailingIconClassName="text-runs"
TrailingIcon={RunsIcon}
className="text-text-bright"
>
View runs
</LinkButton>
Expand Down Expand Up @@ -304,4 +299,3 @@ function BatchProgressMeter({ successCount, failureCount, totalCount }: BatchPro
</div>
);
}

Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ArrowRightIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid";
import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
import { BookOpenIcon } from "@heroicons/react/24/solid";
import { type MetaFunction, Outlet, useNavigation, useParams, useLocation } from "@remix-run/react";
import { type MetaFunction, Outlet, useLocation, useNavigation, useParams } from "@remix-run/react";
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { RunsIcon } from "~/assets/icons/RunsIcon";
import { BatchesNone } from "~/components/BlankStatePanels";
import { ListPagination } from "~/components/ListPagination";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
Expand All @@ -13,6 +14,8 @@ import { DateTime } from "~/components/primitives/DateTime";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
collapsibleHandleClassName,
RESIZABLE_PANEL_ANIMATION,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
Expand Down Expand Up @@ -143,14 +146,25 @@ export default function Page() {
/>
</div>
</ResizablePanel>
{isShowingInspector && (
<>
<ResizableHandle id="batches-handle" />
<ResizablePanel id="batches-inspector" min="100px" default="500px">
<Outlet />
</ResizablePanel>
</>
)}
<ResizableHandle
id="batches-handle"
className={collapsibleHandleClassName(isShowingInspector)}
/>
<ResizablePanel
id="batches-inspector"
min="370px"
default="370px"
className="overflow-hidden"
collapsible
collapsed={!isShowingInspector}
onCollapseChange={() => {}}
collapsedSize="0px"
collapseAnimation={RESIZABLE_PANEL_ANIMATION}
>
<div className="h-full" style={{ minWidth: 370 }}>
<Outlet />
</div>
</ResizablePanel>
</ResizablePanelGroup>
)}
</PageBody>
Expand Down Expand Up @@ -287,8 +301,14 @@ function BatchActionsCell({ runsPath }: { runsPath: string }) {
<TableCellMenu
isSticky
hiddenButtons={
<LinkButton to={runsPath} variant="minimal/small" LeadingIcon={ArrowRightIcon}>
View runs
<LinkButton
to={runsPath}
variant="minimal/small"
TrailingIcon={RunsIcon}
trailingIconClassName="text-runs"
className="text-text-bright"
>
<span>View runs</span>
</LinkButton>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
RESIZABLE_PANEL_ANIMATION,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
collapsibleHandleClassName,
} from "~/components/primitives/Resizable";
import {
Table,
Expand Down Expand Up @@ -170,14 +172,26 @@ export default function Page() {
)}
</div>
</ResizablePanel>
{isShowingInspector && (
<>
<ResizableHandle id="bulk-actions-handle" />
<ResizablePanel id="bulk-actions-inspector" min="100px" default="500px">
<Outlet />
</ResizablePanel>
</>
)}
<ResizableHandle
id="bulk-actions-handle"
className={collapsibleHandleClassName(isShowingInspector)}
/>
<ResizablePanel
id="bulk-actions-inspector"
default="500px"
min="500px"
max="800px"
className="overflow-hidden"
collapsible
collapsed={!isShowingInspector}
onCollapseChange={() => {}}
collapsedSize="0px"
collapseAnimation={RESIZABLE_PANEL_ANIMATION}
>
<div className="h-full" style={{ minWidth: 500 }}>
<Outlet />
</div>
</ResizablePanel>
</ResizablePanelGroup>
)}
</PageBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
RESIZABLE_PANEL_ANIMATION,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
collapsibleHandleClassName,
} from "~/components/primitives/Resizable";
import {
Table,
Expand Down Expand Up @@ -255,7 +257,7 @@ export default function Page() {
<TableRow key={deployment.id} className="group" isSelected={isSelected}>
<TableCell to={path} isTabbableCell isSelected={isSelected}>
<div className="flex items-center gap-2">
<Paragraph variant="extra-small">{deployment.shortCode}</Paragraph>
<Paragraph variant="extra-small" className="group-hover/table-row:text-text-bright">{deployment.shortCode}</Paragraph>
{deployment.label && (
<Badge variant="extra-small">{titleCase(deployment.label)}</Badge>
)}
Expand Down Expand Up @@ -388,14 +390,26 @@ export default function Page() {
)}
</ResizablePanel>

{deploymentParam && (
<>
<ResizableHandle id="deployments-handle" />
<ResizablePanel id="deployments-inspector" min="500px" max="800px">
<Outlet />
</ResizablePanel>
</>
)}
<ResizableHandle
id="deployments-handle"
className={collapsibleHandleClassName(!!deploymentParam)}
/>
<ResizablePanel
id="deployments-inspector"
default="400px"
min="400px"
max="800px"
className="overflow-hidden"
collapsible
collapsed={!deploymentParam}
onCollapseChange={() => {}}
collapsedSize="0px"
collapseAnimation={RESIZABLE_PANEL_ANIMATION}
>
<div className="h-full" style={{ minWidth: 400 }}>
<Outlet />
</div>
</ResizablePanel>
</ResizablePanelGroup>
</PageBody>
</PageContainer>
Expand All @@ -405,8 +419,8 @@ export default function Page() {
export function UserTag({ name, avatarUrl }: { name: string; avatarUrl?: string }) {
return (
<div className="flex items-center gap-1">
<UserAvatar avatarUrl={avatarUrl} name={name} className="h-4 w-4" />
<Paragraph variant="extra-small">{name}</Paragraph>
<UserAvatar avatarUrl={avatarUrl} name={name} className="h-4 w-4 group-hover/table-row:text-text-bright" />
<Paragraph variant="extra-small" className="group-hover/table-row:text-text-bright">{name}</Paragraph>
</div>
);
}
Expand Down
Loading