@@ -29,6 +29,29 @@ import { useOrganization } from "~/hooks/useOrganizations";
2929import { useProject } from "~/hooks/useProject" ;
3030import { QueueName } from "../runs/v3/QueueName" ;
3131
32+ const MAX_STRING_DISPLAY_LENGTH = 64 ;
33+
34+ /**
35+ * Truncate a string for display, adding ellipsis if it exceeds max length
36+ */
37+ function truncateString ( value : string , maxLength : number = MAX_STRING_DISPLAY_LENGTH ) : string {
38+ if ( value . length <= maxLength ) {
39+ return value ;
40+ }
41+ return value . slice ( 0 , maxLength ) + "…" ;
42+ }
43+
44+ /**
45+ * Convert any value to a string suitable for copying
46+ * Objects and arrays are JSON stringified, primitives use String()
47+ */
48+ function valueToString ( value : unknown ) : string {
49+ if ( value === null ) return "NULL" ;
50+ if ( value === undefined ) return "UNDEFINED" ;
51+ if ( typeof value === "object" ) return JSON . stringify ( value ) ;
52+ return String ( value ) ;
53+ }
54+
3255/**
3356 * Check if a ClickHouse type is a DateTime type
3457 */
@@ -76,9 +99,25 @@ function CellValue({
7699 column : OutputColumnMetadata ;
77100 prettyFormatting ?: boolean ;
78101} ) {
79- // Plain text mode - render everything as monospace text
102+ // Plain text mode - render everything as monospace text with truncation
80103 if ( ! prettyFormatting ) {
81- return < pre className = "font-mono text-xs" > { value === null ? "NULL" : String ( value ) } </ pre > ;
104+ const plainValue = value === null ? "NULL" : String ( value ) ;
105+ const isTruncated = plainValue . length > MAX_STRING_DISPLAY_LENGTH ;
106+
107+ if ( isTruncated ) {
108+ return (
109+ < SimpleTooltip
110+ content = {
111+ < pre className = "max-w-sm whitespace-pre-wrap break-all font-mono text-xs" >
112+ { plainValue }
113+ </ pre >
114+ }
115+ button = { < pre className = "font-mono text-xs" > { truncateString ( plainValue ) } </ pre > }
116+ />
117+ ) ;
118+ }
119+
120+ return < pre className = "font-mono text-xs" > { plainValue } </ pre > ;
82121 }
83122
84123 if ( value === null ) {
@@ -201,15 +240,51 @@ function CellValue({
201240
202241 // JSON type
203242 if ( type === "JSON" ) {
204- return < span className = "font-mono text-xs text-text-dimmed" > { JSON . stringify ( value ) } </ span > ;
243+ const jsonString = JSON . stringify ( value ) ;
244+ const isTruncated = jsonString . length > MAX_STRING_DISPLAY_LENGTH ;
245+
246+ if ( isTruncated ) {
247+ return (
248+ < SimpleTooltip
249+ content = {
250+ < pre className = "max-w-sm whitespace-pre-wrap break-all font-mono text-xs" >
251+ { jsonString }
252+ </ pre >
253+ }
254+ button = {
255+ < span className = "font-mono text-xs text-text-dimmed" > { truncateString ( jsonString ) } </ span >
256+ }
257+ />
258+ ) ;
259+ }
260+ return < span className = "font-mono text-xs text-text-dimmed" > { jsonString } </ span > ;
205261 }
206262
207263 // Array types
208264 if ( type . startsWith ( "Array" ) ) {
209- return < span className = "font-mono text-xs text-text-dimmed" > { JSON . stringify ( value ) } </ span > ;
265+ const arrayString = JSON . stringify ( value ) ;
266+ const isTruncated = arrayString . length > MAX_STRING_DISPLAY_LENGTH ;
267+
268+ if ( isTruncated ) {
269+ return (
270+ < SimpleTooltip
271+ content = {
272+ < pre className = "max-w-sm whitespace-pre-wrap break-all font-mono text-xs" >
273+ { arrayString }
274+ </ pre >
275+ }
276+ button = {
277+ < span className = "font-mono text-xs text-text-dimmed" >
278+ { truncateString ( arrayString ) }
279+ </ span >
280+ }
281+ />
282+ ) ;
283+ }
284+ return < span className = "font-mono text-xs text-text-dimmed" > { arrayString } </ span > ;
210285 }
211286
212- // Boolean-like types (UInt8 is commonly used for booleans in ClickHouse)
287+ // Boolean types
213288 if ( isBooleanType ( type ) ) {
214289 if ( typeof value === "boolean" ) {
215290 return < span className = "text-text-dimmed" > { value ? "true" : "false" } </ span > ;
@@ -220,16 +295,32 @@ function CellValue({
220295 return < span > { String ( value ) } </ span > ;
221296 }
222297
223- // Numeric types (excluding UInt8 which is handled as boolean above)
224- if ( isNumericType ( type ) && type !== "UInt8" && type !== "Nullable(UInt8)" ) {
298+ // Numeric types
299+ if ( isNumericType ( type ) ) {
225300 if ( typeof value === "number" ) {
226301 return < span className = "tabular-nums" > { formatNumber ( value ) } </ span > ;
227302 }
228303 return < span > { String ( value ) } </ span > ;
229304 }
230305
231- // Default to string rendering
232- return < span > { String ( value ) } </ span > ;
306+ // Default to string rendering with truncation for long values
307+ const stringValue = String ( value ) ;
308+ const isTruncated = stringValue . length > MAX_STRING_DISPLAY_LENGTH ;
309+
310+ if ( isTruncated ) {
311+ return (
312+ < SimpleTooltip
313+ content = {
314+ < pre className = "max-w-sm whitespace-pre-wrap break-all font-mono text-xs" >
315+ { stringValue }
316+ </ pre >
317+ }
318+ button = { < span > { truncateString ( stringValue ) } </ span > }
319+ />
320+ ) ;
321+ }
322+
323+ return < span > { stringValue } </ span > ;
233324}
234325
235326function ProjectCellValue ( { value } : { value : string } ) {
@@ -268,13 +359,7 @@ function isRightAlignedColumn(column: OutputColumnMetadata): boolean {
268359 return true ;
269360 }
270361
271- // Check for numeric types (excluding UInt8 which is often used for booleans)
272- const { type } = column ;
273- if ( type === "UInt8" || type === "Nullable(UInt8)" ) {
274- return false ;
275- }
276-
277- return isNumericType ( type ) ;
362+ return isNumericType ( column . type ) ;
278363}
279364
280365export function TSQLResultsTable ( {
@@ -331,7 +416,7 @@ export function TSQLResultsTable({
331416 < CopyableTableCell
332417 key = { col . name }
333418 alignment = { isRightAlignedColumn ( col ) ? "right" : "left" }
334- value = { String ( row [ col . name ] ) }
419+ value = { valueToString ( row [ col . name ] ) }
335420 >
336421 < span className = "flex-1" >
337422 < CellValue
0 commit comments