'use client'
import { ExampleLayoutProps } from '../example-layout'
const appJsCode = `import { useEffect, useState, useCallback, useRef } from 'react';
import { createClient } from '@supabase/supabase-js';
import { AutoSizer, Table, Column, InfiniteLoader } from 'react-virtualized';
import 'react-virtualized/styles.css';
// Initialize Supabase client
const supabaseUrl = '${process.env.NEXT_PUBLIC_EXAMPLES_SUPABASE_URL}';
const supabaseKey = '${process.env.NEXT_PUBLIC_EXAMPLES_SUPABASE_ANON_KEY}';
const supabase = createClient(supabaseUrl, supabaseKey);
// Constants
const PAGE_SIZE = 50;
const TABLE_NAME = 'logging_data';
// Log level colors
const LOG_LEVEL_COLORS = {
ERROR: 'bg-red-800',
WARN: 'bg-amber-800',
INFO: 'bg-green-800',
DEBUG: 'bg-blue-800'
};
export default function App() {
// Instance ID for channel
const instanceId = useRef(Math.random().toString(36).substring(2, 9)).current;
// Log viewer state
const [logs, setLogs] = useState([]);
const [totalCount, setTotalCount] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [isAtTop, setIsAtTop] = useState(true);
const [logCountAtScroll, setLogCountAtScroll] = useState(0);
// Refs
const tableRef = useRef(null);
const gridRef = useRef(null);
const newLogsRef = useRef([]);
// Function to load rows from Supabase
const loadMoreRows = async ({ startIndex, stopIndex }) => {
try {
setIsLoading(true);
setError(null);
console.log(\`Loading rows from \${startIndex} to \${stopIndex}\`);
// Calculate the range for the query
const from = startIndex;
const to = stopIndex;
// Query Supabase for the range of rows
const { data, error, count } = await supabase
.from(TABLE_NAME)
.select('*', { count: 'exact' })
.range(from, to)
.order('created_at', { ascending: false });
if (error) throw error;
// Update total count if we got it
if (count !== null) {
setTotalCount(count);
}
// Update logs with the fetched data
setLogs(prevLogs => {
// Create a new array with the same length as before
const newLogs = [...prevLogs];
// Insert the fetched logs at their correct positions
data.forEach((log, index) => {
newLogs[startIndex + index] = log;
});
return newLogs;
});
return data;
} catch (err) {
console.error('Error loading logs:', err);
setError(err.message);
return [];
} finally {
setIsLoading(false);
}
};
// Function to determine if a row is loaded
const isRowLoaded = ({ index }) => {
return Boolean(logs[index]);
};
// Cell data getters
const cellDataGetter = ({ dataKey, rowData }) => {
return rowData ? rowData[dataKey] : null;
};
// Cell renderers
const idCellRenderer = ({ cellData }) => {
return
#{cellData || ''}
;
};
const messageCellRenderer = ({ cellData }) => {
return (
{cellData || ''}
);
};
const levelCellRenderer = ({ cellData }) => {
if (!cellData) return ;
const level = cellData;
const colorClass = LOG_LEVEL_COLORS[level] || 'bg-neutral-700';
return (
{level}
);
};
const timestampCellRenderer = ({ cellData }) => {
if (!cellData) return ;
const date = new Date(cellData);
const formatted = date.toLocaleString();
return {formatted}
;
};
// Header renderer for all columns
const headerRenderer = ({ label }) => {
return {label}
;
};
// Function to scroll to top
const scrollToTop = () => {
if (gridRef.current) {
gridRef.current.scrollToPosition({ scrollTop: 0 });
// Apply new logs to the main logs array
if (newLogsRef.current.length > 0) {
setLogs(prevLogs => {
// Create a new array with new logs at the beginning
const updatedLogs = [...newLogsRef.current, ...prevLogs];
// Reset new logs reference
newLogsRef.current = [];
return updatedLogs;
});
// Update total count
setTotalCount(prev => prev + newLogsRef.current.length);
}
setIsAtTop(true);
setLogCountAtScroll(0); // Reset the log count at scroll
}
};
// Handle scroll events to detect if at top and capture log count
const handleScroll = ({ scrollTop }) => {
const wasAtTop = isAtTop;
setIsAtTop(scrollTop === 0);
// If we just started scrolling, capture the current log count
if (wasAtTop && scrollTop > 0) {
setLogCountAtScroll(totalCount);
}
// If scrolled back to top, apply new logs and reset counter
if (scrollTop === 0 && newLogsRef.current.length > 0) {
// Apply new logs to the main logs array
setLogs(prevLogs => {
// Create a new array with new logs at the beginning
const updatedLogs = [...newLogsRef.current, ...prevLogs];
// Reset new logs reference
newLogsRef.current = [];
return updatedLogs;
});
// Update total count
setTotalCount(prev => prev + newLogsRef.current.length);
setLogCountAtScroll(0); // Reset the log count at scroll
}
};
// Load initial data when component mounts
useEffect(() => {
const loadInitialData = async () => {
try {
console.log('Loading initial log data');
setIsLoading(true);
setError(null);
// Query Supabase for the first page of logs
const { data, error, count } = await supabase
.from(TABLE_NAME)
.select('*', { count: 'exact' })
.order('created_at', { ascending: false })
.limit(PAGE_SIZE);
if (error) throw error;
console.log(\`Loaded \${data?.length || 0} initial logs, total count: \${count}\`);
// Update total count if we got it
if (count !== null) {
setTotalCount(count);
}
// Set the initial logs
setLogs(data || []);
} catch (err) {
console.error('Error loading initial logs:', err);
setError(err.message);
} finally {
setIsLoading(false);
}
};
loadInitialData();
}, []);
// Subscribe to real-time updates
useEffect(() => {
// Set up the channel for real-time updates
const channel = supabase
.channel('logs', { config: { private: true } })
.on('broadcast', { event: 'INSERT' }, (payload) => {
console.log('New log entry received:', payload);
// If we're at the top, add the new log to the beginning of the list
if (isAtTop) {
setLogs(prevLogs => [payload.payload.record, ...prevLogs]);
setTotalCount(prev => prev + 1);
} else {
// Otherwise, store it in the newLogsRef and increment the counter
newLogsRef.current = [payload.payload.record, ...newLogsRef.current];
setTotalCount(prev => prev + 1);
}
})
.subscribe(status => console.log("status:", status));
console.log('Real-time subscription set up successfully');
return () => {
console.log('Cleaning up real-time subscription');
channel.unsubscribe();
};
}, []); // Only run once on mount
// Add a separate effect to handle isAtTop changes
useEffect(() => {
console.log('isAtTop changed:', isAtTop);
// If we're back at the top and have new logs, apply them
if (isAtTop && newLogsRef.current.length > 0) {
console.log('Applying new logs at top:', newLogsRef.current.length);
setLogs(prevLogs => {
// Create a new array with new logs at the beginning
const updatedLogs = [...newLogsRef.current, ...prevLogs];
// Reset new logs reference
newLogsRef.current = [];
return updatedLogs;
});
// Update total count and reset counter
setTotalCount(prev => prev + newLogsRef.current.length);
setLogCountAtScroll(0); // Reset the log count at scroll
}
}, [isAtTop]);
// Save grid reference when table is rendered
const onTableRendered = useCallback(({ registerChild }) => {
return (ref) => {
registerChild(ref);
tableRef.current = ref;
// Get the Grid component from the Table
if (ref) {
gridRef.current = ref.Grid;
}
};
}, []);
// Calculate if we should show the new logs notification
const shouldShowNotification = !isAtTop && logCountAtScroll > 0 && totalCount > logCountAtScroll;
return (
{/* Header */}
Log Viewer
{isLoading && Loading...}
Total Logs: {totalCount}
{/* Error message */}
{error && (
Error: {error}
)}
{/* New logs badge */}
{shouldShowNotification && (
{totalCount - logCountAtScroll} new log{totalCount - logCountAtScroll === 1 ? '' : 's'}
)}
{/* Main content */}
{({ width, height }) => (
{({ onRowsRendered, registerChild }) => (
logs[index] || {}}
onRowsRendered={onRowsRendered}
onScroll={handleScroll}
overscanRowCount={10}
className="w-full"
headerClassName="text-xs uppercase text-neutral-500"
gridClassName="w-full"
rowClassName="border-b border-neutral-800 hover:bg-neutral-800 w-full"
>
)}
)}
);
}`
const logViewerFiles = {
'/App.js': appJsCode,
'/styles.css': `/* No custom CSS needed - using Tailwind */`,
}
const layoutProps: ExampleLayoutProps = {
appJsCode,
files: logViewerFiles,
dependencies: {
'react-virtualized': 'latest',
},
title: 'Log Viewer',
description:
"A real-time log viewer that uses Supabase Realtime's broadcast channel to stream and display log entries as they occur across multiple instances.",
}
export default layoutProps