From 34b4f398c1ee5ec9fe50b82c4b62fec970a3d565 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Wed, 6 Nov 2024 03:28:47 +0530 Subject: [PATCH 1/4] feat: update and optimize all the files Signed-off-by: Adithya Krishna --- server/ui/src/components/CopyText.tsx | 40 ++-- server/ui/src/components/Footer.tsx | 15 +- server/ui/src/components/InfoBox.tsx | 9 +- .../ui/src/components/NamespaceSelector.tsx | 39 ++-- .../{inputs => }/TruncatedDescription.tsx | 0 server/ui/src/components/TruncatedText.tsx | 12 +- server/ui/src/components/VersionDisplay.tsx | 160 ++++++++------- .../components/cards/ComputeGraphsCard.tsx | 157 ++++++--------- .../ui/src/components/cards/ExecutorsCard.tsx | 153 ++++++++------- .../src/components/cards/NamespacesCard.tsx | 129 +++++++------ server/ui/src/components/inputs/DropZone.tsx | 152 --------------- .../ui/src/components/inputs/LabelsInput.tsx | 139 ------------- .../src/components/inputs/ScrollableChips.tsx | 91 --------- .../components/tables/ComputeGraphTable.tsx | 143 +++++++------- .../tables/InvocationOutputTable.tsx | 99 +++++----- .../tables/InvocationTasksTable.tsx | 182 +++++++----------- .../components/tables/InvocationsTable.tsx | 39 ++-- server/ui/src/index.tsx | 39 ++-- .../routes/Namespace/ComputeGraphsPage.tsx | 14 +- .../ui/src/routes/Namespace/ExecutorsPage.tsx | 24 +-- .../Namespace/IndividualComputeGraphPage.tsx | 77 +++++--- .../Namespace/IndividualInvocationPage.tsx | 137 +++++++------ .../src/routes/Namespace/NamespacesPage.tsx | 22 +-- server/ui/src/routes/Namespace/types.ts | 30 +++ server/ui/src/routes/root.tsx | 175 +++++++++-------- server/ui/src/utils/helpers.ts | 139 +++++++------ server/ui/src/utils/loaders.ts | 66 ++++--- 27 files changed, 998 insertions(+), 1284 deletions(-) rename server/ui/src/components/{inputs => }/TruncatedDescription.tsx (100%) delete mode 100644 server/ui/src/components/inputs/DropZone.tsx delete mode 100644 server/ui/src/components/inputs/LabelsInput.tsx delete mode 100644 server/ui/src/components/inputs/ScrollableChips.tsx create mode 100644 server/ui/src/routes/Namespace/types.ts diff --git a/server/ui/src/components/CopyText.tsx b/server/ui/src/components/CopyText.tsx index 0489111a1..4e721bec8 100644 --- a/server/ui/src/components/CopyText.tsx +++ b/server/ui/src/components/CopyText.tsx @@ -2,33 +2,41 @@ import { Box, IconButton, Tooltip } from "@mui/material"; import ContentCopy from "@mui/icons-material/ContentCopy"; import { useState } from "react"; -const CopyText = ({ - text, - color, - className, - tooltipTitle = "Copy to clipboard", - copiedTooltipTitle = "Copied!" -}: { +interface CopyTextProps { text: string; color?: string; className?: string; tooltipTitle?: string; copiedTooltipTitle?: string; -}) => { - const [showAlert, setShowAlert] = useState(false); - const handleCopy = () => { - navigator.clipboard.writeText(text); - setShowAlert(true); +} + +export function CopyText({ + text, + className, + tooltipTitle = "Copy to clipboard", + copiedTooltipTitle = "Copied!" +}: CopyTextProps) { + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + } catch (error) { + console.error('Failed to copy text:', error); + } }; + return ( - - - + + + ); -}; +} export default CopyText; diff --git a/server/ui/src/components/Footer.tsx b/server/ui/src/components/Footer.tsx index f1c055da4..00195b5d2 100644 --- a/server/ui/src/components/Footer.tsx +++ b/server/ui/src/components/Footer.tsx @@ -1,19 +1,20 @@ import { Box, Typography } from "@mui/material"; import Link from "@mui/material/Link"; -const Footer = () => { +export function Footer() { + const currentYear = new Date().getFullYear(); + return ( - + - {"Copyright © "} + Copyright © Tensorlake - {" "} - {new Date().getFullYear()} - {"."} + + {` ${currentYear}.`} ); -}; +} export default Footer; diff --git a/server/ui/src/components/InfoBox.tsx b/server/ui/src/components/InfoBox.tsx index 51ac4f972..23f4fb81e 100644 --- a/server/ui/src/components/InfoBox.tsx +++ b/server/ui/src/components/InfoBox.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Box, Typography } from '@mui/material'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; @@ -6,7 +5,7 @@ interface InfoBoxProps { text?: string; } -const InfoBox: React.FC = ({text}) => { +export function InfoBox({ text }: InfoBoxProps) { return ( = ({text}) => { justifyContent: 'center', maxWidth: '500px', margin: '10px', - boxShadow: "0px 1px 2px 0px #00000040 inset", + boxShadow: '0px 1px 2px 0px #00000040 inset', }} > = ({text}) => { ); -}; +} -export default InfoBox; \ No newline at end of file +export default InfoBox; diff --git a/server/ui/src/components/NamespaceSelector.tsx b/server/ui/src/components/NamespaceSelector.tsx index dcae77931..a7dff01b7 100644 --- a/server/ui/src/components/NamespaceSelector.tsx +++ b/server/ui/src/components/NamespaceSelector.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { useNavigate, useParams, useLocation, useLoaderData } from 'react-router-dom'; import { FormControl, @@ -19,7 +18,7 @@ interface LoaderData { namespace: string; } -const NamespaceSelector = () => { +export function NamespaceSelector() { const navigate = useNavigate(); const location = useLocation(); const { namespace } = useParams(); @@ -27,49 +26,39 @@ const NamespaceSelector = () => { const handleNamespaceChange = (event: SelectChangeEvent) => { const value = event.target.value; - const pathSegments = location.pathname.split('/').filter(Boolean); - - if (pathSegments[0] === 'namespaces') { + const [firstSegment, secondSegment, ...rest] = location.pathname + .split('/') + .filter(Boolean); + + if (firstSegment === 'namespaces' || !secondSegment) { navigate(`/${value}/compute-graphs`); return; } - if (pathSegments.length >= 2) { - const currentRoute = pathSegments[1]; - const remainingPath = pathSegments.slice(2).join('/'); - const newPath = `/${value}/${currentRoute}${remainingPath ? '/' + remainingPath : ''}`; - navigate(newPath); - } else { - navigate(`/${value}/compute-graphs`); - } + const remainingPath = rest.length ? `/${rest.join('/')}` : ''; + navigate(`/${value}/${secondSegment}${remainingPath}`); }; - const currentNamespace = namespace || 'default'; - return ( Select your namespace ); -}; +} export default NamespaceSelector; diff --git a/server/ui/src/components/inputs/TruncatedDescription.tsx b/server/ui/src/components/TruncatedDescription.tsx similarity index 100% rename from server/ui/src/components/inputs/TruncatedDescription.tsx rename to server/ui/src/components/TruncatedDescription.tsx diff --git a/server/ui/src/components/TruncatedText.tsx b/server/ui/src/components/TruncatedText.tsx index 093dc2929..20451b50d 100644 --- a/server/ui/src/components/TruncatedText.tsx +++ b/server/ui/src/components/TruncatedText.tsx @@ -1,13 +1,17 @@ import { Tooltip, Typography } from '@mui/material'; -const TruncatedText = ({ text, maxLength = 25 }: { text: string, maxLength: number}) => { +interface TruncatedTextProps { + text: string; + maxLength?: number; +} +export function TruncatedText({ text, maxLength = 25 }: TruncatedTextProps) { const truncatedText = text.length > maxLength ? `${text.slice(0, maxLength)}...` : text; return ( - + ); -}; +} -export default TruncatedText; \ No newline at end of file +export default TruncatedText; diff --git a/server/ui/src/components/VersionDisplay.tsx b/server/ui/src/components/VersionDisplay.tsx index 4a174daee..4d99a1e8e 100644 --- a/server/ui/src/components/VersionDisplay.tsx +++ b/server/ui/src/components/VersionDisplay.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Alert, Typography, IconButton, Box } from '@mui/material'; import InfoIcon from '@mui/icons-material/Info'; import WarningIcon from '@mui/icons-material/Warning'; @@ -40,118 +40,112 @@ interface VersionDisplayProps { drawerWidth: number; } -const VersionDisplay: React.FC = ({ owner, repo, variant = 'sidebar', serviceUrl, drawerWidth }) => { - const [openApiVersion, setOpenApiVersion] = useState(null); - const [githubVersion, setGithubVersion] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - const [dismissed, setDismissed] = useState(false); +interface VersionState { + openApi: string | null; + github: string | null; + error: string | null; + isLoading: boolean; + isDismissed: boolean; +} - useEffect(() => { - const fetchOpenApiVersion = async () => { - try { - const response = await fetch(`${serviceUrl}/docs/openapi.json`); - if (!response.ok) { - throw new Error('Failed to fetch OpenAPI version'); - } - const data = await response.json(); - setOpenApiVersion(data.info.version); - } catch (error) { - console.error('Error fetching OpenAPI version:', error); - setError('Failed to fetch OpenAPI version'); - } - }; +export function VersionDisplay({ owner, repo, variant = 'sidebar', serviceUrl, drawerWidth }: VersionDisplayProps) { + const [state, setState] = useState({ + openApi: null, + github: null, + error: null, + isLoading: true, + isDismissed: false, + }); - const fetchGithubVersion = async () => { + useEffect(() => { + async function fetchVersions() { try { - const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`); - if (!response.ok) { - throw new Error('Failed to fetch GitHub version'); - } - const data = await response.json(); - setGithubVersion(data.tag_name); + const [openApiResponse, githubResponse] = await Promise.all([ + fetch(`${serviceUrl}/docs/openapi.json`), + fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`) + ]); + + if (!openApiResponse.ok || !githubResponse.ok) + throw new Error('Failed to fetch versions'); + + const [openApiData, githubData] = await Promise.all([ + openApiResponse.json(), + githubResponse.json() + ]); + + setState(prev => ({ + ...prev, + openApi: openApiData.info.version, + github: githubData.tag_name, + isLoading: false + })); } catch (error) { - console.error('Error fetching GitHub version:', error); - setError('Failed to fetch GitHub version'); + setState(prev => ({ + ...prev, + error: 'Failed to fetch version information', + isLoading: false + })); } - }; - - Promise.all([fetchOpenApiVersion(), fetchGithubVersion()]) - .then(() => setLoading(false)) - .catch(() => setLoading(false)); + } - // Check if the announcement has been dismissed const dismissedData = localStorage.getItem('versionAnnouncementDismissed'); if (dismissedData) { const { timestamp, dismissed } = JSON.parse(dismissedData); const today = new Date().toDateString(); if (new Date(timestamp).toDateString() === today && dismissed) { - setDismissed(true); + setState(prev => ({ ...prev, isDismissed: true })); } else { localStorage.removeItem('versionAnnouncementDismissed'); } } + + void fetchVersions(); }, [owner, repo, serviceUrl]); - const compareVersions = (v1: string, v2: string): number => { - const v1Parts = v1.replace('v', '').split('.').map(Number); - const v2Parts = v2.replace('v', '').split('.').map(Number); + function compareVersions(v1: string, v2: string): number { + const normalize = (v: string) => v.replace('v', '').split('.').map(Number); + const [parts1, parts2] = [normalize(v1), normalize(v2)]; - for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { - const part1 = v1Parts[i] || 0; - const part2 = v2Parts[i] || 0; - if (part1 > part2) return 1; - if (part1 < part2) return -1; + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const diff = (parts1[i] || 0) - (parts2[i] || 0); + if (diff !== 0) return diff > 0 ? 1 : -1; } return 0; - }; + } - const getAlertSeverity = (): 'info' | 'warning' | 'error' => { - if (!openApiVersion || !githubVersion) return 'info'; - const comparison = compareVersions(githubVersion, openApiVersion); - if (comparison === 0) return 'info'; - if (comparison === 1) return 'warning'; - return 'error'; - }; + function getAlertSeverity(): 'info' | 'warning' | 'error' { + if (!state.openApi || !state.github) return 'info'; + const comparison = compareVersions(state.github, state.openApi); + return comparison === 0 ? 'info' : comparison === 1 ? 'warning' : 'error'; + } - const getAlertIcon = (severity: 'info' | 'warning' | 'error') => { - switch (severity) { - case 'warning': - return ; - case 'error': - return ; - default: - return ; - } + const alertIcons = { + warning: , + error: , + info: }; - const handleDismiss = () => { - setDismissed(true); - const dismissData = { + function handleDismiss() { + setState(prev => ({ ...prev, isDismissed: true })); + localStorage.setItem('versionAnnouncementDismissed', JSON.stringify({ timestamp: new Date().toISOString(), dismissed: true - }; - localStorage.setItem('versionAnnouncementDismissed', JSON.stringify(dismissData)); - }; - - if (loading) { - return Loading version...; + })); } - if (error) { - return Error: {error}; - } + if (state.isLoading) return Loading version...; + if (state.error) return Error: {state.error}; const severity = getAlertSeverity(); if (variant === 'announcement') { - if (severity === 'info' || dismissed) return null; + if (severity === 'info' || state.isDismissed) return null; return ( = ({ owner, repo, variant = } - sx={{ - ...alertStyle, - width: '100%', - }} + sx={{ ...alertStyle, width: '100%' }} > - A new version of Indexify ({githubVersion}) is available. Your current version: v{openApiVersion} + A new version of Indexify ({state.github}) is available. Your current version: v{state.openApi} ); } - // Sidebar version return ( } sx={alertStyle}> - Indexify Version: {openApiVersion} + Indexify Version: {state.openApi} ); -}; +} export default VersionDisplay; diff --git a/server/ui/src/components/cards/ComputeGraphsCard.tsx b/server/ui/src/components/cards/ComputeGraphsCard.tsx index 6ef068163..9b2957a95 100644 --- a/server/ui/src/components/cards/ComputeGraphsCard.tsx +++ b/server/ui/src/components/cards/ComputeGraphsCard.tsx @@ -1,116 +1,89 @@ -import { useState, useEffect } from 'react'; -import { IndexifyClient } from 'getindexify' +import { useState } from 'react' import { Alert, Card, CardContent, Grid, IconButton, Typography } from '@mui/material' import { Box, Stack } from '@mui/system' -import CopyText from '../CopyText' import { Cpu, InfoCircle } from 'iconsax-react' +import DeleteIcon from '@mui/icons-material/Delete' import { Link } from 'react-router-dom' -import DeleteIcon from '@mui/icons-material/Delete'; +import { IndexifyClient } from 'getindexify' +import { ComputeGraph, ComputeGraphsList } from '../../types' +import CopyText from '../CopyText' import TruncatedText from '../TruncatedText' -import { ComputeGraph, ComputeGraphsList } from '../../types'; -const ComputeGraphsCard = ({ - client, - computeGraphs, - namespace -}: { +interface ComputeGraphsCardProps { client: IndexifyClient computeGraphs: ComputeGraphsList namespace: string -}) => { - const [localComputeGraphs, setLocalComputeGraphs] = useState(computeGraphs); - const [error, setError] = useState(null); +} - useEffect(() => { - setLocalComputeGraphs(computeGraphs); - }, [computeGraphs, namespace]); +export function ComputeGraphsCard({ client, computeGraphs, namespace }: ComputeGraphsCardProps) { + const [localGraphs, setLocalGraphs] = useState(computeGraphs.compute_graphs || []) + const [error, setError] = useState(null) - const handleDeleteComputeGraph = async (computeGraphName: string) => { + async function handleDeleteGraph(graphName: string) { try { - await client.deleteComputeGraph(computeGraphName); - setLocalComputeGraphs(prevGraphs => ({ - compute_graphs: prevGraphs.compute_graphs?.filter(graph => graph.name !== computeGraphName) || [] - })); - } catch (error) { - console.error("Error deleting compute graph:", error); - setError('Failed to delete compute graph. Please try again.'); + await client.deleteComputeGraph(graphName) + setLocalGraphs(prevGraphs => prevGraphs.filter(graph => graph.name !== graphName)) + } catch (err) { + console.error('Error deleting compute graph:', err) + setError('Failed to delete compute graph. Please try again.') } - }; + } - const renderContent = () => { - if (error) { - return ( - - - {error} - - - ); - } + function renderGraphCard(graph: ComputeGraph) { + return ( + + + + + + + + + + handleDeleteGraph(graph.name)} + aria-label="delete compute graph" + > + + + + + + Version: {graph.version || 'N/A'} + + + Namespace: {graph.namespace} + + + Number of Nodes: {Object.keys(graph.nodes || {}).length} + + + + + ) + } - if (!localComputeGraphs.compute_graphs || localComputeGraphs.compute_graphs.length === 0) { - return ( - - - No Graphs Found - - - ); - } + function renderContent() { + if (error) + return {error} - const sortedGraphs = [...localComputeGraphs.compute_graphs].sort((a, b) => a.name.localeCompare(b.name)); + if (!localGraphs.length) + return No Graphs Found + + const sortedGraphs = [...localGraphs].sort((a, b) => a.name.localeCompare(b.name)) return ( - + - {sortedGraphs.map((graph: ComputeGraph) => ( - - - -
- - - - - - handleDeleteComputeGraph(graph.name)} aria-label="delete compute graph"> - - - -
- - Version: {graph.version} - - - Namespace: {graph.namespace} - - - Number of Nodes: {Object.keys(graph.nodes || {}).length} - -
-
-
- ))} + {sortedGraphs.map(renderGraphCard)}
- ); - }; + ) + } return ( <> - +
@@ -126,7 +99,5 @@ const ComputeGraphsCard = ({
{renderContent()} - ); -}; - -export default ComputeGraphsCard; + ) +} diff --git a/server/ui/src/components/cards/ExecutorsCard.tsx b/server/ui/src/components/cards/ExecutorsCard.tsx index 565bb057f..ab512000a 100644 --- a/server/ui/src/components/cards/ExecutorsCard.tsx +++ b/server/ui/src/components/cards/ExecutorsCard.tsx @@ -1,85 +1,98 @@ -import React from 'react'; -import { Alert, IconButton, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip } from '@mui/material'; -import { Box, Stack } from '@mui/system'; -import { Setting4, InfoCircle } from 'iconsax-react'; -import { ExecutorMetadata } from '../../types'; +import { Alert, IconButton, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip } from '@mui/material' +import { Box, Stack } from '@mui/system' +import { Setting4, InfoCircle } from 'iconsax-react' -const ExecutorsCard = ({ executors }: { executors: ExecutorMetadata[] }) => { - const renderContent = () => { - if (!executors || executors.length === 0) { - return ( - - - No Executors Found - - - ); - } +interface ExecutorMetadata { + image_name: string + id: string + executor_version: string + addr: string + labels: Record +} + +interface ExecutorsCardProps { + executors: ExecutorMetadata[] +} + +function ExecutorsContent({ executors }: ExecutorsCardProps) { + if (!executors?.length) return ( - - - - - Image Name - ID - Version - Address - Labels + + + No Executors Found + + + ) + + return ( + +
+ + + Image Name + ID + Version + Address + Labels + + + + {executors.map((executor) => ( + + + + + {executor.image_name} + + + {executor.id} + {executor.executor_version} + {executor.addr} + + + {Object.entries(executor.labels).map(([key, value]) => ( + + ))} + + - - - {executors.map((executor) => ( - - - - - {executor.image_name} - - - {executor.id} - {executor.executor_version} - {executor.addr} - - - {Object.entries(executor.labels).map(([key, value]) => ( - - ))} - - - - ))} - -
-
- ); - }; + ))} + + + + ) +} +export function ExecutorsCard({ executors }: ExecutorsCardProps) { return ( <>
- +
Executors - - + +
- {renderContent()} + - ); -}; - -export default ExecutorsCard; + ) +} diff --git a/server/ui/src/components/cards/NamespacesCard.tsx b/server/ui/src/components/cards/NamespacesCard.tsx index 4af3ccbfe..32dcfbdc5 100644 --- a/server/ui/src/components/cards/NamespacesCard.tsx +++ b/server/ui/src/components/cards/NamespacesCard.tsx @@ -4,75 +4,94 @@ import { TableDocument, InfoCircle } from 'iconsax-react'; import { formatTimestamp } from '../../utils/helpers'; import { Namespace } from '../../types'; -const NamespacesCard = ({ namespaces }: { namespaces: Namespace[] }) => { - const renderContent = () => { - if (namespaces.length === 0) { - return ( - - - No Namespaces Found - +interface NamespacesCardProps { + namespaces: Namespace[]; +} + +function TableDocumentIcon({ size }: { size: string }) { + return ( +
+ +
+ ); +} + +function NamespaceItem({ namespace }: { namespace: Namespace }) { + return ( + + + + + + + + + {namespace.name} + + + + + + Created At:   + + {formatTimestamp(namespace.created_at)} - ); - } + + + ); +} + +function NamespacesCard({ namespaces }: NamespacesCardProps) { + if (namespaces.length === 0) return ( - - - {namespaces.map((namespace) => ( - - - - -
- -
-
- - - {namespace.name} - - -
- - - Created At:   - - {formatTimestamp(namespace.created_at)} - -
-
- ))} -
+ + + No Namespaces Found + ); - }; return ( <> - -
- -
+ + Namespaces - + - {renderContent()} + + + {namespaces.map((namespace) => ( + + ))} + + ); -}; +} export default NamespacesCard; diff --git a/server/ui/src/components/inputs/DropZone.tsx b/server/ui/src/components/inputs/DropZone.tsx deleted file mode 100644 index 139cc3fa9..000000000 --- a/server/ui/src/components/inputs/DropZone.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { - Box, - List, - ListItem, - ListItemText, - Paper, - Button, - Typography, - Divider -} from '@mui/material'; -import UploadFileIcon from '@mui/icons-material/UploadFile'; -import RemoveCircleIcon from '@mui/icons-material/RemoveCircle'; -import { styled } from '@mui/system'; -import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined'; - -interface FileItem { - name: string; - size: number; -} - -interface FileDropZoneProps { - onFileSelect: (files: File[]) => void; -} - -const FileInput = styled('input')({ - display: 'none', -}); - -function truncateFilename(filename: string) { - if (filename.length > 25) { - return filename.substring(0, 15) + '...'; - } - return filename; -} - -const FileDropZone: React.FC = ({ onFileSelect }) => { - const [files, setFiles] = useState([]); - - const handleDrop = (event: React.DragEvent) => { - event.preventDefault(); - const newFiles = Array.from(event.dataTransfer.files).map(file => ({ - name: file.name, - size: file.size - })); - setFiles([...files, ...newFiles]); - onFileSelect(Array.from(event.dataTransfer.files)); - }; - - const handleRemove = useCallback((index: number) => { - setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); - }, []); - - const handleChange = useCallback((e: React.ChangeEvent) => { - e.preventDefault(); - if (e.target.files) { - const newFiles = Array.from(e.target.files).map(file => ({ - name: file.name, - size: file.size - })); - setFiles((prevFiles) => [...prevFiles, ...newFiles]); - onFileSelect(Array.from(e.target.files)); - } - }, [onFileSelect]); - - return ( - event.preventDefault()} - > - - {files.length === 0 && ( - <> - - - - Drag and drop files here, or choose files - - - )} - - {files.map((file, index) => ( - - -
-
- -
- - {truncateFilename(file.name)} | {(file.size / 1024).toFixed(2)} kb - -
- -
- -
- ))} -
-
-
- ); -}; - -export default FileDropZone; diff --git a/server/ui/src/components/inputs/LabelsInput.tsx b/server/ui/src/components/inputs/LabelsInput.tsx deleted file mode 100644 index f1b079db5..000000000 --- a/server/ui/src/components/inputs/LabelsInput.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { Box, IconButton, Typography, OutlinedInput } from "@mui/material"; -import AddCircleIcon from "@mui/icons-material/AddCircle"; -import RemoveCircleIcon from "@mui/icons-material/RemoveCircle"; -import { useState } from "react"; - -interface LabelsInputProps { - onChange: (labels: Record) => void; - disabled: boolean; -} - -const LabelsInput = ({ onChange, disabled }: LabelsInputProps) => { - const [labels, setLabels] = useState>({}); - const [newKey, setNewKey] = useState(""); - const [newValue, setNewValue] = useState(""); - const entries = Object.entries(labels || {}); - const lastKey = entries.length > 0 ? entries[entries.length - 1][0] : null; - - const handleAddLabel = () => { - if (newKey && newValue) { - const updatedLabels = { ...labels, [newKey]: newValue }; - setLabels(updatedLabels); - onChange(updatedLabels); - setNewKey(""); - setNewValue(""); - } - }; - - const handleDeleteLabel = (key: string) => { - const { [key]: _, ...remainingLabels } = labels; - setLabels(remainingLabels); - onChange(remainingLabels); - }; - - const handleChange = ( - setValue: React.Dispatch> - ) => (e: React.ChangeEvent) => { - const regex = /^[a-zA-Z0-9-_]*$/; - if (regex.test(e.target.value)) { - setValue(e.target.value); - } - }; - - return ( - - - Labels - - - 0 && { paddingBottom: "0px" } - }} - > - - - - - - - {Object.entries(labels).map(([key, value]) => - - - - handleDeleteLabel(key)} - > - - - - )} - - - ); -}; - -export default LabelsInput; diff --git a/server/ui/src/components/inputs/ScrollableChips.tsx b/server/ui/src/components/inputs/ScrollableChips.tsx deleted file mode 100644 index e002fd684..000000000 --- a/server/ui/src/components/inputs/ScrollableChips.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useRef, useState, useEffect } from 'react'; -import { Box, Stack, Chip, IconButton } from '@mui/material'; -import ArrowCircleLeftRoundedIcon from '@mui/icons-material/ArrowCircleLeftRounded'; -import ArrowCircleRightRoundedIcon from '@mui/icons-material/ArrowCircleRightRounded'; - -export const ScrollableChips = ({ inputParams }: { inputParams: any }) => { - const scrollContainerRef = useRef(null); - const [showLeftArrow, setShowLeftArrow] = useState(false); - const [showRightArrow, setShowRightArrow] = useState(false); - - const checkScroll = () => { - if (scrollContainerRef.current) { - const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current; - setShowLeftArrow(scrollLeft > 0); - setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 1); - } - }; - - useEffect(() => { - checkScroll(); - window.addEventListener('resize', checkScroll); - return () => window.removeEventListener('resize', checkScroll); - }, []); - - const scroll = (scrollOffset: number) => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollBy({ left: scrollOffset, behavior: 'smooth' }); - setTimeout(checkScroll, 100); - } - }; - - if (!inputParams || Object.keys(inputParams).length === 0) { - return ; - } - - return ( - - {showLeftArrow && ( - scroll(-100)} - sx={{ - position: 'absolute', - left: -20, - top: '50%', - transform: 'translateY(-50%)', - zIndex: 1, - '&:hover': { backgroundColor: '#3296fe4d' }, - }} - > - - - )} - - - {Object.keys(inputParams).map((val: string) => ( - - ))} - - - {showRightArrow && ( - scroll(100)} - sx={{ - position: 'absolute', - right: -20, - top: '50%', - transform: 'translateY(-50%)', - zIndex: 1, - '&:hover': { backgroundColor: '#3296fe4d' }, - }} - > - - - )} - - ); -}; diff --git a/server/ui/src/components/tables/ComputeGraphTable.tsx b/server/ui/src/components/tables/ComputeGraphTable.tsx index f39d1e338..27544e4b1 100644 --- a/server/ui/src/components/tables/ComputeGraphTable.tsx +++ b/server/ui/src/components/tables/ComputeGraphTable.tsx @@ -1,5 +1,14 @@ -import React from 'react'; -import { TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, Box, Chip } from '@mui/material'; +import { + TableContainer, + Paper, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + Box, + Chip +} from '@mui/material'; import CopyText from '../CopyText'; import { ComputeGraph } from '../../types'; @@ -16,85 +25,87 @@ interface RowData { dependencies: string[]; } -const ComputeGraphTable: React.FC = ({ graphData, namespace }) => { - const rows: RowData[] = Object.entries(graphData.nodes).map(([nodeName, node]) => { - if ('compute_fn' in node) { - return { - name: nodeName, - type: 'compute_fn', - fn_name: node.compute_fn.fn_name, - description: node.compute_fn.description, - dependencies: graphData.edges[nodeName] || [], - }; - } else { - return { - name: nodeName, - type: 'dynamic_router', - fn_name: node.dynamic_router.source_fn, - description: node.dynamic_router.description, - dependencies: node.dynamic_router.target_fns, - }; - } - }); +const TYPE_COLORS = { + compute_fn: 'primary', + dynamic_router: 'secondary', +} satisfies Record; - const getChipColor = (type: string) => { - switch (type) { - case 'compute_fn': - return 'primary'; - case 'dynamic_router': - return 'secondary'; - default: - return 'default'; - } - }; +const CELL_STYLES = { fontSize: 14, pt: 1 } as const; +const CHIP_STYLES = { + height: '16px', + fontSize: '0.625rem', + '& .MuiChip-label': { padding: '0 6px' }, +} as const; + +const TABLE_CONTAINER_STYLES = { + borderRadius: '8px', + mt: 2, + boxShadow: "0px 0px 2px 0px rgba(51, 132, 252, 0.5) inset" +} as const; + +const TABLE_HEADERS = ['Node Name', 'Out Edges', 'Description'] as const; + +function ComputeGraphTable({ graphData, namespace }: ComputeGraphTableProps) { + const rows = Object.entries(graphData.nodes).map(([nodeName, node]) => ({ + name: nodeName, + type: 'compute_fn' in node ? 'compute_fn' : 'dynamic_router', + fn_name: 'compute_fn' in node ? node.compute_fn.fn_name : node.dynamic_router.source_fn, + description: 'compute_fn' in node ? node.compute_fn.description : node.dynamic_router.description, + dependencies: 'compute_fn' in node + ? graphData.edges[nodeName] || [] + : node.dynamic_router.target_fns, + })); return ( - + - - - Node Name - Out Edges - Description + + + {TABLE_HEADERS.map(header => ( + + {header} + + ))} - {rows.sort((a, b) => a.name.localeCompare(b.name)).map((row) => ( - - - - - {row.name} - - - + {rows + .sort((a, b) => a.name.localeCompare(b.name)) + .map((row) => ( + + + + + {row.name} + + - - - - {row.dependencies.map((dep, index) => ( - - ))} - - {row.description} - - ))} + + + {row.dependencies.map((dep) => ( + + ))} + + + {row.description} + + + ))}
); -}; +} export default ComputeGraphTable; diff --git a/server/ui/src/components/tables/InvocationOutputTable.tsx b/server/ui/src/components/tables/InvocationOutputTable.tsx index 6d8edd8e5..f9683efa1 100644 --- a/server/ui/src/components/tables/InvocationOutputTable.tsx +++ b/server/ui/src/components/tables/InvocationOutputTable.tsx @@ -36,54 +36,69 @@ interface InvocationOutputTableProps { computeGraph: string; } -const InvocationOutputTable: React.FC = ({ indexifyServiceURL, invocationId, namespace, computeGraph }) => { - const [outputs, setOutputs] = useState([]); - const [searchTerms, setSearchTerms] = useState>({}); - const [filteredOutputs, setFilteredOutputs] = useState>({}); - const [expandedPanels, setExpandedPanels] = useState>({}); +interface GroupedOutputs { + [key: string]: Output[]; +} - useEffect(() => { - const fetchOutputs = async () => { - try { - const url = `${indexifyServiceURL}/namespaces/${namespace}/compute_graphs/${computeGraph}/invocations/${invocationId}/outputs`; - const response = await axios.get<{ outputs: Output[] }>(url, { - headers: { - 'accept': 'application/json' - } - }); - setOutputs(response.data.outputs); - } catch (error) { - console.error('Error fetching outputs:', error); - toast.error('Failed to fetch outputs. Please try again later.'); - } - }; +interface ExpandedPanels { + [key: string]: boolean; +} - fetchOutputs(); +interface SearchTerms { + [key: string]: string; +} + +function InvocationOutputTable({ indexifyServiceURL, invocationId, namespace, computeGraph }: InvocationOutputTableProps) { + const [outputs, setOutputs] = useState([]); + const [searchTerms, setSearchTerms] = useState({}); + const [filteredOutputs, setFilteredOutputs] = useState({}); + const [expandedPanels, setExpandedPanels] = useState({}); + + const fetchOutputs = useCallback(async () => { + try { + const url = `${indexifyServiceURL}/namespaces/${namespace}/compute_graphs/${computeGraph}/invocations/${invocationId}/outputs`; + const response = await axios.get<{ outputs: Output[] }>(url, { + headers: { accept: 'application/json' } + }); + setOutputs(response.data.outputs); + } catch (error) { + console.error('Error fetching outputs:', error); + toast.error('Failed to fetch outputs. Please try again later.'); + } }, [indexifyServiceURL, invocationId, namespace, computeGraph]); const handleSearch = useCallback((computeFn: string, term: string) => { const filtered = outputs.filter(output => - output.compute_fn === computeFn && output.id.toLowerCase().includes(term.toLowerCase()) + output.compute_fn === computeFn && + output.id.toLowerCase().includes(term.toLowerCase()) ); setFilteredOutputs(prev => ({ ...prev, [computeFn]: filtered })); }, [outputs]); + const handleAccordionChange = useCallback((panel: string) => + (event: React.SyntheticEvent, isExpanded: boolean) => { + const target = event.target as HTMLElement; + if (target.classList.contains('MuiAccordionSummary-expandIconWrapper') || + target.closest('.MuiAccordionSummary-expandIconWrapper')) { + setExpandedPanels(prev => ({ ...prev, [panel]: isExpanded })); + } + }, []); + + useEffect(() => { + fetchOutputs(); + }, [fetchOutputs]); + useEffect(() => { const grouped = outputs.reduce((acc, output) => { - if (!acc[output.compute_fn]) { - acc[output.compute_fn] = []; - } + if (!acc[output.compute_fn]) acc[output.compute_fn] = []; acc[output.compute_fn].push(output); return acc; - }, {} as Record); + }, {} as GroupedOutputs); setFilteredOutputs(grouped); - - const initialExpandedState = Object.keys(grouped).reduce((acc, key) => { - acc[key] = true; - return acc; - }, {} as Record); - setExpandedPanels(initialExpandedState); + setExpandedPanels( + Object.keys(grouped).reduce((acc, key) => ({ ...acc, [key]: true }), {}) + ); }, [outputs]); useEffect(() => { @@ -92,20 +107,10 @@ const InvocationOutputTable: React.FC = ({ indexifyS }); }, [searchTerms, handleSearch]); - const handleAccordionChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { - const target = event.target as HTMLElement; - if (target.classList.contains('MuiAccordionSummary-expandIconWrapper') || - target.closest('.MuiAccordionSummary-expandIconWrapper')) { - setExpandedPanels(prev => ({ ...prev, [panel]: isExpanded })); - } - }; - - if (!outputs || outputs.length === 0) { + if (!outputs.length) { return ( - - No Outputs Found - + No Outputs Found ); @@ -116,7 +121,7 @@ const InvocationOutputTable: React.FC = ({ indexifyS Outputs for Invocation {Object.entries(filteredOutputs).map(([computeFn, outputs], index) => ( @@ -148,7 +153,7 @@ const InvocationOutputTable: React.FC = ({ indexifyS
- + @@ -174,6 +179,6 @@ const InvocationOutputTable: React.FC = ({ indexifyS ); -}; +} export default InvocationOutputTable; diff --git a/server/ui/src/components/tables/InvocationTasksTable.tsx b/server/ui/src/components/tables/InvocationTasksTable.tsx index 6e2ac51e6..831ffb21d 100644 --- a/server/ui/src/components/tables/InvocationTasksTable.tsx +++ b/server/ui/src/components/tables/InvocationTasksTable.tsx @@ -41,70 +41,65 @@ interface InvocationTasksTableProps { computeGraph: string; } -const InvocationTasksTable: React.FC = ({ indexifyServiceURL, invocationId, namespace, computeGraph }) => { +const OUTCOME_STYLES = { + success: { + color: '#008000b8', + backgroundColor: 'rgb(0 200 0 / 19%)', + }, + failed: { + color: 'red', + backgroundColor: 'rgba(255, 0, 0, 0.1)', + }, + default: { + color: 'blue', + backgroundColor: 'rgba(0, 0, 255, 0.1)', + }, +} as const; + +export function InvocationTasksTable({ indexifyServiceURL, invocationId, namespace, computeGraph }: InvocationTasksTableProps) { const [tasks, setTasks] = useState([]); const [searchTerms, setSearchTerms] = useState>({}); const [filteredTasks, setFilteredTasks] = useState>({}); const [expandedPanels, setExpandedPanels] = useState>({}); - useEffect(() => { - const fetchTasks = async () => { - try { - const url = `${indexifyServiceURL}/namespaces/${namespace}/compute_graphs/${computeGraph}/invocations/${invocationId}/tasks`; - const response = await axios.get<{ tasks: Task[] }>(url, { - headers: { - 'accept': 'application/json' - } - }); - setTasks(response.data.tasks); - } catch (error) { - console.error('Error fetching tasks:', error); - toast.error('Failed to fetch tasks. Please try again later.'); - } - }; + const fetchTasks = async () => { + try { + const url = `${indexifyServiceURL}/namespaces/${namespace}/compute_graphs/${computeGraph}/invocations/${invocationId}/tasks`; + const response = await axios.get<{ tasks: Task[] }>(url, { + headers: { accept: 'application/json' } + }); + setTasks(response.data.tasks); + } catch (error) { + console.error('Error fetching tasks:', error); + toast.error('Failed to fetch tasks. Please try again later.'); + } + }; - fetchTasks(); - }, [indexifyServiceURL, invocationId, namespace, computeGraph]); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { fetchTasks(); }, [indexifyServiceURL, invocationId, namespace, computeGraph]); useEffect(() => { - const grouped = tasks.reduce((acc, task) => { - if (!acc[task.compute_fn]) { - acc[task.compute_fn] = []; - } - acc[task.compute_fn].push(task); - return acc; - }, {} as Record); + const grouped = tasks.reduce((acc, task) => ({ + ...acc, + [task.compute_fn]: [...(acc[task.compute_fn] || []), task], + }), {} as Record); setFilteredTasks(grouped); - - const initialExpandedState = Object.keys(grouped).reduce((acc, key) => { - acc[key] = true; - return acc; - }, {} as Record); - setExpandedPanels(initialExpandedState); + setExpandedPanels(Object.keys(grouped).reduce((acc, key) => ({ ...acc, [key]: true }), {})); }, [tasks]); useEffect(() => { - const filtered = Object.keys(searchTerms).reduce((acc, computeFn) => { - const term = searchTerms[computeFn].toLowerCase(); - acc[computeFn] = tasks.filter(task => - task.compute_fn === computeFn && - task.id.toLowerCase().includes(term) - ); + const filtered = tasks.reduce((acc, task) => { + const searchTerm = searchTerms[task.compute_fn]?.toLowerCase(); + if (!searchTerm || task.id.toLowerCase().includes(searchTerm)) { + return { + ...acc, + [task.compute_fn]: [...(acc[task.compute_fn] || []), task], + }; + } return acc; }, {} as Record); - tasks.forEach(task => { - if (!searchTerms[task.compute_fn]) { - if (!filtered[task.compute_fn]) { - filtered[task.compute_fn] = []; - } - if (!filtered[task.compute_fn].find(t => t.id === task.id)) { - filtered[task.compute_fn].push(task); - } - } - }); - setFilteredTasks(filtered); }, [tasks, searchTerms]); @@ -119,85 +114,52 @@ const InvocationTasksTable: React.FC = ({ indexifySer const viewLogs = async (task: Task, logType: 'stdout' | 'stderr') => { try { const url = `${indexifyServiceURL}/namespaces/${namespace}/compute_graphs/${computeGraph}/invocations/${invocationId}/fn/${task.compute_fn}/tasks/${task.id}/logs/${logType}`; - const response = await axios.get(url, { + const { data: logContent } = await axios.get(url, { responseType: 'text', - headers: { - 'accept': 'text/plain' - } + headers: { accept: 'text/plain' } }); - const logContent = response.data; - - if (!logContent || logContent.trim() === '') { + if (!logContent?.trim()) { toast.info(`No ${logType} logs found for task ${task.id}.`); return; } const newWindow = window.open('', '_blank'); - if (newWindow) { - newWindow.document.write(` - - - Task ${task.id} - ${logType} Log - - - ${logContent} - - `); - newWindow.document.close(); - } else { + if (!newWindow) { toast.error('Unable to open new window. Please check your browser settings.'); + return; } + + newWindow.document.write(` + + + Task ${task.id} - ${logType} Log + + + ${logContent} + + `); + newWindow.document.close(); } catch (error) { - if (axios.isAxiosError(error) && error.response) { - if (error.response.status === 404) { - toast.info(`No ${logType} logs found for task ${task.id}.`); - } else { - toast.error(`Failed to fetch ${logType} logs for task ${task.id}. Please try again later.`); - } + if (axios.isAxiosError(error) && error.response?.status === 404) { + toast.info(`No ${logType} logs found for task ${task.id}.`); } else { - toast.error(`An unexpected error occurred while fetching ${logType} logs for task ${task.id}.`); + toast.error(`Failed to fetch ${logType} logs for task ${task.id}. Please try again later.`); + console.error(`Error fetching ${logType} logs:`, error); } - console.error(`Error fetching ${logType} logs:`, error); } }; - const getChipStyles = (outcome: string) => { - const baseStyle = { - borderRadius: '16px', - fontWeight: 'bold', - }; - - switch (outcome.toLowerCase()) { - case 'success': - return { - ...baseStyle, - color: '#008000b8', - backgroundColor: 'rgb(0 200 0 / 19%)', - }; - case 'failed': - return { - ...baseStyle, - color: 'red', - backgroundColor: 'rgba(255, 0, 0, 0.1)', - }; - default: - return { - ...baseStyle, - color: 'blue', - backgroundColor: 'rgba(0, 0, 255, 0.1)', - }; - } - }; + const getChipStyles = (outcome: string) => ({ + borderRadius: '16px', + fontWeight: 'bold', + ...(OUTCOME_STYLES[outcome.toLowerCase() as keyof typeof OUTCOME_STYLES] || OUTCOME_STYLES.default), + }); - if (!tasks || tasks.length === 0) { + if (!tasks?.length) { return ( - - No Tasks Found - + No Tasks Found ); @@ -208,7 +170,7 @@ const InvocationTasksTable: React.FC = ({ indexifySer Tasks for Invocation {Object.entries(filteredTasks).map(([computeFn, tasks], index) => ( @@ -283,6 +245,6 @@ const InvocationTasksTable: React.FC = ({ indexifySer ); -}; +} export default InvocationTasksTable; diff --git a/server/ui/src/components/tables/InvocationsTable.tsx b/server/ui/src/components/tables/InvocationsTable.tsx index 15d76ddc2..cbdb877a3 100644 --- a/server/ui/src/components/tables/InvocationsTable.tsx +++ b/server/ui/src/components/tables/InvocationsTable.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Table, TableBody, @@ -25,19 +24,15 @@ interface InvocationsTableProps { onDelete: (updatedList: DataObject[]) => void; } -const InvocationsTable: React.FC = ({ invocationsList, computeGraph, onDelete, namespace }) => { +export function InvocationsTable({ invocationsList, computeGraph, onDelete, namespace }: InvocationsTableProps) { const handleDelete = async (invocationId: string) => { try { - const url = `${getIndexifyServiceURL()}/namespaces/${namespace}/compute_graphs/${computeGraph}/invocations/${invocationId}`; - await axios.delete(url, { - headers: { - 'accept': '*/*' - } - }); + await axios.delete( + `${getIndexifyServiceURL()}/namespaces/${namespace}/compute_graphs/${computeGraph}/invocations/${invocationId}`, + { headers: { accept: '*/*' } } + ); - const updatedList = invocationsList.filter(invocation => invocation.id !== invocationId); - onDelete(updatedList); - console.log(`Invocation ${invocationId} deleted successfully`); + onDelete(invocationsList.filter(invocation => invocation.id !== invocationId)); } catch (error) { console.error(`Error deleting invocation ${invocationId}:`, error); } @@ -46,7 +41,7 @@ const InvocationsTable: React.FC = ({ invocationsList, co return ( Invocations - +
@@ -57,22 +52,20 @@ const InvocationsTable: React.FC = ({ invocationsList, co - {invocationsList.map((invocation) => ( - + {invocationsList.map(({ id, created_at, payload_size }) => ( + - - {invocation.id} + + {id} - + - {formatTimestamp(invocation.created_at)} - {formatBytes(invocation.payload_size)} + {formatTimestamp(created_at)} + {formatBytes(payload_size)} - handleDelete(invocation.id)} color="error" size="small"> + handleDelete(id)} color="error" size="small"> @@ -83,6 +76,6 @@ const InvocationsTable: React.FC = ({ invocationsList, co ); -}; +} export default InvocationsTable; diff --git a/server/ui/src/index.tsx b/server/ui/src/index.tsx index 9afd57c5d..9033d56f3 100644 --- a/server/ui/src/index.tsx +++ b/server/ui/src/index.tsx @@ -30,26 +30,27 @@ import { IndexifyClient } from "getindexify"; import { getIndexifyServiceURL } from "./utils/helpers"; function RedirectToComputeGraphs() { - const { namespace } = useParams(); - const currentNamespace = namespace || 'default'; + const { namespace } = useParams<{ namespace: string }>(); - if (namespace === "namespaces") { - return null; - } else { - return ; - } + if (namespace === "namespaces") return null; + + const currentNamespace = namespace || 'default'; + return ; } async function rootLoader({ params }: LoaderFunctionArgs) { - const response = await IndexifyClient.namespaces({ - serviceUrl: getIndexifyServiceURL(), - }); - - const namespace = params.namespace || 'default'; - return { - namespaces: response, - namespace - }; + try { + const serviceUrl = getIndexifyServiceURL(); + const response = await IndexifyClient.namespaces({ serviceUrl }); + + return { + namespaces: response, + namespace: params.namespace || 'default' + }; + } catch (error) { + console.error('Failed to load namespaces:', error); + throw new Error('Failed to load namespaces. Please try again later.'); + } } const router = createBrowserRouter( @@ -62,7 +63,8 @@ const router = createBrowserRouter( children: [ { path: "/:namespace", - element: + element: , + errorElement: }, { path: "/namespaces", @@ -103,8 +105,9 @@ const router = createBrowserRouter( const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); + root.render( -); \ No newline at end of file +); diff --git a/server/ui/src/routes/Namespace/ComputeGraphsPage.tsx b/server/ui/src/routes/Namespace/ComputeGraphsPage.tsx index c70677b53..9cc0f552e 100644 --- a/server/ui/src/routes/Namespace/ComputeGraphsPage.tsx +++ b/server/ui/src/routes/Namespace/ComputeGraphsPage.tsx @@ -1,19 +1,17 @@ import { Box, Alert } from "@mui/material"; -import { ComputeGraphsList, IndexifyClient } from "getindexify"; import { useLoaderData } from "react-router-dom"; -import ComputeGraphsCard from "../../components/cards/ComputeGraphsCard"; +import { ComputeGraphsCard } from "../../components/cards/ComputeGraphsCard"; +import type { ComputeGraphLoaderData } from "./types"; const ComputeGraphsPage = () => { - const { client, computeGraphs, namespace } = useLoaderData() as { - client: IndexifyClient; - computeGraphs: ComputeGraphsList; - namespace: string; - }; + const { client, computeGraphs, namespace } = useLoaderData() as ComputeGraphLoaderData; if (!client || !computeGraphs || !namespace) { return ( - Failed to load compute graphs data. Please try again. + + Failed to load compute graphs data. Please try again. + ); } diff --git a/server/ui/src/routes/Namespace/ExecutorsPage.tsx b/server/ui/src/routes/Namespace/ExecutorsPage.tsx index a43294bb4..43e658e27 100644 --- a/server/ui/src/routes/Namespace/ExecutorsPage.tsx +++ b/server/ui/src/routes/Namespace/ExecutorsPage.tsx @@ -1,15 +1,15 @@ -import { Box } from '@mui/material' -import { useLoaderData } from 'react-router-dom' -import ExecutorsCard from '../../components/cards/ExecutorsCard' -import { ExecutorMetadata } from '../../types' +import { Box } from '@mui/material'; +import { useLoaderData } from 'react-router-dom'; +import { ExecutorsCard } from '../../components/cards/ExecutorsCard'; +import type { ExecutorsLoaderData } from './types'; const ExecutorsPage = () => { - const { - executors - } = useLoaderData() as { - executors: ExecutorMetadata[] - } - return -} + const { executors } = useLoaderData() as ExecutorsLoaderData; + return ( + + + + ); +}; -export default ExecutorsPage; \ No newline at end of file +export default ExecutorsPage; diff --git a/server/ui/src/routes/Namespace/IndividualComputeGraphPage.tsx b/server/ui/src/routes/Namespace/IndividualComputeGraphPage.tsx index 21e6c856c..2a46d355e 100644 --- a/server/ui/src/routes/Namespace/IndividualComputeGraphPage.tsx +++ b/server/ui/src/routes/Namespace/IndividualComputeGraphPage.tsx @@ -1,34 +1,23 @@ -import { - Box, - Breadcrumbs, - Typography, - Stack, - Chip, -} from '@mui/material'; +import { Box, Breadcrumbs, Typography, Stack, Chip } from '@mui/material'; import { TableDocument } from 'iconsax-react'; -import NavigateNextIcon from '@mui/icons-material/NavigateNext' +import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import { Link, useLoaderData } from 'react-router-dom'; +import { useState, useCallback } from 'react'; +import type { DataObject } from '../../types'; +import type { IndividualComputeGraphLoaderData } from './types'; import ComputeGraphTable from '../../components/tables/ComputeGraphTable'; import CopyText from '../../components/CopyText'; import InvocationsTable from '../../components/tables/InvocationsTable'; -import { useState } from 'react'; -import { ComputeGraph, DataObject } from '../../types'; const IndividualComputeGraphPage = () => { - const { - invocationsList, - computeGraph, - namespace - } = - useLoaderData() as { - invocationsList: DataObject[], - computeGraph: ComputeGraph - namespace: string - } + const { invocationsList, computeGraph, namespace } = + useLoaderData() as IndividualComputeGraphLoaderData; + const [invocations, setInvocations] = useState(invocationsList); - const handleDelete = (updatedList: DataObject[]) => { + + const handleDelete = useCallback((updatedList: DataObject[]) => { setInvocations(updatedList); - }; + }, []); return ( @@ -37,27 +26,55 @@ const IndividualComputeGraphPage = () => { separator={} > {namespace} - + Compute Graphs {computeGraph.name} - + +
- +
- - {computeGraph.name} + + {computeGraph.name} + +
- + +
- + +
); }; -export default IndividualComputeGraphPage; \ No newline at end of file +export default IndividualComputeGraphPage; diff --git a/server/ui/src/routes/Namespace/IndividualInvocationPage.tsx b/server/ui/src/routes/Namespace/IndividualInvocationPage.tsx index 7932043ea..e4ad6d97f 100644 --- a/server/ui/src/routes/Namespace/IndividualInvocationPage.tsx +++ b/server/ui/src/routes/Namespace/IndividualInvocationPage.tsx @@ -1,61 +1,86 @@ -import { - Box, - Breadcrumbs, - Typography, - Stack, -} from '@mui/material'; -import { TableDocument } from 'iconsax-react'; +import { Suspense } from 'react' +import { Breadcrumbs, Typography } from '@mui/material' +import { TableDocument } from 'iconsax-react' import NavigateNextIcon from '@mui/icons-material/NavigateNext' -import { Link, useLoaderData } from 'react-router-dom'; -import CopyText from '../../components/CopyText'; -import InvocationOutputTable from '../../components/tables/InvocationOutputTable'; -import InvocationTasksTable from '../../components/tables/InvocationTasksTable'; - -const IndividualInvocationPage = () => { - const { - indexifyServiceURL, - invocationId, - computeGraph, - namespace - } = - useLoaderData() as { - indexifyServiceURL: string - invocationId: string, - computeGraph: string, - namespace: string - } +import { Link, useLoaderData } from 'react-router-dom' +import CopyText from '../../components/CopyText' +import InvocationTasksTable from '../../components/tables/InvocationTasksTable' +import InvocationOutputTable from '../../components/tables/InvocationOutputTable' + +interface InvocationPageData { + indexifyServiceURL: string + invocationId: string + computeGraph: string + namespace: string +} + +function BreadcrumbTrail({ namespace, computeGraph, invocationId }: Omit) { + return ( + } + > + {namespace} + + Compute Graphs + + + {computeGraph} + + {invocationId} + + ) +} + +function InvocationHeader({ invocationId }: { invocationId: string }) { return ( - - } - > - {namespace} - - Compute Graphs - - - {computeGraph} - - {invocationId} - - - -
-
- -
- - Invocation - {invocationId} - -
- -
- -
-
- ); -}; +
+
+ +
+ + Invocation - {invocationId} + + +
+ ) +} + +function IndividualInvocationPage() { + const { indexifyServiceURL, invocationId, computeGraph, namespace } = useLoaderData() as InvocationPageData + + return ( +
+ + +
+ + + Loading output table...
}> + + + + Loading tasks table...
}> + + + + + ) +} export default IndividualInvocationPage; diff --git a/server/ui/src/routes/Namespace/NamespacesPage.tsx b/server/ui/src/routes/Namespace/NamespacesPage.tsx index 04177c0c4..c6cc4ddc2 100644 --- a/server/ui/src/routes/Namespace/NamespacesPage.tsx +++ b/server/ui/src/routes/Namespace/NamespacesPage.tsx @@ -1,15 +1,15 @@ -import { Box } from '@mui/material' -import { useLoaderData } from 'react-router-dom' -import NamespacesCard from '../../components/cards/NamespacesCard' -import { Namespace } from '../../types' +import { Box } from '@mui/material'; +import { useLoaderData } from 'react-router-dom'; +import NamespacesCard from '../../components/cards/NamespacesCard'; +import type { NamespacesLoaderData } from './types'; const NamespacesPage = () => { - const { - namespaces - } = useLoaderData() as { - namespaces: Namespace[] - } - return -} + const { namespaces } = useLoaderData() as NamespacesLoaderData; + return ( + + + + ); +}; export default NamespacesPage; diff --git a/server/ui/src/routes/Namespace/types.ts b/server/ui/src/routes/Namespace/types.ts new file mode 100644 index 000000000..d19c72ce3 --- /dev/null +++ b/server/ui/src/routes/Namespace/types.ts @@ -0,0 +1,30 @@ +import { IndexifyClient } from 'getindexify'; +import { ComputeGraph, DataObject, Namespace, ExecutorMetadata, ComputeGraphsList } from '../../types'; + +export interface NamespaceLoaderData { + namespace: string; + client?: IndexifyClient; +} + +export interface ComputeGraphLoaderData extends NamespaceLoaderData { + computeGraphs: ComputeGraphsList; +} + +export interface IndividualComputeGraphLoaderData extends NamespaceLoaderData { + invocationsList: DataObject[]; + computeGraph: ComputeGraph; +} + +export interface IndividualInvocationLoaderData extends NamespaceLoaderData { + indexifyServiceURL: string; + invocationId: string; + computeGraph: string; +} + +export interface ExecutorsLoaderData { + executors: ExecutorMetadata[]; +} + +export interface NamespacesLoaderData { + namespaces: Namespace[]; +} diff --git a/server/ui/src/routes/root.tsx b/server/ui/src/routes/root.tsx index f74bec17b..3695f7a5a 100644 --- a/server/ui/src/routes/root.tsx +++ b/server/ui/src/routes/root.tsx @@ -1,56 +1,84 @@ +import { type ReactElement } from 'react'; import { ThemeProvider } from '@mui/material/styles'; -import CssBaseline from '@mui/material/CssBaseline'; -import Box from '@mui/material/Box'; -import Toolbar from '@mui/material/Toolbar'; -import Typography from '@mui/material/Typography'; +import { + Box, + CssBaseline, + Toolbar, + Typography, + Drawer, + List, + ListItemButton, + ListItemText, + Divider +} from '@mui/material'; import { LoaderFunctionArgs, Outlet, redirect, useLoaderData, useLocation, + Link } from 'react-router-dom'; -import theme from '../theme'; -import { Stack } from '@mui/system'; +import { Cpu, TableDocument, Setting4 } from 'iconsax-react'; import { IndexifyClient } from 'getindexify'; import { getIndexifyServiceURL } from '../utils/helpers'; +import theme from '../theme'; + +// Components import Footer from '../components/Footer'; -import { - Divider, - Drawer, - List, - ListItemButton, - ListItemText, -} from '@mui/material'; -import { Link } from 'react-router-dom'; -import { Cpu, TableDocument, Setting4 } from 'iconsax-react'; import VersionDisplay from '../components/VersionDisplay'; import NamespaceSelector from '../components/NamespaceSelector'; -const indexifyServiceURL = getIndexifyServiceURL(); +// Constants +const DRAWER_WIDTH = 240; +const SERVICE_URL = getIndexifyServiceURL(); + +interface LoaderData { + namespace: string; +} + +interface NavItem { + path: string; + icon: ReactElement; + label: string; +} export async function loader({ params }: LoaderFunctionArgs) { const response = await IndexifyClient.namespaces({ - serviceUrl: indexifyServiceURL, + serviceUrl: SERVICE_URL, }); const namespaceNames = response.map((repo) => repo.name); if (!params.namespace || !namespaceNames.includes(params.namespace)) { - return redirect(`/default/compute-graphs`); + return redirect('/default/compute-graphs'); } - return { namespaces: response, namespace: params.namespace || 'default' }; + return { namespace: params.namespace || 'default' }; } -const drawerWidth = 240; - -export default function Dashboard() { - const { namespace } = useLoaderData() as { - namespace: string; - }; +function Dashboard() { + const { namespace } = useLoaderData() as LoaderData; const location = useLocation(); + const navItems: NavItem[] = [ + { + path: `/${namespace}/compute-graphs`, + icon: , + label: 'Compute Graphs' + }, + { + path: '/namespaces', + icon: , + label: 'Namespaces' + }, + { + path: '/executors', + icon: , + label: 'Executors' + } + ]; + return ( - theme.palette.mode === 'light' - ? '#F7F9FC' - : theme.palette.grey[900], + theme.palette.mode === 'light' ? '#F7F9FC' : theme.palette.grey[900], }} > - + + - logo - + Indexify - - + + + - - - - - - - - - - - - + {navItems.map(({ path, icon, label }) => ( + + {icon} + + + ))} + - + - {/* page content */} + ); } + +export default Dashboard; \ No newline at end of file diff --git a/server/ui/src/utils/helpers.ts b/server/ui/src/utils/helpers.ts index 75ca6dd43..ad5b50986 100644 --- a/server/ui/src/utils/helpers.ts +++ b/server/ui/src/utils/helpers.ts @@ -1,68 +1,99 @@ -export const stringToColor = (str: string): string => { - let hash = 5381 - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) + hash) + str.charCodeAt(i) - } - return `#${(hash & 0xFFFFFF).toString(16).padStart(6, '0')}` +const BYTE_SIZES = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] as const +const BYTE_MULTIPLIER = 1000 +const TIMESTAMP_THRESHOLD = 1e12 +const API_KEY_PATTERNS = new Set([ + 'key', + 'api_key', + 'key_api', + 'api', + 'api-key' +].map(pattern => pattern.toLowerCase())) + +interface DateFormatOptions extends Intl.DateTimeFormatOptions { + year: 'numeric' + month: 'short' + day: '2-digit' + hour: '2-digit' + minute: '2-digit' + second: '2-digit' + hour12: true } -export const getIndexifyServiceURL = (): string => { - return process.env.NODE_ENV === 'development' ? 'http://localhost:8900' : window.location.origin +// Generates a consistent color hash from a string +export function stringToColor(input: string): string { + const hash = Array.from(input).reduce( + (acc, char) => ((acc << 5) + acc) + char.charCodeAt(0), + 5381 + ) + return `#${(hash & 0xFFFFFF).toString(16).padStart(6, '0')}` } +// Returns the appropriate service URL based on environment +export function getIndexifyServiceURL(): string { + return process.env.NODE_ENV === 'development' + ? 'http://localhost:8900' + : window.location.origin +} -export const formatBytes = (() => { - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] - const k = 1000 - return (bytes: number, decimals: number = 2): string => { - if (bytes === 0) return '0 Bytes' - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}` - } -})() +// Formats byte sizes into human-readable strings +export function formatBytes(bytes: number, decimals = 2): string { + if (bytes === 0) return '0 Bytes' + + const exponent = Math.floor(Math.log(bytes) / Math.log(BYTE_MULTIPLIER)) + const value = bytes / Math.pow(BYTE_MULTIPLIER, exponent) + + return `${value.toFixed(decimals)} ${BYTE_SIZES[exponent]}` +} -export const splitLabels = (data: { [key: string]: string }): string[] => { +// Converts an object of key-value pairs into an array of formatted strings +export function splitLabels(data: Record): string[] { return Object.entries(data).map(([key, value]) => `${key}: ${value}`) } -export const formatTimestamp = (() => { - const MILLISECONDS_MULTIPLIER = 1e12 - const dateFormatOptions: Intl.DateTimeFormatOptions = { - year: 'numeric', - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: true - } - - return (value: string | number | null | undefined): string => { - if (value == null) return 'N/A' - - const timestamp = typeof value === 'string' ? parseInt(value, 10) : value - - if (typeof timestamp !== 'number' || isNaN(timestamp)) return 'Invalid Date' - - const milliseconds = timestamp < MILLISECONDS_MULTIPLIER ? timestamp * 1000 : timestamp - - return new Date(milliseconds).toLocaleString(undefined, dateFormatOptions) - } -})() +const DATE_FORMAT_OPTIONS: DateFormatOptions = { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true +} -const keyPatterns: Set = new Set(['key', 'api_key', 'key_api', 'api', 'api-key'].map(s => s.toLowerCase())) +// Formats timestamps into localized date strings +export function formatTimestamp(value: string | number | null | undefined): string { + if (value == null) return 'N/A' + + const timestamp = typeof value === 'string' ? parseInt(value, 10) : value + + if (typeof timestamp !== 'number' || isNaN(timestamp)) return 'Invalid Date' + + const milliseconds = timestamp < TIMESTAMP_THRESHOLD + ? timestamp * 1000 + : timestamp + + return new Date(milliseconds).toLocaleString(undefined, DATE_FORMAT_OPTIONS) +} -export function maskApiKeys(inputString: string): string { +// Masks sensitive API keys in JSON strings +export function maskApiKeys(input: string): string { try { - const data: { [key: string]: any } = JSON.parse(inputString) - for (const [key, value] of Object.entries(data)) { - if (keyPatterns.has(key.toLowerCase())) { - data[key] = '*'.repeat(String(value).length) - } - } - return JSON.stringify(data) - } catch (error) { - return inputString.replace(/"(key|api_key|key_api|api|api-key)":\s*"([^"]*)"/gi, - (_, key, value) => `"${key}":"${'*'.repeat(value.length)}"`) + const data = JSON.parse(input) as Record + + return JSON.stringify( + Object.fromEntries( + Object.entries(data).map(([key, value]) => [ + key, + API_KEY_PATTERNS.has(key.toLowerCase()) + ? '*'.repeat(String(value).length) + : value + ]) + ) + ) + } catch { + return input.replace( + /"(key|api_key|key_api|api|api-key)":\s*"([^"]*)"/gi, + (_, key, value) => `"${key}":"${'*'.repeat(value.length)}"` + ) } } diff --git a/server/ui/src/utils/loaders.ts b/server/ui/src/utils/loaders.ts index 8020e57a2..24876f521 100644 --- a/server/ui/src/utils/loaders.ts +++ b/server/ui/src/utils/loaders.ts @@ -1,21 +1,30 @@ import { IndexifyClient } from 'getindexify' import { LoaderFunctionArgs, redirect } from 'react-router-dom' import { getIndexifyServiceURL } from './helpers' -import axios from 'axios'; -import { ComputeGraph, ComputeGraphsList } from '../types'; +import axios from 'axios' +import { ComputeGraph, ComputeGraphsList } from '../types' -const indexifyServiceURL = getIndexifyServiceURL(); +const indexifyServiceURL = getIndexifyServiceURL() export const apiClient = axios.create({ baseURL: indexifyServiceURL, -}); +}) +async function apiGet(url: string): Promise { + try { + const response = await apiClient.get(url) + return response.data + } catch (error) { + console.error(`Error fetching ${url}:`, error) + throw error + } +} function createClient(namespace: string) { return IndexifyClient.createClient({ serviceUrl: indexifyServiceURL, namespace: namespace || 'default', - }); + }) } export async function ContentsPageLoader({ params }: LoaderFunctionArgs) { @@ -25,23 +34,14 @@ export async function ContentsPageLoader({ params }: LoaderFunctionArgs) { } export async function ComputeGraphsPageLoader({ params }: LoaderFunctionArgs) { - const namespace = params.namespace || 'default'; - const client = createClient(namespace); + const namespace = params.namespace || 'default' + const client = createClient(namespace) try { - const computeGraphs = await apiClient.get(`/namespaces/${namespace}/compute_graphs`); - return { - client, - computeGraphs: computeGraphs.data, - namespace, - } - } catch (error) { - console.error("Error fetching compute graphs:", error) - return { - client, - computeGraphs: { compute_graphs: [] }, - namespace, - } + const computeGraphs = await apiGet(`/namespaces/${namespace}/compute_graphs`) + return { client, computeGraphs, namespace } + } catch { + return { client, computeGraphs: { compute_graphs: [] }, namespace } } } @@ -50,25 +50,27 @@ export async function IndividualComputeGraphPageLoader({ params }: LoaderFunctio if (!namespace) return redirect('/') try { - const [computeGraphsResponse, invocationsResponse] = await Promise.all([ - apiClient.get(`/namespaces/${params.namespace}/compute_graphs`), - apiClient.get(`/namespaces/${params.namespace}/compute_graphs/${computeGraph}/invocations`) - ]); - - const localComputeGraph = computeGraphsResponse.data.compute_graphs.find((graph: ComputeGraph) => graph.name === computeGraph); + const [computeGraphs, invocations] = await Promise.all([ + apiGet(`/namespaces/${namespace}/compute_graphs`), + apiGet<{ invocations: unknown[] }>(`/namespaces/${namespace}/compute_graphs/${computeGraph}/invocations`) + ]) + + const localComputeGraph = computeGraphs.compute_graphs.find( + (graph: ComputeGraph) => graph.name === computeGraph + ) if (!localComputeGraph) { - throw new Error(`Compute graph ${computeGraph} not found`); + throw new Error(`Compute graph ${computeGraph} not found`) } return { - invocationsList: invocationsResponse.data.invocations, + invocationsList: invocations.invocations, computeGraph: localComputeGraph, namespace, } } catch (error) { - console.error("Error fetching compute graph data:", error); - throw error; + console.error("Error fetching compute graph data:", error) + throw error } } @@ -83,12 +85,12 @@ export async function InvocationsPageLoader({ params }: LoaderFunctionArgs) { } export async function NamespacesPageLoader() { - const namespaces = (await apiClient.get(`/namespaces`)).data.namespaces; + const { namespaces } = await apiGet<{ namespaces: string[] }>('/namespaces') return { namespaces } } export async function ExecutorsPageLoader() { - const executors = (await apiClient.get(`/internal/executors`)).data; + const executors = await apiGet('/internal/executors') return { executors } } From bc17c65d4e9cfb93ba42b53a80d03f3e3d9288c2 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Thu, 7 Nov 2024 00:09:02 +0530 Subject: [PATCH 2/4] feat: removed other namespace options Signed-off-by: Adithya Krishna --- .../ui/src/components/NamespaceSelector.tsx | 64 ------------------- .../components/tables/ComputeGraphTable.tsx | 1 + server/ui/src/index.tsx | 8 --- server/ui/src/routes/root.tsx | 34 ++-------- server/ui/src/utils/loaders.ts | 5 -- 5 files changed, 5 insertions(+), 107 deletions(-) delete mode 100644 server/ui/src/components/NamespaceSelector.tsx diff --git a/server/ui/src/components/NamespaceSelector.tsx b/server/ui/src/components/NamespaceSelector.tsx deleted file mode 100644 index a7dff01b7..000000000 --- a/server/ui/src/components/NamespaceSelector.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useNavigate, useParams, useLocation, useLoaderData } from 'react-router-dom'; -import { - FormControl, - Select, - MenuItem, - SelectChangeEvent, - Box, - InputLabel -} from '@mui/material'; - -interface Namespace { - name: string; - created_at: number; -} - -interface LoaderData { - namespaces: Namespace[]; - namespace: string; -} - -export function NamespaceSelector() { - const navigate = useNavigate(); - const location = useLocation(); - const { namespace } = useParams(); - const { namespaces } = useLoaderData() as LoaderData; - - const handleNamespaceChange = (event: SelectChangeEvent) => { - const value = event.target.value; - const [firstSegment, secondSegment, ...rest] = location.pathname - .split('/') - .filter(Boolean); - - if (firstSegment === 'namespaces' || !secondSegment) { - navigate(`/${value}/compute-graphs`); - return; - } - - const remainingPath = rest.length ? `/${rest.join('/')}` : ''; - navigate(`/${value}/${secondSegment}${remainingPath}`); - }; - - return ( - - - Select your namespace - - - - ); -} - -export default NamespaceSelector; diff --git a/server/ui/src/components/tables/ComputeGraphTable.tsx b/server/ui/src/components/tables/ComputeGraphTable.tsx index 27544e4b1..33c6c6336 100644 --- a/server/ui/src/components/tables/ComputeGraphTable.tsx +++ b/server/ui/src/components/tables/ComputeGraphTable.tsx @@ -33,6 +33,7 @@ const TYPE_COLORS = { const CELL_STYLES = { fontSize: 14, pt: 1 } as const; const CHIP_STYLES = { height: '16px', + width: 'fit-content', fontSize: '0.625rem', '& .MuiChip-label': { padding: '0 6px' }, } as const; diff --git a/server/ui/src/index.tsx b/server/ui/src/index.tsx index 9033d56f3..e9bcda99f 100644 --- a/server/ui/src/index.tsx +++ b/server/ui/src/index.tsx @@ -17,11 +17,9 @@ import { ExecutorsPageLoader, IndividualComputeGraphPageLoader, IndividualInvocationPageLoader, - NamespacesPageLoader, } from "./utils/loaders"; import { ComputeGraphsPage, - NamespacesPage, IndividualComputeGraphPage, IndividualInvocationPage, ExecutorsPage, @@ -66,12 +64,6 @@ const router = createBrowserRouter( element: , errorElement: }, - { - path: "/namespaces", - element: , - loader: NamespacesPageLoader, - errorElement: - }, { path: "/:namespace/compute-graphs", element: , diff --git a/server/ui/src/routes/root.tsx b/server/ui/src/routes/root.tsx index 3695f7a5a..061cd6140 100644 --- a/server/ui/src/routes/root.tsx +++ b/server/ui/src/routes/root.tsx @@ -15,28 +15,19 @@ import { LoaderFunctionArgs, Outlet, redirect, - useLoaderData, useLocation, Link } from 'react-router-dom'; -import { Cpu, TableDocument, Setting4 } from 'iconsax-react'; -import { IndexifyClient } from 'getindexify'; +import { Cpu, Setting4 } from 'iconsax-react'; import { getIndexifyServiceURL } from '../utils/helpers'; import theme from '../theme'; -// Components import Footer from '../components/Footer'; import VersionDisplay from '../components/VersionDisplay'; -import NamespaceSelector from '../components/NamespaceSelector'; -// Constants const DRAWER_WIDTH = 240; const SERVICE_URL = getIndexifyServiceURL(); -interface LoaderData { - namespace: string; -} - interface NavItem { path: string; icon: ReactElement; @@ -44,34 +35,18 @@ interface NavItem { } export async function loader({ params }: LoaderFunctionArgs) { - const response = await IndexifyClient.namespaces({ - serviceUrl: SERVICE_URL, - }); - - const namespaceNames = response.map((repo) => repo.name); - - if (!params.namespace || !namespaceNames.includes(params.namespace)) { - return redirect('/default/compute-graphs'); - } - - return { namespace: params.namespace || 'default' }; + return redirect('/default/compute-graphs'); } function Dashboard() { - const { namespace } = useLoaderData() as LoaderData; const location = useLocation(); const navItems: NavItem[] = [ { - path: `/${namespace}/compute-graphs`, + path: '/default/compute-graphs', icon: , label: 'Compute Graphs' }, - { - path: '/namespaces', - icon: , - label: 'Namespaces' - }, { path: '/executors', icon: , @@ -140,7 +115,6 @@ function Dashboard() { }} > - {navItems.map(({ path, icon, label }) => ( ('/namespaces') - return { namespaces } -} - export async function ExecutorsPageLoader() { const executors = await apiGet('/internal/executors') return { executors } From 2b441c03fa41f9485f9d6151ba234398dbb56147 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Thu, 7 Nov 2024 00:11:24 +0530 Subject: [PATCH 3/4] chore: removed compute function column Signed-off-by: Adithya Krishna --- server/ui/src/components/tables/InvocationOutputTable.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/ui/src/components/tables/InvocationOutputTable.tsx b/server/ui/src/components/tables/InvocationOutputTable.tsx index f9683efa1..65379d636 100644 --- a/server/ui/src/components/tables/InvocationOutputTable.tsx +++ b/server/ui/src/components/tables/InvocationOutputTable.tsx @@ -131,7 +131,7 @@ function InvocationOutputTable({ indexifyServiceURL, invocationId, namespace, co id={`panel${index}-header`} > - {computeFn} ({outputs.length} outputs) + Compute Function - {computeFn} ({outputs.length} outputs) e.stopPropagation()} @@ -157,7 +157,6 @@ function InvocationOutputTable({ indexifyServiceURL, invocationId, namespace, co
- Compute Function ID Created At @@ -165,7 +164,6 @@ function InvocationOutputTable({ indexifyServiceURL, invocationId, namespace, co {outputs.map((output, idx) => ( - {output.compute_fn} {output.id} {formatTimestamp(output.created_at)} From 266161f0d74f5b003192bee8a5967ba4e3d7faaa Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Thu, 7 Nov 2024 10:29:14 +0700 Subject: [PATCH 4/4] feat: updated links and fixed styling issues Signed-off-by: Adithya Krishna --- .../components/cards/ComputeGraphsCard.tsx | 2 +- .../ui/src/components/cards/ExecutorsCard.tsx | 2 +- .../Namespace/IndividualInvocationPage.tsx | 137 +++++++----------- 3 files changed, 58 insertions(+), 83 deletions(-) diff --git a/server/ui/src/components/cards/ComputeGraphsCard.tsx b/server/ui/src/components/cards/ComputeGraphsCard.tsx index 9b2957a95..ba46483cc 100644 --- a/server/ui/src/components/cards/ComputeGraphsCard.tsx +++ b/server/ui/src/components/cards/ComputeGraphsCard.tsx @@ -90,7 +90,7 @@ export function ComputeGraphsCard({ client, computeGraphs, namespace }: ComputeG Compute Graphs diff --git a/server/ui/src/components/cards/ExecutorsCard.tsx b/server/ui/src/components/cards/ExecutorsCard.tsx index ab512000a..f55269901 100644 --- a/server/ui/src/components/cards/ExecutorsCard.tsx +++ b/server/ui/src/components/cards/ExecutorsCard.tsx @@ -83,7 +83,7 @@ export function ExecutorsCard({ executors }: ExecutorsCardProps) { Executors { + const { + indexifyServiceURL, + invocationId, + computeGraph, + namespace + } = + useLoaderData() as { + indexifyServiceURL: string + invocationId: string, + computeGraph: string, + namespace: string + } - -interface InvocationPageData { - indexifyServiceURL: string - invocationId: string - computeGraph: string - namespace: string -} - -function BreadcrumbTrail({ namespace, computeGraph, invocationId }: Omit) { - return ( - } - > - {namespace} - - Compute Graphs - - - {computeGraph} - - {invocationId} - - ) -} - -function InvocationHeader({ invocationId }: { invocationId: string }) { return ( -
-
- -
- - Invocation - {invocationId} - - -
- ) -} - -function IndividualInvocationPage() { - const { indexifyServiceURL, invocationId, computeGraph, namespace } = useLoaderData() as InvocationPageData - - return ( -
- - -
- - - Loading output table...
}> - - - - Loading tasks table...
}> - - - - - ) -} + + } + > + {namespace} + + Compute Graphs + + + {computeGraph} + + {invocationId} + + + +
+
+ +
+ + Invocation - {invocationId} + +
+ +
+ +
+
+ ); +}; export default IndividualInvocationPage;