visual-notebook / frontend /src /components /SchemaView.tsx
Sid-WC121
bugfix3
0719363
Raw History Blame Contribute Delete
6.05 kB
import React, { useEffect, useState } from 'react';
import { useSchema } from '../api/hooks';
import { useSessionStore } from '../store/session';
import { formatNumber, typeBadgeClass, typeLabel } from '../lib/format';
import type { ColumnInfo } from '../api/types';
interface Props {
compact?: boolean;
}
export function SchemaView({ compact = false }: Props) {
const { columns, rowCount } = useSessionStore();
const { fetch, loading } = useSchema();
const [localColumns, setLocalColumns] = useState<ColumnInfo[]>([]);
// Re-fetch schema when columns from the store change (e.g. after operations)
useEffect(() => {
loadSchema();
}, [columns.length, columns.map(c => c.name).join(',')]);
const loadSchema = async () => {
const data = await fetch();
if (data) {
setLocalColumns(data.columns);
useSessionStore.getState().setSchema(data.columns, data.row_count);
}
};
const displayColumns = localColumns.length > 0 ? localColumns : columns;
// Only show numeric stat columns if any column has numeric stats
const anyHasNumericStats = displayColumns.some(
(col) => col.has_numeric_stats === true || col.column_type === 'numeric'
);
const renderStatCell = (value: number | string | null | undefined, col: ColumnInfo) => {
const isNumeric = col.has_numeric_stats === true || col.column_type === 'numeric';
// Non-numeric column: show subtle dash
if (!isNumeric) {
return <span className="stat-na">—</span>;
}
// Numeric column but value is null/undefined
if (value == null) {
return <span className="stat-na">N/A</span>;
}
// Numeric column with actual value: show the formatted value
const numVal = typeof value === 'number' ? value : Number(value);
if (isNaN(numVal)) {
return <span className="stat-na">N/A</span>;
}
return <span className="text-gray-700 font-mono text-xs">{formatNumber(numVal)}</span>;
};
if (loading && displayColumns.length === 0) {
return <div className="text-center py-6 text-gray-400">Loading schema...</div>;
}
// Compact mode: card-based layout for sidebar (avoids wide table overflow)
if (compact) {
return (
<div className="space-y-1.5">
{displayColumns.map((col) => {
const isNumeric = col.has_numeric_stats === true || col.column_type === 'numeric';
return (
<div
key={col.name}
className="bg-gray-50 rounded-lg px-3 py-2 border border-gray-100"
>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-medium text-gray-900 truncate max-w-[140px]" title={col.name}>
{col.name}
</span>
<span className={`badge ${typeBadgeClass(col.column_type)} text-[10px]`}>
{typeLabel(col.column_type)}
</span>
</div>
<div className="flex items-center gap-3 text-[10px] text-gray-500">
<span>{formatNumber(col.count - col.null_count)} filled</span>
<span>{formatNumber(col.unique_count)} unique</span>
{col.null_count > 0 && (
<span className="text-amber-600">{formatNumber(col.null_count)} null</span>
)}
</div>
{isNumeric && (
<div className="flex items-center gap-3 mt-1 text-[10px] text-gray-400 font-mono">
{col.min != null && <span>min {formatNumber(col.min)}</span>}
{col.max != null && <span>max {formatNumber(col.max)}</span>}
{col.mean != null && <span>avg {formatNumber(col.mean)}</span>}
{col.std != null && <span>std {formatNumber(col.std)}</span>}
</div>
)}
</div>
);
})}
{!anyHasNumericStats && (
<p className="text-[10px] text-gray-400 italic mt-2">
No numeric columns — statistical summaries require numeric data.
</p>
)}
</div>
);
}
// Full table mode for cell view
return (
<div className="overflow-x-auto -mx-4 px-4">
<table className="data-table w-full text-xs">
<thead>
<tr>
<th>Column</th>
<th>Type</th>
<th>Non-Null</th>
<th>Unique</th>
{anyHasNumericStats && <th>Min</th>}
{anyHasNumericStats && <th>Max</th>}
{anyHasNumericStats && <th>Mean</th>}
{anyHasNumericStats && <th>Std</th>}
{anyHasNumericStats && <th>Median</th>}
{anyHasNumericStats && <th>IQR</th>}
</tr>
</thead>
<tbody>
{displayColumns.map((col) => {
return (
<tr key={col.name}>
<td className="font-medium text-gray-900 whitespace-normal break-all max-w-[120px]">{col.name}</td>
<td>
<span className={`badge ${typeBadgeClass(col.column_type)}`}>
{typeLabel(col.column_type)} {col.column_type}
</span>
</td>
<td>{formatNumber(col.count - col.null_count)}</td>
<td>{formatNumber(col.unique_count)}</td>
{anyHasNumericStats && <td>{renderStatCell(col.min, col)}</td>}
{anyHasNumericStats && <td>{renderStatCell(col.max, col)}</td>}
{anyHasNumericStats && <td>{renderStatCell(col.mean, col)}</td>}
{anyHasNumericStats && <td>{renderStatCell(col.std, col)}</td>}
{anyHasNumericStats && <td>{renderStatCell(col.median, col)}</td>}
{anyHasNumericStats && <td>{renderStatCell(col.iqr, col)}</td>}
</tr>
);
})}
</tbody>
</table>
{!anyHasNumericStats && (
<p className="text-xs text-gray-400 mt-3 italic">No numeric columns found — statistical summaries require numeric data.</p>
)}
</div>
);
}