Skip to content

Commit

Permalink
feat(dashboard): large variable display (#1213)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryson-g authored Dec 20, 2024
1 parent 838ac65 commit 3a51fbb
Show file tree
Hide file tree
Showing 8 changed files with 693 additions and 7 deletions.
523 changes: 517 additions & 6 deletions dashboard/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
"@tanstack/react-query": "^5.37.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { VARIABLE_TYPES } from '@/app/constants'
import { getVariableValue } from '@/app/utils'
import { ThreadVarDef, Variable, WfRunVariableAccessLevel } from 'littlehorse-client/proto'
import { FC } from 'react'
import { OverflowText } from '../../../../components/OverflowText'

type VariablesProps = {
variableDefs: ThreadVarDef[]
Expand Down Expand Up @@ -30,7 +31,12 @@ export const Variables: FC<VariablesProps> = ({ variableDefs, variables }) => {
<span className="rounded bg-green-300 p-1 text-xs">{accessLevels[variable.accessLevel]}</span>
<span>=</span>
<span className="truncate">
{getVariableValue(variables.find(v => v.id?.name === variable.varDef?.name)?.value)?.toString()}
<OverflowText
className="max-w-96"
text={
getVariableValue(variables.find(v => v.id?.name === variable.varDef?.name)?.value)?.toString() ?? ''
}
/>
</span>
</div>
))}
Expand Down
29 changes: 29 additions & 0 deletions dashboard/src/app/[tenantId]/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client'
import { FC, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Check, Copy } from 'lucide-react'
import { cn } from '@/components/utils'

interface CopyButtonProps {
value: string
className?: string
}

export const CopyButton: FC<CopyButtonProps> = ({ value, className }) => {
const [copied, setCopied] = useState(false)

return (
<Button
variant="ghost"
size="icon"
onClick={() => {
navigator.clipboard.writeText(value)
setCopied(true)
setTimeout(() => setCopied(false), 1000)
}}
className={cn(className)}
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
)
}
60 changes: 60 additions & 0 deletions dashboard/src/app/[tenantId]/components/OverflowText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client'
import { FC, useEffect, useRef, useState } from 'react'
import { cn } from '@/components/utils'
import { Button } from '@/components/ui/button'
import { ChevronRight } from 'lucide-react'
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
import { CopyButton } from './CopyButton'
import { tryFormatAsJson } from '@/app/utils/tryFormatAsJson'

type OverflowTextProps = {
text: string
className?: string
}

export const OverflowText: FC<OverflowTextProps> = ({ text, className }) => {
const textRef = useRef<HTMLDivElement>(null)
const [isOverflowing, setIsOverflowing] = useState(false)

useEffect(() => {
const element = textRef.current
if (element) {
setIsOverflowing(element.scrollWidth > element.clientWidth)
}
}, [text])

const formattedText = tryFormatAsJson(text)

if (isOverflowing) {
return (
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
className={cn(
'flex h-auto w-full items-center justify-between truncate p-1 font-normal hover:no-underline',
className
)}
>
<span className="truncate">{formattedText}</span>
<div className="flex flex-shrink-0 items-center gap-1 text-xs text-muted-foreground">
View More
<ChevronRight className="h-4 w-4 opacity-50" />
</div>
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl gap-2 overflow-visible">
<CopyButton value={formattedText} className="h-8 w-8 rounded-full" />
<div className="h-96 overflow-auto rounded-lg bg-gray-100">
<div className="max-w-full whitespace-pre-wrap break-words p-4">{formattedText}</div>
</div>
</DialogContent>
</Dialog>
)
}
return (
<div ref={textRef} className={className}>
{formattedText}
</div>
)
}
8 changes: 8 additions & 0 deletions dashboard/src/app/utils/tryFormatAsJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function tryFormatAsJson(text: string): string {
try {
const parsed = JSON.parse(text)
return JSON.stringify(parsed, null, 4)
} catch {
return text
}
}
40 changes: 40 additions & 0 deletions dashboard/src/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client'

import * as React from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'

import { cn } from '@/components/utils'

const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export { ScrollArea, ScrollBar }
30 changes: 30 additions & 0 deletions dashboard/src/components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'

import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'

import { cn } from '@/components/utils'

const TooltipProvider = TooltipPrimitive.Provider

const Tooltip = TooltipPrimitive.Root

const TooltipTrigger = TooltipPrimitive.Trigger

const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 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
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName

export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

0 comments on commit 3a51fbb

Please sign in to comment.