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