Repository: tensorzero/tensorzero
Pull request #1730: Add reusable ID and Timestamp table items 
Author: login: maxbokov
Created: 2025-04-10T01:25:57Z | Merged: 2025-04-18T19:41:46Z

We reuse similar table components across the app in different tables. We want to isolate them instead of setting them up in every file because 1. it's easier to design and update the styles, and 2. we want to extend their functionality, which will make per-file setup problematic.

- Added TableItems — a set of common table components. Currently has two items for IDs and Timestamps;
- TableItemId takes an ID, and optionally a link, shortens the ID by default to 8 symbols, and shows the full ID on hover in the tooltip;
- TableItemTime takes a timestamp, formats the date (as we have the same formatting for all table timestamps), and prevents wrapping.
<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> Add reusable `TableItemShortUuid` and `TableItemTime` components for consistent ID and timestamp rendering in tables.
> 
>   - **Components**:
>     - Added `TableItemShortUuid` and `TableItemTime` in `TableItems.tsx` for reusable table components.
>     - `TableItemShortUuid` shortens IDs to 8 characters, shows full ID on hover, and optionally links to a URL.
>     - `TableItemTime` formats timestamps consistently and prevents wrapping.
>   - **Usage**:
>     - Replaced ID and timestamp rendering in `FeedbackTable.tsx` with `TableItemShortUuid` and `TableItemTime`.
>     - Replaced ID and timestamp rendering in `InferencesTable.tsx` with `TableItemShortUuid` and `TableItemTime`.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=tensorzero%2Ftensorzero&utm_source=github&utm_medium=referral)<sup> for 3de0cf3a1bb4641c460a82b5eb92efbf50b87bcb. It will automatically update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->


Diff:
diff --git a/ui/app/components/feedback/FeedbackTable.tsx b/ui/app/components/feedback/FeedbackTable.tsx
index e533855366b..1b4e3e5857f 100644
--- a/ui/app/components/feedback/FeedbackTable.tsx
+++ b/ui/app/components/feedback/FeedbackTable.tsx
@@ -10,10 +10,9 @@ import {
 import FeedbackValue from "~/components/feedback/FeedbackValue";
 import { getMetricName } from "~/utils/clickhouse/helpers";
 import type { FeedbackRow } from "~/utils/clickhouse/feedback";
-import { formatDate } from "~/utils/date";
 import MetricBadges from "~/components/metric/MetricBadges";
 import { useConfig } from "~/context/config";
-
+import { TableItemShortUuid, TableItemTime } from "~/components/ui/TableItems";
 export default function FeedbackTable({
   feedback,
 }: {
@@ -39,9 +38,7 @@ export default function FeedbackTable({
             feedback.map((item) => (
               <TableRow key={item.id}>
                 <TableCell className="max-w-[200px]">
-                  <code className="block overflow-hidden rounded font-mono text-ellipsis whitespace-nowrap">
-                    {item.id}
-                  </code>
+                  <TableItemShortUuid id={item.id} />
                 </TableCell>
                 <TableCell>
                   <div className="flex items-center gap-2">
@@ -58,7 +55,9 @@ export default function FeedbackTable({
                     metric={metrics[getMetricName(item)]}
                   />
                 </TableCell>
-                <TableCell>{formatDate(new Date(item.timestamp))}</TableCell>
+                <TableCell>
+                  <TableItemTime timestamp={item.timestamp} />
+                </TableCell>
               </TableRow>
             ))
           )}
diff --git a/ui/app/components/ui/TableItems.tsx b/ui/app/components/ui/TableItems.tsx
new file mode 100644
index 00000000000..0ffb3e7cd27
--- /dev/null
+++ b/ui/app/components/ui/TableItems.tsx
@@ -0,0 +1,62 @@
+import { Link } from "react-router";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+  TooltipProvider,
+} from "./tooltip";
+import { formatDate } from "~/utils/date";
+
+interface TableItemShortUuidProps {
+  id: string;
+  link?: string;
+}
+
+function TableItemShortUuid({ id, link }: TableItemShortUuidProps) {
+  const content = (
+    <span
+      className="inline-block max-w-[80px] overflow-hidden align-middle font-mono whitespace-nowrap"
+      style={{ direction: "rtl", textOverflow: "ellipsis" }}
+    >
+      {id}
+    </span>
+  );
+
+  return (
+    <TooltipProvider delayDuration={400}>
+      <Tooltip>
+        <TooltipTrigger asChild>
+          {link ? (
+            <Link
+              to={link}
+              aria-label={id}
+              className="block no-underline transition-colors duration-300 hover:text-gray-500"
+            >
+              {content}
+            </Link>
+          ) : (
+            content
+          )}
+        </TooltipTrigger>
+        <TooltipContent
+          className="border-border bg-bg-secondary text-fg-primary border font-mono shadow-lg"
+          sideOffset={5}
+        >
+          {id}
+        </TooltipContent>
+      </Tooltip>
+    </TooltipProvider>
+  );
+}
+
+interface TableItemTimeProps {
+  timestamp: string | Date;
+}
+
+function TableItemTime({ timestamp }: TableItemTimeProps) {
+  return (
+    <span className="whitespace-nowrap">{formatDate(new Date(timestamp))}</span>
+  );
+}
+
+export { TableItemShortUuid, TableItemTime };
diff --git a/ui/app/routes/observability/inferences/InferencesTable.tsx b/ui/app/routes/observability/inferences/InferencesTable.tsx
index b4eada90bc3..b6e865a2989 100644
--- a/ui/app/routes/observability/inferences/InferencesTable.tsx
+++ b/ui/app/routes/observability/inferences/InferencesTable.tsx
@@ -1,4 +1,3 @@
-import { Link } from "react-router";
 import type { InferenceByIdRow } from "~/utils/clickhouse/inference";
 import {
   Table,
@@ -9,7 +8,7 @@ import {
   TableRow,
   TableEmptyState,
 } from "~/components/ui/table";
-import { formatDate } from "~/utils/date";
+import { TableItemShortUuid, TableItemTime } from "~/components/ui/TableItems";
 import { FunctionLink } from "~/components/function/FunctionLink";
 import { VariantLink } from "~/components/function/variant/VariantLink";
 
@@ -36,25 +35,17 @@ export default function InferencesTable({
           ) : (
             inferences.map((inference) => (
               <TableRow key={inference.id} id={inference.id}>
-                <TableCell className="max-w-[200px]">
-                  <Link
-                    to={`/observability/inferences/${inference.id}`}
-                    className="block no-underline"
-                  >
-                    <code className="block overflow-hidden rounded font-mono text-ellipsis whitespace-nowrap transition-colors duration-300 hover:text-gray-500">
-                      {inference.id}
-                    </code>
-                  </Link>
+                <TableCell>
+                  <TableItemShortUuid
+                    id={inference.id}
+                    link={`/observability/inferences/${inference.id}`}
+                  />
                 </TableCell>
-                <TableCell className="max-w-[200px]">
-                  <Link
-                    to={`/observability/episodes/${inference.episode_id}`}
-                    className="block no-underline"
-                  >
-                    <code className="block overflow-hidden rounded font-mono text-ellipsis whitespace-nowrap transition-colors duration-300 hover:text-gray-500">
-                      {inference.episode_id}
-                    </code>
-                  </Link>
+                <TableCell>
+                  <TableItemShortUuid
+                    id={inference.episode_id}
+                    link={`/observability/episodes/${inference.episode_id}`}
+                  />
                 </TableCell>
                 <TableCell>
                   <FunctionLink functionName={inference.function_name}>
@@ -74,7 +65,7 @@ export default function InferencesTable({
                   </VariantLink>
                 </TableCell>
                 <TableCell>
-                  {formatDate(new Date(inference.timestamp))}
+                  <TableItemTime timestamp={inference.timestamp} />
                 </TableCell>
               </TableRow>
             ))
diff --git a/ui/fixtures/check-fixtures.sh b/ui/fixtures/check-fixtures.sh
index 262f037bc2d..ffd412a4f15 100755
--- a/ui/fixtures/check-fixtures.sh
+++ b/ui/fixtures/check-fixtures.sh
@@ -26,16 +26,16 @@ for table in "${!all_tables[@]}"; do
     # Get total count from database
     db_count=$(clickhouse-client --host $CLICKHOUSE_HOST --user chuser --password chpassword \
               --database tensorzero_ui_fixtures --query "SELECT count() FROM $table" 2>/dev/null || echo "ERROR")
-    
+
     if [[ "$db_count" == "ERROR" ]]; then
         echo "  $table: ERROR accessing ClickHouse"
         mismatch=1
         continue
     fi
-    
+
     # Get the list of files for this table
     read -r -a files <<< "${all_tables[$table]}"
-    
+
     # Count total non-empty lines across all source files
     total_file_count=0
     for file in "${files[@]}"; do
@@ -48,7 +48,7 @@ for table in "${!all_tables[@]}"; do
             mismatch=1
         fi
     done
-    
+
     # Compare counts
     if [ "$db_count" -eq "$total_file_count" ]; then
         echo "  $table: OK (expected: $total_file_count, actual: $db_count rows)"
@@ -68,4 +68,4 @@ if [ $mismatch -eq 0 ]; then
 else
     echo "Some fixture table counts don't match. Please check the output above."
     exit 1
-fi
\ No newline at end of file
+fi


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T01:26:18Z
user: login: chaance
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T02:28:46Z
user: login: maxbokov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T03:37:36Z
user: login: chaance
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T04:23:32Z
user: login: GabrielBianconi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T04:40:55Z
user: login: virajmehta
body: looks good to me.
@maxbokov why are we getting big formatting changes here? 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T01:54:41Z
user: login: maxbokov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T18:33:25Z
user: login: virajmehta
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-13T20:49:55Z
user: login: GabrielBianconi
body: The ID field is not compatible with "Cmd+F". We need to overflow the text into the table (or similar), like we discussed.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-13T21:07:04Z
user: login: virajmehta
body: this looks good!  we should apply these components across the tables in the UI. Can open an issue if you want to do in another PR

state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-17T01:59:35Z
user: login: GabrielBianconi
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-18T14:52:07Z
user: login: virajmehta
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-18T14:52:40Z

Review comments:
diff_hunk: @@ -0,0 +1,67 @@
+import * as React from "react";
+import { Link } from "react-router";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+  TooltipProvider,
+} from "./tooltip";
+import { formatDate } from "~/utils/date";
+
+interface TableItemIdProps {
+  id: string;
+  link?: string;
+}
+
+const TableItemId = React.forwardRef<HTMLSpanElement, TableItemIdProps>(
+  ({ id, link }, ref) => {
+    const content = (
+      <span ref={ref} className="font-mono whitespace-nowrap">
+        {id.length > 5 ? `…${id.slice(-8)}` : id}
+      </span>
path: ui/app/components/ui/TableItems.tsx
user: login: Copilot
body: The component is supposed to shorten the ID to 8 symbols, but the condition 'id.length > 5' may not correctly enforce this. Consider changing the condition to 'id.length > 8' to ensure that only IDs longer than 8 characters are shortened.
```suggestion
        {id.length > 8 ? `…${id.slice(-8)}` : id}
```
created_at: 2025-04-10T01:26:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+import * as React from "react";
+import { Link } from "react-router";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+  TooltipProvider,
+} from "./tooltip";
+import { formatDate } from "~/utils/date";
+
+interface TableItemIdProps {
+  id: string;
+  link?: string;
+}
+
+const TableItemId = React.forwardRef<HTMLSpanElement, TableItemIdProps>(
+  ({ id, link }, ref) => {
+    const content = (
+      <span ref={ref} className="font-mono whitespace-nowrap">
+        {id.length > 8 ? `…${id.slice(-8)}` : id}
+      </span>
+    );
+
+    return (
+      <TooltipProvider delayDuration={400}>
+        <Tooltip>
+          <TooltipTrigger asChild>
+            {link ? (
+              <Link
+                to={link}
+                className="block no-underline transition-colors duration-300 hover:text-gray-500"
+              >
+                {content}
+              </Link>
+            ) : (
+              content
+            )}
+          </TooltipTrigger>
+          <TooltipContent
+            className="border-border bg-bg-secondary text-fg-primary border font-mono shadow-lg"
+            sideOffset={5}
+          >
+            {id}
+          </TooltipContent>
+        </Tooltip>
+      </TooltipProvider>
+    );
+  },
+);
+TableItemId.displayName = "TableItemId";
+
+interface TableItemTimeProps {
+  timestamp: string | Date;
+}
+
+const TableItemTime = React.forwardRef<HTMLSpanElement, TableItemTimeProps>(
path: ui/app/components/ui/TableItems.tsx
user: login: chaance
body: FYI, `forwardRef` is no longer needed in React 19. `ref` is now treated as a normal prop. Simplifies a lot of code to just get rid of it until you actually need it.
created_at: 2025-04-10T02:28:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,67 @@
+import * as React from "react";
+import { Link } from "react-router";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+  TooltipProvider,
+} from "./tooltip";
+import { formatDate } from "~/utils/date";
+
+interface TableItemIdProps {
+  id: string;
+  link?: string;
+}
+
+const TableItemId = React.forwardRef<HTMLSpanElement, TableItemIdProps>(
+  ({ id, link }, ref) => {
+    const content = (
+      <span ref={ref} className="font-mono whitespace-nowrap">
+        {id.length > 8 ? `…${id.slice(-8)}` : id}
+      </span>
+    );
+
+    return (
+      <TooltipProvider delayDuration={400}>
+        <Tooltip>
+          <TooltipTrigger asChild>
+            {link ? (
+              <Link
+                to={link}
+                className="block no-underline transition-colors duration-300 hover:text-gray-500"
+              >
+                {content}
+              </Link>
+            ) : (
+              content
+            )}
+          </TooltipTrigger>
+          <TooltipContent
+            className="border-border bg-bg-secondary text-fg-primary border font-mono shadow-lg"
+            sideOffset={5}
+          >
+            {id}
+          </TooltipContent>
+        </Tooltip>
+      </TooltipProvider>
+    );
+  },
+);
+TableItemId.displayName = "TableItemId";
+
+interface TableItemTimeProps {
+  timestamp: string | Date;
+}
+
+const TableItemTime = React.forwardRef<HTMLSpanElement, TableItemTimeProps>(
path: ui/app/components/ui/TableItems.tsx
user: login: maxbokov
body: sg, removed. thanks!
created_at: 2025-04-10T03:37:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,59 @@
+import * as React from "react";
+import { Link } from "react-router";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+  TooltipProvider,
+} from "./tooltip";
+import { formatDate } from "~/utils/date";
+
+interface TableItemIdProps {
+  id: string;
+  link?: string;
+}
+
+function TableItemId({ id, link }: TableItemIdProps) {
+  const content = (
+    <span className="font-mono whitespace-nowrap">
+      {id.length > 8 ? `…${id.slice(-8)}` : id}
path: ui/app/components/ui/TableItems.tsx
user: login: chaance
body: If the ID isn't truncated, I don't think it would need a tooltip since it's redundant, no?
created_at: 2025-04-10T04:20:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,59 @@
+import * as React from "react";
+import { Link } from "react-router";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+  TooltipProvider,
+} from "./tooltip";
+import { formatDate } from "~/utils/date";
+
+interface TableItemIdProps {
+  id: string;
+  link?: string;
+}
+
+function TableItemId({ id, link }: TableItemIdProps) {
+  const content = (
+    <span className="font-mono whitespace-nowrap">
+      {id.length > 8 ? `…${id.slice(-8)}` : id}
+    </span>
+  );
+
+  return (
+    <TooltipProvider delayDuration={400}>
+      <Tooltip>
+        <TooltipTrigger asChild>
+          {link ? (
+            <Link
+              to={link}
+              className="block no-underline transition-colors duration-300 hover:text-gray-500"
path: ui/app/components/ui/TableItems.tsx
user: login: chaance
body: ```suggestion
              aria-label={id}
              className="block no-underline transition-colors duration-300 hover:text-gray-500"
```
Useful to label the link since the actual contents may be truncated.
created_at: 2025-04-10T04:22:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,59 @@
+import * as React from "react";
+import { Link } from "react-router";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+  TooltipProvider,
+} from "./tooltip";
+import { formatDate } from "~/utils/date";
+
+interface TableItemIdProps {
+  id: string;
+  link?: string;
+}
+
+function TableItemId({ id, link }: TableItemIdProps) {
+  const content = (
+    <span className="font-mono whitespace-nowrap">
+      {id.length > 8 ? `…${id.slice(-8)}` : id}
path: ui/app/components/ui/TableItems.tsx
user: login: GabrielBianconi
body: Today, all IDs are UUIDs. (We use UUIDv7, so the first few characters are often very similar/identical - that's why we show the last 8 characters.)

Maybe makes sense to specialize this as `TableItemUuid` and assume truncation will always be relevant here? No strong opinion, defer to you guys.

created_at: 2025-04-10T04:40:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,59 @@
+import * as React from "react";
+import { Link } from "react-router";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+  TooltipProvider,
+} from "./tooltip";
+import { formatDate } from "~/utils/date";
+
+interface TableItemIdProps {
+  id: string;
+  link?: string;
+}
+
+function TableItemId({ id, link }: TableItemIdProps) {
+  const content = (
+    <span className="font-mono whitespace-nowrap">
+      {id.length > 8 ? `…${id.slice(-8)}` : id}
path: ui/app/components/ui/TableItems.tsx
user: login: maxbokov
body: Hm, yeah, I had in mind that all IDs are UUIDs and they always be truncated. I can imagine that we may have slightly different treatments for different IDs in the future based on the type of the item. Some IDs or names may be truncated on the end and not the beginning, for some of the items we may want to add icons before it, or color codes, and potentially different hover behavior. So this is more of a clean-up iteration fix for the current items.

I'd rename it to TableItemShortUuid and keep as is. If you think we should address the short IDs case anyway here, let me know @chaance , I'm down to add it.
created_at: 2025-04-11T18:33:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: maxbokov
created_at: 2025-04-11T18:14:05Z
body: > why are we getting big formatting changes here?

hm, I haven't touch those files, it looks like pre-commit prettifier adjustment, the code is the same. Do you want me to pull main and create another pr @virajmehta ? 

author_association: CONTRIBUTOR
user: login: virajmehta
created_at: 2025-04-11T19:36:55Z
body: @maxbokov I'll fix these merge conflicts later today
author_association: MEMBER
user: login: maxbokov
created_at: 2025-04-16T17:15:26Z
body: > The ID field is not compatible with "Cmd+F". We need to overflow the text into the table (or similar), like we discussed.

That's fair. I changed the truncation from slice to textOverflow with max-width. Looks the same, but cmd+f works now, and the full id is in the DOM. Take a look @GabrielBianconi 
author_association: CONTRIBUTOR