@@ -6,16 +6,18 @@ import {
66 ExclamationTriangleIcon ,
77 LightBulbIcon ,
88 MagnifyingGlassIcon ,
9+ XMarkIcon ,
910 UserPlusIcon ,
1011 VideoCameraIcon ,
1112} from "@heroicons/react/20/solid" ;
1213import { json , type MetaFunction } from "@remix-run/node" ;
13- import { Link , useRevalidator , useSubmit } from "@remix-run/react" ;
14+ import { Link , useFetcher , useRevalidator } from "@remix-run/react" ;
1415import { type ActionFunctionArgs , type LoaderFunctionArgs } from "@remix-run/server-runtime" ;
1516import { DiscordIcon } from "@trigger.dev/companyicons" ;
1617import { formatDurationMilliseconds } from "@trigger.dev/core/v3" ;
1718import type { TaskRunStatus } from "@trigger.dev/database" ;
18- import { Fragment , Suspense , useEffect , useState } from "react" ;
19+ import { Fragment , Suspense , useCallback , useEffect , useRef , useState } from "react" ;
20+ import type { PanelHandle } from "react-window-splitter" ;
1921import { Bar , BarChart , ResponsiveContainer , Tooltip , type TooltipProps } from "recharts" ;
2022import { TypedAwait , typeddefer , useTypedLoaderData } from "remix-typedjson" ;
2123import { ExitIcon } from "~/assets/icons/ExitIcon" ;
@@ -42,9 +44,11 @@ import { Paragraph } from "~/components/primitives/Paragraph";
4244import { PopoverMenuItem } from "~/components/primitives/Popover" ;
4345import * as Property from "~/components/primitives/PropertyTable" ;
4446import {
47+ RESIZABLE_PANEL_ANIMATION ,
4548 ResizableHandle ,
4649 ResizablePanel ,
4750 ResizablePanelGroup ,
51+ collapsibleHandleClassName ,
4852} from "~/components/primitives/Resizable" ;
4953import { Spinner } from "~/components/primitives/Spinner" ;
5054import { StepNumber } from "~/components/primitives/StepNumber" ;
@@ -84,6 +88,7 @@ import {
8488 uiPreferencesStorage ,
8589} from "~/services/preferences/uiPreferences.server" ;
8690import { requireUserId } from "~/services/session.server" ;
91+ import { motion } from "framer-motion" ;
8792import { cn } from "~/utils/cn" ;
8893import {
8994 docsPath ,
@@ -192,14 +197,20 @@ export default function Page() {
192197 } , [ streamedEvents ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
193198
194199 const [ showUsefulLinks , setShowUsefulLinks ] = useState ( usefulLinksPreference ?? true ) ;
200+ const usefulLinksPanelRef = useRef < PanelHandle > ( null ) ;
201+ const fetcher = useFetcher ( ) ;
202+ const fetcherRef = useRef ( fetcher ) ;
203+ fetcherRef . current = fetcher ;
195204
196- // Create a submit handler to save the preference
197- const submit = useSubmit ( ) ;
198-
199- const handleUsefulLinksToggle = ( show : boolean ) => {
205+ const toggleUsefulLinks = useCallback ( ( show : boolean ) => {
200206 setShowUsefulLinks ( show ) ;
201- submit ( { showUsefulLinks : show . toString ( ) } , { method : "post" } ) ;
202- } ;
207+ if ( show ) {
208+ usefulLinksPanelRef . current ?. expand ( ) ;
209+ } else {
210+ usefulLinksPanelRef . current ?. collapse ( ) ;
211+ }
212+ fetcherRef . current . submit ( { showUsefulLinks : show . toString ( ) } , { method : "post" } ) ;
213+ } , [ ] ) ;
203214
204215 return (
205216 < PageContainer >
@@ -226,27 +237,24 @@ export default function Page() {
226237 </ NavBar >
227238 < PageBody scrollable = { false } >
228239 < ResizablePanelGroup orientation = "horizontal" className = "max-h-full" >
229- < ResizablePanel id = "tasks-main" className = "max-h-full" >
240+ < ResizablePanel id = "tasks-main" min = "100px" className = "max-h-full" >
230241 < div className = { cn ( "grid h-full grid-rows-1" ) } >
231242 { hasTasks ? (
232243 < div className = "flex min-w-0 max-w-full flex-col" >
233244 { tasks . length === 0 ? < UserHasNoTasks /> : null }
234245 < div className = "max-h-full overflow-hidden" >
235- < div className = "flex items-center gap-1 p-2" >
236- < Input
237- placeholder = "Search tasks"
238- variant = "tertiary"
239- icon = { MagnifyingGlassIcon }
240- fullWidth = { true }
246+ < div className = "flex items-center justify-between gap-1 p-2" >
247+ < AnimatedSearchField
241248 value = { filterText }
242- onChange = { ( e ) => setFilterText ( e . target . value ) }
249+ onChange = { setFilterText }
250+ placeholder = "Search tasks…"
243251 autoFocus
244252 />
245- { ! showUsefulLinks && (
253+ { ! showUsefulLinks && (
246254 < Button
247- variant = "minimal /small"
255+ variant = "secondary /small"
248256 TrailingIcon = { LightBulbIcon }
249- onClick = { ( ) => handleUsefulLinksToggle ( true ) }
257+ onClick = { ( ) => toggleUsefulLinks ( true ) }
250258 className = "px-2.5"
251259 />
252260 ) }
@@ -417,20 +425,29 @@ export default function Page() {
417425 ) }
418426 </ div >
419427 </ ResizablePanel >
420- { hasTasks && showUsefulLinks ? (
421- < >
422- < ResizableHandle id = "tasks-handle" />
423- < ResizablePanel
424- id = "tasks-inspector"
425- min = "200px"
426- default = "400px"
427- max = "500px"
428- className = "w-full"
429- >
430- < HelpfulInfoHasTasks onClose = { ( ) => handleUsefulLinksToggle ( false ) } />
431- </ ResizablePanel >
432- </ >
433- ) : null }
428+ < ResizableHandle
429+ id = "tasks-handle"
430+ className = { collapsibleHandleClassName ( hasTasks && showUsefulLinks ) }
431+ />
432+ < ResizablePanel
433+ id = "tasks-inspector"
434+ handle = { usefulLinksPanelRef }
435+ default = "400px"
436+ min = "400px"
437+ max = "500px"
438+ className = "overflow-hidden"
439+ collapsible
440+ collapsed = { ! hasTasks || ! showUsefulLinks }
441+ onCollapseChange = { ( ) => { } }
442+ collapsedSize = "0px"
443+ collapseAnimation = { RESIZABLE_PANEL_ANIMATION }
444+ >
445+ < div className = "h-full" style = { { minWidth : 400 } } >
446+ { hasTasks && (
447+ < HelpfulInfoHasTasks onClose = { ( ) => toggleUsefulLinks ( false ) } />
448+ ) }
449+ </ div >
450+ </ ResizablePanel >
434451 </ ResizablePanelGroup >
435452 </ PageBody >
436453 </ PageContainer >
@@ -850,3 +867,54 @@ function FailedToLoadStats() {
850867 />
851868 ) ;
852869}
870+
871+ function AnimatedSearchField ( {
872+ value,
873+ onChange,
874+ placeholder,
875+ autoFocus,
876+ } : {
877+ value : string ;
878+ onChange : ( value : string ) => void ;
879+ placeholder ?: string ;
880+ autoFocus ?: boolean ;
881+ } ) {
882+ const [ isFocused , setIsFocused ] = useState ( false ) ;
883+
884+ return (
885+ < motion . div
886+ initial = { { width : "auto" } }
887+ animate = { { width : isFocused && value . length > 0 ? "24rem" : "auto" } }
888+ transition = { { type : "spring" , stiffness : 300 , damping : 30 } }
889+ className = "relative h-6 min-w-52"
890+ >
891+ < Input
892+ type = "text"
893+ variant = "secondary-small"
894+ placeholder = { placeholder }
895+ value = { value }
896+ onChange = { ( e ) => onChange ( e . target . value ) }
897+ fullWidth
898+ autoFocus = { autoFocus }
899+ className = { cn ( isFocused && "placeholder:text-text-dimmed/70" ) }
900+ onFocus = { ( ) => setIsFocused ( true ) }
901+ onBlur = { ( ) => setIsFocused ( false ) }
902+ onKeyDown = { ( e ) => {
903+ if ( e . key === "Escape" ) e . currentTarget . blur ( ) ;
904+ } }
905+ icon = { < MagnifyingGlassIcon className = "size-4" /> }
906+ accessory = {
907+ value . length > 0 ? (
908+ < button
909+ type = "button"
910+ onClick = { ( ) => onChange ( "" ) }
911+ className = "flex size-4.5 items-center justify-center rounded-[2px] border border-text-dimmed/40 text-text-dimmed transition hover:bg-charcoal-600 hover:text-text-bright"
912+ >
913+ < XMarkIcon className = "size-3" />
914+ </ button >
915+ ) : undefined
916+ }
917+ />
918+ </ motion . div >
919+ ) ;
920+ }
0 commit comments