Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
94 changes: 94 additions & 0 deletions components/Smart-Data-viewer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
## Username

widlestudiollp

## Project Name

Smart Data Viewer

## About

Smart Data Viewer is an intelligent data inspection component for Retool that transforms raw object and JSON data into a clean, structured, and interactive viewing experience. It automatically analyzes incoming data and renders each field using context-aware UI patterns such as badges, tags, nested object explorers, links, and expandable content blocks.

The component supports multiple viewing layouts including list, card, and table modes, allowing users to inspect structured data in the format best suited to their workflow. Smart Data Viewer helps developers and internal teams quickly explore records, API responses, payloads, and deeply nested objects without manually formatting raw data.

## Preview

![Smart Data Viewer Preview](preview.png)

## How it works

The component receives data through Retool state (`data`) and dynamically inspects each field to determine how it should be rendered.

It evaluates the type and structure of each value, then automatically applies the most appropriate visual representation for improved readability and usability.

### Rendering logic

* Boolean values → Active / Inactive status chips
* Arrays → Tag pills
* Objects → Expandable nested object viewer
* URLs → Clickable external links
* Emails → Inline email formatting
* Long text → Truncated preview with Show more / Show less
* Null values → Muted null badge
* Standard text / numbers → Plain formatted text

The component recursively renders nested objects, allowing users to expand and inspect deeply structured data without losing context.

Users can switch between multiple view modes depending on how they want to inspect the data.

### View modes

* List view → Key-value row layout for detailed scanning
* Card view → Compact card-based layout for dashboard browsing
* Table view → Structured table layout for dense data inspection

### Example input

```json
{
"id": 8421,
"fullName": "Ada Lovelace",
"email": "ada@analyticalengine.io",
"website": "https://analyticalengine.io/profile/ada",
"is_active": true,
"is_verified": false,
"role": "Senior Engineer",
"skills": ["React", "TypeScript", "Retool", "PostgreSQL"],
"bio": "Ada is a passionate engineer with over a decade of experience building scalable systems and delightful user interfaces.",
"metadata": {
"createdAt": "2024-01-12T10:24:00Z",
"lastLogin": "2026-04-21T08:11:53Z",
"preferences": {
"theme": "dark",
"notifications": true
}
}
}
```

## Build process

The component is built using React and integrates with Retool through `@tryretool/custom-component-support`. It uses custom rendering logic and adaptive UI patterns to intelligently display structured data in a polished and readable format.

### Key implementation details

* Uses React state for layout switching and expandable content
* Integrates directly with Retool state using `Retool.useStateObject`
* Implements intelligent type detection for adaptive rendering
* Recursively renders nested objects for deep inspection
* Supports expandable long-text previews and collapsible object trees
* Automatically adapts layout across list, card, and table views
* Uses custom CSS for a modern dark UI with responsive behavior

### Extensibility

The component is designed to be easily extendable. Developers can:

* Add additional render types (e.g. dates, images, code blocks)
* Add custom field formatters
* Extend nested object rendering behavior
* Add field grouping and sorting
* Add search and filtering controls
* Customize themes and layout styles
* Integrate advanced schema-aware rendering
7 changes: 7 additions & 0 deletions components/Smart-Data-viewer/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "smart-data-viewer",
"title": "Smart Data Viewer",
"author": "@widlestudiollp",
"shortDescription": "An intelligent data inspection component for Retool that analyzes structured input and automatically renders fields using adaptive UI patterns like cards, tables, tags, badges, and nested object explorers.",
"tags": ["Data Viewer", "JSON Viewer", "Object Inspector", "Retool", "Developer Tools"]
}
48 changes: 48 additions & 0 deletions components/Smart-Data-viewer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "custom-component-collection",
"version": "0.1.0",
"private": true,
"dependencies": {
"@tryretool/custom-component-support": "latest",
"qrcode.react": "^4.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "npx retool-ccl dev",
"deploy": "npx retool-ccl deploy",
"test": "vitest"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react": "^18.2.55",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"postcss-modules": "^6.0.0",
"prettier": "^3.0.3",
"vitest": "^4.0.17"
},
"retoolCustomComponentLibraryConfig": {
"name": "SmartForm",
"label": "Smart Form",
"description": "form",
"entryPoint": "src/index.tsx",
"outputPath": "dist"
}
}
Binary file added components/Smart-Data-viewer/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
222 changes: 222 additions & 0 deletions components/Smart-Data-viewer/src/components/keyViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import React, { useState } from "react";
import { Retool } from "@tryretool/custom-component-support";
import "./styles.css";

const SmartDataViewer = () => {

const capitalize = (text: string) => {
if (!text) return "";
return text.charAt(0).toUpperCase() + text.slice(1);
};

const [data] = Retool.useStateObject({
name: "data",
initialValue: {}
});

const [viewMode, setViewMode] = useState("list");
const [search, setSearch] = useState("");
const [expanded, setExpanded] = useState<Record<string, boolean>>({});

if (!data || typeof data !== "object") {
return <div className="empty"> No data </div>;
}

const entries = Object.entries(data).filter(([k]) =>
k.toLowerCase().includes(search.toLowerCase())
);

const getType = (v : any) => {
if (v === null || v === undefined) return "null";
if (typeof v === "boolean") return "boolean";
if (Array.isArray(v)) return "array";
if (typeof v === "object") return "object";
if (typeof v === "string" && v.startsWith("http")) return "url";
if (typeof v === "string" && v.includes("@")) return "email";
if (typeof v === "string" && v.length > 120) return "long";
return "text";
};

const renderValue = (value: any, key: string) => {
const type = getType(value);
switch (type) {
case "boolean":
return (
<span className={`chip ${value ? "active" : "inactive"}`}>
<span className="dot"></span>
{value ? "Active" : "Inactive"}
</span>
);

case "array":
return value.map((v: any, i: any) => (
<span key={i} className="tag">
{capitalize(String(v))}
</span>
));

case "object":
if (!value)
return <span className="null-text">null</span>;

return (
<div className="object-container">

<div
className="object-header"
onClick={() =>
setExpanded({ ...expanded, [key]: !expanded[key] })
} >
<span className="arrow">{expanded[key] ? "▾" : "▸"}</span>
<span className="object-label">Object</span>
<span className="object-count">
({Object.keys(value).length})
</span>
</div>


{expanded[key] && (
<div className="object-children">
{Object.entries(value).map(([k, v]) => (
<div key={k} className="object-row">
<div className="object-key"> {capitalize(k)}:</div>
<div className="object-value">
{renderValue(v, k)}
</div>
</div>
))}
</div>
)}
</div>
);


case "null":
return <span className="badge gray">Null</span>;
case "url":
return (
<a href={value} target="_blank" rel="noreferrer">
{value} ↗
</a>
);

case "email": return <span> ✉ {value} </span>;

case "long":
return (
<>
<span>
{expanded[key]
? capitalize(value)
: capitalize(value.substring(0, 120)) + "..."}
</span>
<div
className="expand"
onClick={() =>
setExpanded({ ...expanded, [key]: !expanded[key] })
}>
{expanded[key] ? "Show less" : "Show more"}
</div>
</>
);

default:
return <span>{capitalize(String(value))}</span>;
}
};

return (
<div className="wrapper">
<div className="header">
<div className="title">
<div className="title-top">
<div className="title-icon">🗄</div>
<div className="title-content">
<div className="title-text">Data Viewer</div>
<span>{entries.length} fields</span>
</div>
</div>
</div>

<div className="actions">
<div className="view-toggle">

<button
onClick={() => setViewMode("list")}
className={viewMode === "list" ? "active" : ""} >
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<line x1="4" y1="6" x2="20" y2="6" stroke="currentColor" strokeWidth="2" />
<line x1="4" y1="12" x2="20" y2="12" stroke="currentColor" strokeWidth="2" />
<line x1="4" y1="18" x2="20" y2="18" stroke="currentColor" strokeWidth="2" />
</svg>
</button>

<button
onClick={() => setViewMode("card")}
className={viewMode === "card" ? "active" : ""} >
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<rect x="4" y="4" width="6" height="6" stroke="currentColor" strokeWidth="2" />
<rect x="14" y="4" width="6" height="6" stroke="currentColor" strokeWidth="2" />
<rect x="4" y="14" width="6" height="6" stroke="currentColor" strokeWidth="2" />
<rect x="14" y="14" width="6" height="6" stroke="currentColor" strokeWidth="2" />
</svg>
</button>


<button
onClick={() => setViewMode("table")}
className={viewMode === "table" ? "active" : ""} >
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="16" stroke="currentColor" strokeWidth="2" />
<line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" strokeWidth="2" />
<line x1="9" y1="4" x2="9" y2="20" stroke="currentColor" strokeWidth="2" />
</svg>
</button>
</div>
</div>
</div>


{viewMode === "list" && (
<div className="list">
{entries.map(([k, v]) => (
<div className="row" key={k}>
<div className="key">
{capitalize(k)}
<div className="sub">{k.toLowerCase()}</div>
</div>
<div className="value">{renderValue(v, k)}</div>
</div>
))}
</div>
)}

{viewMode === "card" && (
<div className="grid">
{entries.map(([k, v]) => (
<div className="card" key={k}>
<div className="card-title"> {capitalize(k)}</div>
<div className="value">{renderValue(v, k)}</div>
</div>
))}
</div>
)}


{viewMode === "table" && (
<table className="table">
<tbody>
{entries.map(([k, v]) => (
<tr key={k}>
<td className="key"> {capitalize(k)}</td>
<td>{renderValue(v, k)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};

export default SmartDataViewer;
Loading
Loading