List.jsx
1 import { useState, useEffect } from "react"; 2 import { Box, Button, Chip, IconButton, Tooltip, styled } from "@mui/material"; 3 import { Add, Edit, Delete, Visibility, Hub } from "@mui/icons-material"; 4 import { useNavigate } from "react-router-dom"; 5 import { toast } from "react-toastify"; 6 import useAuth from "app/hooks/useAuth"; 7 import Breadcrumb from "app/components/Breadcrumb"; 8 import { useTranslation } from "react-i18next"; 9 import DataList from "app/components/DataList"; 10 import api from "app/utils/api"; 11 12 const Container = styled("div")(({ theme }) => ({ 13 margin: "24px 48px", 14 [theme.breakpoints.down("md")]: { margin: "24px 32px" }, 15 [theme.breakpoints.down("sm")]: { margin: 16 }, 16 "& .breadcrumb": { marginBottom: 24 }, 17 })); 18 19 export default function Embeddings() { 20 const { t } = useTranslation(); 21 const [embeddings, setEmbeddings] = useState([]); 22 const auth = useAuth(); 23 const navigate = useNavigate(); 24 const isAdmin = auth.user?.is_admin; 25 26 const fetchEmbeddings = () => { 27 api.get("/embeddings", auth.user.token) 28 .then((d) => setEmbeddings(Array.isArray(d) ? d : (d?.embeddings || []))) 29 .catch(() => {}); 30 }; 31 32 useEffect(() => { 33 document.title = (process.env.REACT_APP_RESTAI_NAME || "RESTai") + " - Embeddings"; 34 fetchEmbeddings(); 35 }, []); 36 37 const handleDelete = (e, em) => { 38 e.stopPropagation(); 39 if (!window.confirm(t("embeddings.info.deleteConfirm", { name: em.name }))) return; 40 api.delete("/embeddings/" + em.id, auth.user.token) 41 .then(() => { 42 toast.success(t("embeddings.info.deleted", { name: em.name })); 43 fetchEmbeddings(); 44 }) 45 .catch(() => {}); 46 }; 47 48 const embTooltip = (row) => ( 49 <Box sx={{ p: 0.5, maxWidth: 320 }}> 50 <Box sx={{ fontWeight: 600, mb: 0.5 }}>{row.name}</Box> 51 {row.description && ( 52 <Box sx={{ mb: 0.75, fontSize: "0.8rem", color: "grey.100", lineHeight: 1.4 }}> 53 {row.description} 54 </Box> 55 )} 56 <Box sx={{ fontSize: "0.75rem", lineHeight: 1.5 }}> 57 <div><strong>{t("embeddings.tooltip.class")}:</strong> {row.class_name}</div> 58 <div><strong>{t("embeddings.tooltip.privacy")}:</strong> {row.privacy}</div> 59 {row.dimension && ( 60 <div><strong>{t("embeddings.tooltip.dimensions")}:</strong> {row.dimension}</div> 61 )} 62 </Box> 63 </Box> 64 ); 65 66 const columns = [ 67 { 68 key: "name", 69 label: t("embeddings.columns.name"), 70 sortable: true, 71 render: (row) => ( 72 <Tooltip title={embTooltip(row)} placement="right" arrow> 73 <Box sx={{ display: "flex", alignItems: "center", gap: 1.5, cursor: "help" }}> 74 <Box 75 sx={{ 76 width: 32, height: 32, borderRadius: "8px", 77 display: "flex", alignItems: "center", justifyContent: "center", 78 background: (t) => t.palette.mode === "dark" ? "rgba(249,115,22,0.15)" : "rgba(249,115,22,0.1)", 79 }} 80 > 81 <Hub sx={{ fontSize: 18, color: "#f97316" }} /> 82 </Box> 83 <Box sx={{ fontWeight: 500 }}>{row.name}</Box> 84 </Box> 85 </Tooltip> 86 ), 87 }, 88 { 89 key: "class_name", 90 label: t("embeddings.columns.class"), 91 sortable: true, 92 render: (row) => ( 93 <Box sx={{ fontFamily: "monospace", fontSize: "0.85rem", color: "text.secondary" }}> 94 {row.class_name} 95 </Box> 96 ), 97 }, 98 { 99 key: "privacy", 100 label: t("embeddings.columns.privacy"), 101 sortable: true, 102 render: (row) => ( 103 <Chip 104 label={row.privacy} 105 size="small" 106 sx={{ 107 backgroundColor: row.privacy === "private" ? "rgba(239,68,68,0.12)" : "rgba(16,185,129,0.12)", 108 color: row.privacy === "private" ? "#ef4444" : "#10b981", 109 fontWeight: 600, 110 fontSize: "0.72rem", 111 textTransform: "uppercase", 112 height: 22, 113 }} 114 /> 115 ), 116 }, 117 { 118 key: "dimension", 119 label: t("embeddings.columns.dimensions"), 120 sortable: true, 121 align: "right", 122 render: (row) => row.dimension ?? "—", 123 }, 124 ]; 125 126 return ( 127 <Container> 128 <Box className="breadcrumb"> 129 <Breadcrumb routeSegments={[{ name: t("nav.embeddings"), path: "/embeddings" }]} /> 130 </Box> 131 132 <DataList 133 title={t("embeddings.title")} 134 subtitle={t("embeddings.subtitle")} 135 data={embeddings} 136 columns={columns} 137 searchKeys={["name", "class_name"]} 138 filters={[ 139 { 140 key: "privacy", 141 label: t("embeddings.columns.privacy"), 142 options: [ 143 { value: "private", label: t("common.private") }, 144 { value: "public", label: t("common.public") }, 145 ], 146 }, 147 ]} 148 onRowClick={(row) => navigate(`/embedding/${row.id}`)} 149 rowKey={(row) => row.id} 150 defaultSort={{ key: "name", direction: "asc" }} 151 headerAction={ 152 isAdmin && ( 153 <Button variant="contained" startIcon={<Add />} onClick={() => navigate("/embeddings/new")}> 154 {t("embeddings.newBreadcrumb")} 155 </Button> 156 ) 157 } 158 actions={(row) => ( 159 <> 160 <Tooltip title={t("embeddings.actions.view")}> 161 <IconButton size="small" onClick={() => navigate(`/embedding/${row.id}`)}> 162 <Visibility fontSize="small" /> 163 </IconButton> 164 </Tooltip> 165 {isAdmin && ( 166 <> 167 <Tooltip title={t("embeddings.actions.edit")}> 168 <IconButton size="small" onClick={() => navigate(`/embedding/${row.id}/edit`)}> 169 <Edit fontSize="small" /> 170 </IconButton> 171 </Tooltip> 172 <Tooltip title={t("embeddings.actions.delete")}> 173 <IconButton size="small" color="error" onClick={(e) => handleDelete(e, row)}> 174 <Delete fontSize="small" /> 175 </IconButton> 176 </Tooltip> 177 </> 178 )} 179 </> 180 )} 181 emptyState={{ 182 icon: Hub, 183 title: t("embeddings.emptyTitle"), 184 message: t("embeddings.emptyMessage"), 185 actionLabel: isAdmin ? t("embeddings.new") : undefined, 186 actionIcon: <Add fontSize="small" />, 187 onAction: isAdmin ? () => navigate("/embeddings/new") : undefined, 188 }} 189 emptyMessage={t("embeddings.noEmbeddings")} 190 /> 191 </Container> 192 ); 193 }