Skip to content

Commit

Permalink
feat: Add view functionality to render tables on ui
Browse files Browse the repository at this point in the history
  • Loading branch information
jayanth-kumar-morem committed Dec 31, 2024
1 parent 7688f32 commit e7e2d56
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 71 deletions.
189 changes: 120 additions & 69 deletions frontend/src/components/pages/Connections.jsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,131 @@
import React from "react";
import React, { useEffect, useState } from 'react';

const Connections = () => {
// Toggle this variable to enable/disable dummy data
const useDummyData = false;
import { websocket } from '../../utils/websocket';

const connections = useDummyData
? [
{
id: 1,
icon: "https://via.placeholder.com/40", // Replace with actual icon URL
title: "User 1 - example_connection",
description: "Connected by User 1",
date: "October 21, 2024",
status: "Connected",
},
{
id: 2,
icon: "https://via.placeholder.com/40", // Replace with actual icon URL
title: "User 2 - another_connection",
description: "Connected by User 2",
date: "October 21, 2024",
status: "Connected",
},
]
: [];
const ConnectionCard = ({ connection }) => {
const getConnectionIcon = (type) => {
switch (type.toLowerCase()) {
case 'dataframe':
return (
<svg className="w-8 h-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2 1 3 3 3h10c2 0 3-1 3-3V7c0-2-1-3-3-3H7c-2 0-3 1-3 3zm0 5h16" />
</svg>
);
case 'engine':
return (
<svg className="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
);
default:
return (
<svg className="w-8 h-8 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2 1 3 3 3h10c2 0 3-1 3-3V7c0-2-1-3-3-3H7c-2 0-3 1-3 3z" />
</svg>
);
}
};

return (
<div className="p-5 max-w-4xl">
<h1 className="text-2xl font-bold mb-4">Connections</h1>
{connections.length === 0 ? (
<p className="text-gray-500">No connections available.</p>
) : (
<div className="space-y-4">
{connections.map((connection) => (
<div
key={connection.id}
className="flex items-center p-4 border rounded-lg shadow-sm hover:shadow-md transition-shadow"
>
{/* Icon */}
<img
src={connection.icon}
alt="icon"
className="w-10 h-10 rounded-full mr-4"
/>
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow duration-200">
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
{getConnectionIcon(connection.type)}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{connection.name}
</h3>
<p className="mt-1 text-sm text-gray-500">
Type: {connection.type}
</p>
<p className="mt-2 text-sm text-gray-600 break-all">
{connection.details}
</p>
</div>
</div>
</div>
);
};

const Connections = () => {
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

{/* Details */}
<div className="flex-1">
<h2 className="text-lg font-medium">{connection.title}</h2>
<p className="text-sm text-gray-500">
{connection.date}{connection.description}
</p>
</div>
useEffect(() => {
// Subscribe to WebSocket updates
const handleWebSocketMessage = (message) => {
if (message.type === 'connections_update') {
setConnections(message.connections);
setLoading(false);
}
};

{/* Status */}
<div className="flex items-center space-x-2">
<span className="px-3 py-1 text-sm font-semibold text-green-800 bg-green-100 rounded-lg">
{connection.status}
</span>
<button
className="text-gray-400 hover:text-gray-600"
aria-label="Options"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M6 10a1 1 0 011-1h6a1 1 0 010 2H7a1 1 0 01-1-1z" />
</svg>
</button>
</div>
websocket.subscribe(handleWebSocketMessage);

return () => {
websocket.unsubscribe(handleWebSocketMessage);
};
}, []);

if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
}

if (error) {
return (
<div className="min-h-screen p-6">
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-red-700">
{error}
</p>
</div>
))}
</div>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900">Data Connections</h1>
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
{connections.length} Active
</span>
</div>
)}

{connections.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm p-6 text-center">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M12 5l7 7-7 7" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No connections</h3>
<p className="mt-1 text-sm text-gray-500">
Get started by creating a new data connection.
</p>
</div>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{connections.map((connection, index) => (
<ConnectionCard key={index} connection={connection} />
))}
</div>
)}
</div>
</div>
);
};
Expand Down
78 changes: 78 additions & 0 deletions preswald/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import numpy as np
import json
import pandas as pd

# Configure logging
logging.basicConfig(level=logging.DEBUG)
Expand Down Expand Up @@ -223,4 +224,81 @@ def plotly(fig):
"error": f"Failed to create plot: {str(e)}"
}
_rendered_html.append(error_component)
return error_component

def table(data, title=None):
"""Create a table component that renders data using TableViewerWidget.
Args:
data: List of dictionaries or pandas DataFrame to display
title: Optional title for the table
"""
id = generate_id("table")
logger.debug(f"Creating table component with id {id}")

try:
# Convert pandas DataFrame to list of dictionaries if needed
if hasattr(data, 'to_dict'):
# Reset index and drop it to avoid index column in output
if isinstance(data, pd.DataFrame):
data = data.reset_index(drop=True)
data = data.to_dict('records')

# Ensure data is a list
if not isinstance(data, list):
data = [data] if data else []

# Convert each row to ensure JSON serialization
processed_data = []
for row in data:
if isinstance(row, dict):
processed_row = {}
for key, value in row.items():
# Convert key to string to ensure it's serializable
key_str = str(key)
# Handle special cases and convert value
if pd.isna(value):
processed_row[key_str] = None
elif isinstance(value, (pd.Timestamp, pd.DatetimeTZDtype)):
processed_row[key_str] = str(value)
elif isinstance(value, (np.integer, np.floating)):
processed_row[key_str] = value.item()
elif isinstance(value, (list, np.ndarray)):
processed_row[key_str] = convert_to_serializable(value)
else:
try:
# Try to serialize to test if it's JSON-compatible
json.dumps(value)
processed_row[key_str] = value
except:
# If serialization fails, convert to string
processed_row[key_str] = str(value)
processed_data.append(processed_row)
else:
# If row is not a dict, convert it to a simple dict
processed_data.append({"value": str(row)})

component = {
"type": "table",
"id": id,
"data": processed_data,
"title": str(title) if title is not None else None
}

# Verify JSON serialization before returning
json.dumps(component)

logger.debug(f"Created table component: {component}")
_rendered_html.append(component)
return component

except Exception as e:
logger.error(f"Error creating table component: {str(e)}")
error_component = {
"type": "table",
"id": id,
"data": [],
"title": f"Error: {str(e)}"
}
_rendered_html.append(error_component)
return error_component
1 change: 1 addition & 0 deletions preswald/static/assets/index-DaWEMM6A.css

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions preswald_project/hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
text("# Earthquake Analytics Dashboard 🌍")

# Load and connect data
connection_name = connect("earthquake_data.csv", "earthquake_connection")
connection_name = connect("/Users/jayanth.kumar/Downloads/work/structuredLabs/preswald/examples/earthquake_data.csv", "earthquake_connection")

psql_connection_name = connect("", "psql_connection")
psql_connection_name = connect("postgresql://iris_user:IrisUser%40123@34.171.68.74/iris_database", "psql_connection")

# Slider for filtering magnitude
# here min_magnitude is {'type': 'slider', 'id': 'slider-f6dab796', 'label': 'Minimum Magnitude', 'min': 0.0, 'max': 10.0, 'step': 1, 'value': 5.0}
Expand Down Expand Up @@ -66,3 +66,5 @@
result = connection.execute(query)

text(str(result.fetchall()))
view(connection_name)
view(psql_connection_name)

0 comments on commit e7e2d56

Please sign in to comment.