github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/pages/repositories/repository/fileRenderers/data.tsx (about) 1 import React, {FC, FormEvent, useCallback, useEffect, useState} from "react"; 2 import {runDuckDBQuery} from "./duckdb"; 3 import * as arrow from 'apache-arrow'; 4 import Form from "react-bootstrap/Form"; 5 import Button from "react-bootstrap/Button"; 6 import {ChevronRightIcon} from "@primer/octicons-react"; 7 import dayjs from "dayjs"; 8 import Table from "react-bootstrap/Table"; 9 10 import {SQLEditor} from "./editor"; 11 import {RendererComponent} from "./types"; 12 import {AlertError, Loading} from "../../../../lib/components/controls"; 13 14 15 const MAX_RESULTS_RETURNED = 1000; 16 17 export const DataLoader: FC = () => { 18 return <Loading/> 19 } 20 21 export const DuckDBRenderer: FC<RendererComponent> = ({repoId, refId, path, fileExtension }) => { 22 let initialQuery = `SELECT * FROM READ_PARQUET('lakefs://${repoId}/${refId}/${path}') LIMIT 20`; 23 if (fileExtension === 'csv') { 24 initialQuery = `SELECT * FROM READ_CSV('lakefs://${repoId}/${refId}/${path}', AUTO_DETECT = TRUE) LIMIT 20` 25 } else if (fileExtension === 'tsv') { 26 initialQuery = `SELECT * FROM READ_CSV('lakefs://${repoId}/${refId}/${path}', DELIM='\t', AUTO_DETECT=TRUE) LIMIT 20` 27 } 28 const [shouldSubmit, setShouldSubmit] = useState<boolean>(true) 29 // eslint-disable-next-line @typescript-eslint/no-explicit-any 30 const [data, setData] = useState<arrow.Table<any> | null>(null); 31 const [error, setError] = useState<string | null>(null) 32 const [loading, setLoading] = useState<boolean>(false) 33 34 const handleSubmit = useCallback((event: FormEvent<HTMLFormElement>) => { 35 event.preventDefault() 36 setShouldSubmit(prev => !prev) 37 }, [setShouldSubmit]) 38 39 const handleRun = useCallback(() => { 40 setShouldSubmit(prev => !prev) 41 }, [setShouldSubmit]) 42 43 44 const [sql, setSql] = useState(initialQuery); 45 const sqlChangeHandler = useCallback((data: React.SetStateAction<string>) => { 46 setSql(data) 47 }, [setSql]) 48 49 useEffect(() => { 50 if (!sql) { 51 return; 52 } 53 const runQuery = async (sql: string) => { 54 setLoading(true) 55 setError(null) 56 try { 57 const results = await runDuckDBQuery(sql) 58 setData(results) 59 } catch (e) { 60 setError(e.toString()) 61 setData(null) 62 } finally { 63 setLoading(false) 64 } 65 } 66 runQuery(sql).catch(console.error); 67 }, [repoId, refId, path, shouldSubmit]) 68 69 let content; 70 const button = ( 71 <Button type="submit" variant="success" disabled={loading}> 72 <ChevronRightIcon /> {" "} 73 { loading ? "Executing..." : "Execute" } 74 </Button> 75 ); 76 77 if (error) { 78 content = <AlertError error={error}/> 79 } else if (data === null) { 80 content = <DataLoader/> 81 } else { 82 if (!data || data.numRows === 0) { 83 content = ( 84 <p className="text-md-center mt-5 mb-5"> 85 No rows returned. 86 </p> 87 ) 88 } else { 89 const fields = data.schema.fields 90 const totalRows = data.numRows 91 let res = data; 92 if (totalRows > MAX_RESULTS_RETURNED) { 93 res = data.slice(0, MAX_RESULTS_RETURNED) 94 } 95 content = ( 96 <> 97 {(res.numRows < data.numRows) && 98 <small>{`Showing only the first ${res.numRows.toLocaleString()} rows (out of ${data.numRows.toLocaleString()})`}</small> 99 } 100 <div className="object-viewer-sql-results"> 101 <Table striped bordered hover size={"sm"} responsive={true}> 102 <thead className="table-dark"> 103 <tr> 104 {fields.map((field, i) => 105 <th key={i}> 106 {field.name} 107 <br/> 108 <small>{field.type.toString()}</small> 109 </th> 110 )} 111 </tr> 112 </thead> 113 <tbody> 114 {[...res].map((row, i) => ( 115 <tr key={`row-${i}`}> 116 {[...row].map((v, j: number) => { 117 return ( 118 <DataRow key={`col-${i}-${j}`} value={v[1]}/> 119 ) 120 121 })} 122 </tr> 123 ))} 124 </tbody> 125 </Table> 126 </div> 127 </> 128 ) 129 } 130 } 131 132 return ( 133 <div> 134 <Form onSubmit={handleSubmit}> 135 <Form.Group className="mt-2 mb-1" controlId="objectQuery"> 136 <SQLEditor initialValue={initialQuery} onChange={sqlChangeHandler} onRun={handleRun}/> 137 </Form.Group> 138 139 140 <div className="d-flex mb-4"> 141 <div className="d-flex flex-fill justify-content-start"> 142 {button} 143 </div> 144 145 <div className="d-flex justify-content-end"> 146 <p className="text-muted text-end powered-by"> 147 <small> 148 Powered by <a href="https://duckdb.org/2021/10/29/duckdb-wasm.html" target="_blank" rel="noreferrer">DuckDB-WASM</a>. 149 For a full SQL reference, see the <a href="https://duckdb.org/docs/sql/statements/select" target="_blank" rel="noreferrer">DuckDB Documentation</a> 150 </small> 151 </p> 152 </div> 153 154 </div> 155 156 157 </Form> 158 <div className="mt-3"> 159 {content} 160 </div> 161 </div> 162 ) 163 } 164 165 // eslint-disable-next-line @typescript-eslint/no-explicit-any 166 const DataRow: FC<{ value: any }> = ({ value }) => { 167 let dataType = 'regular'; 168 if (typeof value === 'string') { 169 dataType = 'string'; 170 } else if (value instanceof Date) { 171 dataType = 'date' 172 } else if (typeof value === 'number') { 173 dataType = 'number' 174 } 175 176 if (dataType === 'string') { 177 return <td>{value}</td> 178 } 179 180 if (dataType === 'date') { 181 return <td>{dayjs(value).format()}</td> 182 } 183 184 if (dataType === 'number') { 185 return <td>{value.toLocaleString("en-US")}</td> 186 } 187 188 return <td>{"" + value}</td>; 189 } 190 191