diff --git a/src/components/gui/aggregate-result/aggregate-result-button.tsx b/src/components/gui/aggregate-result/aggregate-result-button.tsx new file mode 100644 index 00000000..72844bb1 --- /dev/null +++ b/src/components/gui/aggregate-result/aggregate-result-button.tsx @@ -0,0 +1,129 @@ +import { buttonVariants } from "../../ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; +import OptimizeTableState, { + AggregateFunction, +} from "../table-optimized/OptimizeTableState"; +import { useCallback, useEffect, useState } from "react"; +import ListButtonItem from "../list-button-item"; +import { LucideCheck, LucideChevronDown } from "lucide-react"; +export interface AggregateResult { + sum: number | string | undefined; + avg: number | string | undefined; + min: number | string | undefined; + max: number | string | undefined; + count: number | string | undefined; +} + +export default function AggregateResultButton({ + data, +}: { + data: OptimizeTableState; +}) { + const [result, setResult] = useState({ + sum: undefined, + avg: undefined, + min: undefined, + max: undefined, + count: undefined, + }); + + const [defaultFunction, setDefaultFunction] = useState( + data.getDefaultAggregateFunction() + ); + useEffect(() => { + const changeCallback = () => { + setResult({ ...data.getSelectionAggregatedResult() }); + }; + + data.addChangeListener(changeCallback); + return () => data.removeChangeListener(changeCallback); + }, [data]); + + let displayResult = ""; + + if (defaultFunction && result[defaultFunction]) { + displayResult = `${defaultFunction.toUpperCase()}: ${result[defaultFunction]}`; + } else { + if (result.sum !== undefined) { + displayResult = `SUM: ${result.sum}`; + } else if (result.avg !== undefined) { + displayResult = `AVG: ${result.avg}`; + } else if (result.min !== undefined) { + displayResult = `MIN: ${result.min}`; + } else if (result.max !== undefined) { + displayResult = `MAX: ${result.max}`; + } else if (result.count != undefined) { + displayResult = `COUNT: ${result.count}`; + } + } + + const handleSetDefaultFunction = useCallback( + (functionName: AggregateFunction) => { + setDefaultFunction(functionName); + data.setDefaultAggregateFunction(functionName); + }, + [data] + ); + + if (result.count && Number(result.count) <= 1) return null; + + return ( + + +
+ {displayResult}{" "} + {!!displayResult && } +
+
+ +
+ {!!result.sum && ( + + )} + {!!result.avg && ( + + )} + {!!result.max && ( + + )} + {!!result.min && ( + + )} + {!!result.count && ( + + )} +
+
+
+ ); +} diff --git a/src/components/gui/list-button-item.tsx b/src/components/gui/list-button-item.tsx index 05c79734..4b22e6b2 100644 --- a/src/components/gui/list-button-item.tsx +++ b/src/components/gui/list-button-item.tsx @@ -10,7 +10,7 @@ export default function ListButtonItem({ }: Readonly<{ selected?: boolean; text: string; - icon: Icon; + icon?: Icon; onClick: () => void; }>) { return ( @@ -25,7 +25,11 @@ export default function ListButtonItem({ "cursor-pointer" )} > - + {Icon ? ( + + ) : ( +
+ )} {text} ); diff --git a/src/components/gui/query-result-table.tsx b/src/components/gui/query-result-table.tsx index b3d965fe..96275549 100644 --- a/src/components/gui/query-result-table.tsx +++ b/src/components/gui/query-result-table.tsx @@ -24,6 +24,7 @@ import { DropdownMenuSeparator, } from "../ui/dropdown-menu"; import useTableResultContextMenu from "./table-result/context-menu"; +import { cn } from "@/lib/utils"; interface ResultTableProps { data: OptimizeTableState; @@ -36,34 +37,64 @@ interface ResultTableProps { function Header({ children, header, -}: PropsWithChildren<{ header: OptimizeTableHeaderWithIndexProps }>) { + internalState, +}: PropsWithChildren<{ + header: OptimizeTableHeaderWithIndexProps; + internalState: OptimizeTableState; +}>) { const [open, setOpen] = useState(false); + const colIndex = header.index; + + let textClass = "grow line-clamp-1 font-mono font-bold"; + let thClass = "flex grow items-center px-2 overflow-hidden"; + + if (internalState.getSelectedColIndex().includes(colIndex)) { + if (internalState.isFullSelectionCol(colIndex)) { + textClass = cn( + "grow line-clamp-1 font-mono font-bold", + "bg-blue-600 border-red-900 text-white font-bold" + ); + thClass = + "flex grow items-center px-2 overflow-hidden bg-blue-600 dark:bg-blue-800"; + } else { + textClass = "grow line-clamp-1 font-mono font-bold text-white font-bold"; + thClass = + "flex grow items-center px-2 overflow-hidden bg-blue-200 dark:bg-blue-400"; + } + } return ( - - -
{ - setOpen(true); - }} - > - {header.icon ?
{header.icon}
: null} -
- {header.displayName} -
+
{ + const focusCell = internalState.getFocus(); + if (e.shiftKey && focusCell) { + internalState.selectColRange(focusCell.x, colIndex); + } else if (e.ctrlKey && focusCell) { + internalState.addSelectionCol(colIndex); + internalState.setFocus(0, colIndex); + } else { + internalState.selectColumn(colIndex); + internalState.setFocus(0, colIndex); + } + }} + > + {header.icon ?
{header.icon}
: null} +
{header.displayName}
+ + -
- - - {children} - - + + + {children} + + +
); } @@ -108,7 +139,7 @@ export default function ResultTable({ ) : undefined; return ( -
+
{foreignKeyInfo} {generatedInfo} ); }, - [stickyHeaderIndex, tableName, onSortColumnChange] + [data, tableName, stickyHeaderIndex, onSortColumnChange] ); const onHeaderContextMenu = useCallback((e: React.MouseEvent) => { @@ -182,6 +213,56 @@ export default function ResultTable({ pasteCallback, }); + const onShiftKeyDownCallBack = useCallback( + (state: OptimizeTableState, e: React.KeyboardEvent) => { + const focus = state.getFocus(); + if (e.shiftKey && focus) { + let lastMove = null; + if (state.getLastMove()) { + lastMove = state.getLastMove(); + } else { + const selectedRange = state.getSelectionRange(focus.y, focus.x); + if (selectedRange) + lastMove = { x: selectedRange.x2, y: selectedRange.y2 }; + } + + if (lastMove) { + const rows = state.getRowsCount(); + const cols = state.getHeaderCount(); + let newRow = lastMove.y; + let newCol = lastMove.x; + let horizontal: "right" | "left" = "left"; + let vertical: "top" | "bottom" = "bottom"; + if (e.key === "ArrowUp") { + newRow = Math.max(lastMove.y - 1, 0); + horizontal = "left"; + vertical = "top"; + } + if (e.key === "ArrowDown") { + horizontal = "left"; + vertical = "bottom"; + newRow = Math.min(lastMove.y + 1, rows - 1); + } + if (e.key === "ArrowLeft") { + horizontal = "left"; + vertical = "top"; + newCol = Math.max(lastMove.x - 1, 0); + } + if (e.key === "ArrowRight") { + horizontal = "right"; + vertical = "top"; + newCol = Math.min(lastMove.x + 1, cols - 1); + } + + state.selectCellRange(focus.y, focus.x, newRow, newCol); + state.setLastMove(newRow, newCol); + state.scrollToCell(horizontal, vertical, { x: newCol, y: newRow }); + } + } + }, + [] + ); + const onKeyDown = useCallback( (state: OptimizeTableState, e: React.KeyboardEvent) => { if (state.isInEditMode()) return; @@ -193,28 +274,47 @@ export default function ResultTable({ ) { pasteCallback(state); } else if (e.key === "ArrowRight") { - const focus = state.getFocus(); - if (focus && focus.x + 1 < state.getHeaderCount()) { - state.setFocus(focus.y, focus.x + 1); - state.scrollToFocusCell("right", "top"); + if (e.shiftKey) { + onShiftKeyDownCallBack(state, e); + } else { + const focus = state.getFocus(); + if (focus && focus.x + 1 < state.getHeaderCount()) { + state.setFocus(focus.y, focus.x + 1); + state.scrollToCell("right", "top", { y: focus.y, x: focus.x + 1 }); + } } } else if (e.key === "ArrowLeft") { - const focus = state.getFocus(); - if (focus && focus.x - 1 >= 0) { - state.setFocus(focus.y, focus.x - 1); - state.scrollToFocusCell("left", "top"); + if (e.shiftKey) { + onShiftKeyDownCallBack(state, e); + } else { + const focus = state.getFocus(); + if (focus && focus.x - 1 >= 0) { + state.setFocus(focus.y, focus.x - 1); + state.scrollToCell("left", "top", { y: focus.y, x: focus.x - 1 }); + } } } else if (e.key === "ArrowUp") { - const focus = state.getFocus(); - if (focus && focus.y - 1 >= 0) { - state.setFocus(focus.y - 1, focus.x); - state.scrollToFocusCell("left", "top"); + if (e.shiftKey) { + onShiftKeyDownCallBack(state, e); + } else { + const focus = state.getFocus(); + if (focus && focus.y - 1 >= 0) { + state.setFocus(focus.y - 1, focus.x); + state.scrollToCell("left", "top", { y: focus.y - 1, x: focus.x }); + } } } else if (e.key === "ArrowDown") { - const focus = state.getFocus(); - if (focus && focus.y + 1 < state.getRowsCount()) { - state.setFocus(focus.y + 1, focus.x); - state.scrollToFocusCell("left", "bottom"); + if (e.shiftKey) { + onShiftKeyDownCallBack(state, e); + } else { + const focus = state.getFocus(); + if (focus && focus.y + 1 < state.getRowsCount()) { + state.setFocus(focus.y + 1, focus.x); + state.scrollToCell("left", "bottom", { + y: focus.y + 1, + x: focus.x, + }); + } } } else if (e.key === "Tab") { const focus = state.getFocus(); @@ -225,7 +325,10 @@ export default function ResultTable({ const y = Math.floor(n / colCount); if (y >= state.getRowsCount()) return; state.setFocus(y, x); - state.scrollToFocusCell(x === 0 ? "left" : "right", "bottom"); + state.scrollToCell(x === 0 ? "left" : "right", "bottom", { + y: y, + x: x, + }); } } else if (e.key === "Enter") { state.enterEditMode(); @@ -233,7 +336,7 @@ export default function ResultTable({ e.preventDefault(); }, - [copyCallback, pasteCallback] + [copyCallback, onShiftKeyDownCallBack, pasteCallback] ); return ( diff --git a/src/components/gui/result-stat.tsx b/src/components/gui/result-stat.tsx index 7a9bcd9b..5e6b65ff 100644 --- a/src/components/gui/result-stat.tsx +++ b/src/components/gui/result-stat.tsx @@ -25,7 +25,7 @@ export default function ResultStats({ stats }: { stats: DatabaseResultStat }) { {!!stats.rowsAffected && (
- Affected Rows:{" "} + Affected Rows:{" "} {stats.rowsAffected}
)} diff --git a/src/components/gui/table-cell/create-editable-cell.tsx b/src/components/gui/table-cell/create-editable-cell.tsx index 08251925..5a50ba56 100644 --- a/src/components/gui/table-cell/create-editable-cell.tsx +++ b/src/components/gui/table-cell/create-editable-cell.tsx @@ -82,7 +82,7 @@ function InputCellEditor({ applyChange(value, false); state.setFocus(y, x); - state.scrollToFocusCell(x === 0 ? "left" : "right", "bottom"); + state.scrollToCell(x === 0 ? "left" : "right", "bottom", focus); e.preventDefault(); e.stopPropagation(); } diff --git a/src/components/gui/table-optimized/OptimizeTableState.tsx b/src/components/gui/table-optimized/OptimizeTableState.tsx index 415c06bd..2142e1e9 100644 --- a/src/components/gui/table-optimized/OptimizeTableState.tsx +++ b/src/components/gui/table-optimized/OptimizeTableState.tsx @@ -9,6 +9,7 @@ import { } from "@/drivers/base-driver"; import { ReactElement } from "react"; import deepEqual from "deep-equal"; +import { formatNumber } from "@/lib/convertNumber"; export interface OptimizeTableRowValue { raw: Record; @@ -18,6 +19,8 @@ export interface OptimizeTableRowValue { isRemoved?: boolean; } +export type AggregateFunction = "sum" | "avg" | "min" | "max" | "count"; + type TableChangeEventCallback = (state: OptimizeTableState) => void; interface TableSelectionRange { @@ -31,6 +34,9 @@ export default class OptimizeTableState { protected focus: [number, number] | null = null; protected data: OptimizeTableRowValue[] = []; + // last move is used to track cell where user use arrow key to move on using shift key + protected lastMove: [number, number] | null = null; + // Selelection range will be replaced our old selected rows implementation // It offers better flexiblity and allow us to implement more features protected selectionRanges: TableSelectionRange[] = []; @@ -51,6 +57,7 @@ export default class OptimizeTableState { protected changeCounter = 1; protected changeLogs: Record = {}; + protected defaultAggregateFunction: AggregateFunction = "sum"; static createFromResult( driver: BaseDriver, @@ -191,6 +198,80 @@ export default class OptimizeTableState { return true; } + protected mergeSelectionRanges() { + // Sort ranges to simplify merging + this.selectionRanges.sort((a, b) => a.y1 - b.y1 || a.x1 - b.x1); + + const merged: TableSelectionRange[] = []; + let isLastMoveMerged = false; + + for (const range of this.selectionRanges) { + const last = merged[merged.length - 1]; + if ( + last && + ((last.y1 === range.y1 && + last.y2 === range.y2 && + last.x2 + 1 === range.x1) || + (last.x1 === range.x1 && + last.x2 === range.x2 && + last.y2 + 1 === range.y1)) + ) { + last.x2 = Math.max(last.x2, range.x2); + last.y2 = Math.max(last.y2, range.y2); + isLastMoveMerged = true; + } else { + merged.push({ ...range }); + isLastMoveMerged = false; + } + } + this.selectionRanges = merged; + if (isLastMoveMerged) this.mergeSelectionRanges(); + } + + protected splitSelectionRange( + selection: TableSelectionRange, + deselection: TableSelectionRange + ): TableSelectionRange[] { + const result: TableSelectionRange[] = []; + + if (deselection.y1 > selection.y1) { + result.push({ + x1: selection.x1, + y1: selection.y1, + x2: selection.x2, + y2: deselection.y1 - 1, + }); + } + + if (deselection.y2 < selection.y2) { + result.push({ + x1: selection.x1, + y1: deselection.y2 + 1, + x2: selection.x2, + y2: selection.y2, + }); + } + + if (deselection.x1 > selection.x1) { + result.push({ + x1: selection.x1, + y1: Math.max(selection.y1, deselection.y1), + x2: deselection.x1 - 1, + y2: Math.min(selection.y2, deselection.y2), + }); + } + + if (deselection.x2 < selection.x2) { + result.push({ + x1: deselection.x2 + 1, + y1: Math.max(selection.y1, deselection.y1), + x2: selection.x2, + y2: Math.min(selection.y2, deselection.y2), + }); + } + return result; + } + // ------------------------------------------------ // Handle headers and data // ------------------------------------------------ @@ -383,6 +464,23 @@ export default class OptimizeTableState { return this.data[idx]; } + getLastMove() { + return this.lastMove + ? { + x: this.lastMove[1], + y: this.lastMove[0], + } + : null; + } + + setLastMove(y: number, x: number) { + this.lastMove = [y, x]; + } + + clearLastMove() { + this.lastMove = null; + } + // ------------------------------------------------ // Handle focus logic // ------------------------------------------------ @@ -418,6 +516,7 @@ export default class OptimizeTableState { setFocus(y: number, x: number) { this.focus = [y, x]; + this.clearLastMove(); this.broadcastChange(); } @@ -453,11 +552,15 @@ export default class OptimizeTableState { return this.headerWidth; } - scrollToFocusCell(horizontal: "left" | "right", vertical: "top" | "bottom") { - if (this.container && this.focus) { - const cellX = this.focus[1]; - const cellY = this.focus[0]; - let cellLeft = 0; + scrollToCell( + horizontal: "left" | "right", + vertical: "top" | "bottom", + cell: { x: number; y: number } + ) { + if (this.container && cell) { + const cellX = cell.x; + const cellY = cell.y; + let cellLeft = 38; let cellRight = 0; const cellTop = (cellY + 1) * 38; const cellBottom = cellTop + 38; @@ -475,7 +578,7 @@ export default class OptimizeTableState { const containerBottom = containerTop + height; if (horizontal === "right") { - if (cellRight > containerRight) { + if (cellRight - 38 > containerRight) { this.container.scrollLeft = Math.max(0, cellRight - width); } } else { @@ -532,6 +635,44 @@ export default class OptimizeTableState { return Array.from(selectedRows.values()); } + getSelectedColIndex() { + const selectedCols = new Set(); + + for (const range of this.selectionRanges) { + for (let i = range.x1; i <= range.x2; i++) { + selectedCols.add(i); + } + } + + return Array.from(selectedCols.values()); + } + + isFullSelectionRow(y: number) { + for (const range of this.selectionRanges) { + if ( + range.y1 <= y && + range.y2 >= y && + range.x1 === 0 && + range.x2 === this.getHeaderCount() - 1 + ) + return true; + } + return false; + } + + isFullSelectionCol(x: number) { + for (const range of this.selectionRanges) { + if ( + range.x1 <= x && + range.x2 >= x && + range.y1 === 0 && + range.y2 === this.getRowsCount() - 1 + ) + return true; + } + return false; + } + selectRow(y: number) { this.selectionRanges = [ { x1: 0, y1: y, x2: this.headers.length - 1, y2: y }, @@ -540,6 +681,14 @@ export default class OptimizeTableState { this.broadcastChange(); } + selectColumn(x: number) { + this.selectionRanges = [ + { x1: x, y1: 0, x2: x, y2: this.getRowsCount() - 1 }, + ]; + + this.broadcastChange(); + } + selectCell(y: number, x: number, focus = true) { this.selectionRanges = [{ x1: x, y1: y, x2: x, y2: y }]; @@ -559,8 +708,81 @@ export default class OptimizeTableState { this.broadcastChange(); } + findSelectionRange(range: TableSelectionRange) { + return this.selectionRanges.findIndex( + (r) => + r.x1 <= range.x1 && + r.x2 >= range.x2 && + r.y1 <= range.y1 && + r.y2 >= range.y2 + ); + } + + addSelectionRange(y1: number, x1: number, y2: number, x2: number) { + const newRange = { + x1: Math.min(x1, x2), + y1: Math.min(y1, y2), + x2: Math.max(x1, x2), + y2: Math.max(y1, y2), + }; + + const selectedRangeIndex = this.findSelectionRange(newRange); + if (selectedRangeIndex < 0) { + this.selectionRanges.push(newRange); + this.mergeSelectionRanges(); + } else { + const selectedRange = this.selectionRanges[selectedRangeIndex]; + const splitedRanges = this.splitSelectionRange(selectedRange, newRange); + if (splitedRanges.length >= 0) { + this.selectionRanges.splice(selectedRangeIndex, 1); + this.selectionRanges = [...this.selectionRanges, ...splitedRanges]; + this.mergeSelectionRanges(); + } + } + this.broadcastChange(); + } + + addSelectionRow(y: number) { + const newRange = { + x1: 0, + y1: y, + x2: this.headers.length - 1, + y2: y, + }; + + this.addSelectionRange(newRange.y1, newRange.x1, newRange.y2, newRange.x2); + } + + addSelectionCol(x: number) { + const newRange = { + x1: x, + y1: 0, + x2: x, + y2: this.getRowsCount() - 1, + }; + + this.addSelectionRange(newRange.y1, newRange.x1, newRange.y2, newRange.x2); + } + selectRowRange(y1: number, y2: number) { - this.selectionRanges = [{ x1: 0, y1, x2: this.headers.length - 1, y2 }]; + const newRange = { + x1: 0, + y1: Math.min(y1, y2), + x2: this.headers.length - 1, + y2: Math.max(y1, y2), + }; + this.selectionRanges = [newRange]; + this.broadcastChange(); + } + + selectColRange(x1: number, x2: number) { + const newRange = { + x1: Math.min(x1, x2), + y1: 0, + x2: Math.max(x1, x2), + y2: this.getRowsCount() - 1, + }; + this.selectionRanges = [newRange]; this.broadcastChange(); } @@ -610,4 +832,52 @@ export default class OptimizeTableState { return { isFocus, isSelected, isBorderBottom, isBorderRight }; } + + getSelectionAggregatedResult() { + let sum = undefined; + let avg = undefined; + let min = undefined; + let max = undefined; + let count = 0; + + const selectedCell = new Set(); + for (const range of this.selectionRanges) { + for (let x = range.x1; x <= range.x2; x++) { + for (let y = range.y1; y <= range.y2; y++) { + const key = `${x}-${y}`; + if (selectedCell.has(key)) { + continue; + } + selectedCell.add(key); + + const value = this.getValue(y, x); + const parsed = Number(value); + + if (!isNaN(parsed)) { + sum = sum !== undefined ? sum + parsed : parsed; + min = min !== undefined ? (min < parsed ? min : parsed) : parsed; + max = max !== undefined ? (max > parsed ? max : parsed) : parsed; + } + count = count + 1; + } + } + } + if (sum !== undefined && count > 0) { + avg = sum / count; + } + return { + sum: formatNumber(sum), + avg: formatNumber(avg), + min: formatNumber(min), + max: formatNumber(max), + count: formatNumber(count), + }; + } + + setDefaultAggregateFunction(functionName: AggregateFunction) { + this.defaultAggregateFunction = functionName; + } + getDefaultAggregateFunction() { + return this.defaultAggregateFunction; + } } diff --git a/src/components/gui/table-optimized/index.tsx b/src/components/gui/table-optimized/index.tsx index dce1d338..51b0ecfc 100644 --- a/src/components/gui/table-optimized/index.tsx +++ b/src/components/gui/table-optimized/index.tsx @@ -19,6 +19,7 @@ import { TableColumnDataType, } from "@/drivers/base-driver"; import OptimizeTableCell from "./table-cell"; +import { cn } from "@/lib/utils"; export interface OptimizeTableHeaderProps { name: string; @@ -117,18 +118,43 @@ function renderCellList({ const cells = windowArray.map((row, rowIndex) => { const absoluteRowIndex = rowIndex + rowStart; + let textClass = + "libsql-table-cell flex items-center justify-end h-full pr-2 font-mono"; + let tdClass = "sticky left-0 bg-zinc-100 dark:bg-zinc-900"; + + if (internalState.getSelectedRowIndex().includes(absoluteRowIndex)) { + if (internalState.isFullSelectionRow(absoluteRowIndex)) { + textClass = cn( + "libsql-table-cell flex items-center justify-end h-full pr-2 font-mono", + "bg-blue-600 border-red-900 text-white font-bold" + ); + tdClass = "sticky left-0 bg-blue-600 dark:bg-blue-800"; + } else { + textClass = + "libsql-table-cell flex items-center justify-end h-full pr-2 font-mono text-white font-bold"; + tdClass = "sticky left-0 bg-blue-200 dark:bg-blue-400"; + } + } + return ( { - internalState.selectRow(absoluteRowIndex); + onMouseDown={(e) => { + const focusCell = internalState.getFocus(); + if (e.shiftKey && focusCell) { + internalState.selectRowRange(focusCell.y, absoluteRowIndex); + } else if (e.ctrlKey && focusCell) { + internalState.addSelectionRow(absoluteRowIndex); + internalState.setFocus(absoluteRowIndex, 0); + } else { + internalState.selectRow(absoluteRowIndex); + internalState.setFocus(absoluteRowIndex, 0); + } }} > -
- {absoluteRowIndex + 1} -
+
{absoluteRowIndex + 1}
{hasSticky && ( diff --git a/src/components/gui/table-optimized/table-cell.tsx b/src/components/gui/table-optimized/table-cell.tsx index 92e882d2..5a4a795c 100644 --- a/src/components/gui/table-optimized/table-cell.tsx +++ b/src/components/gui/table-optimized/table-cell.tsx @@ -69,6 +69,12 @@ export default function OptimizeTableCell({ const shiftKey = e.shiftKey; const focusedCell = state.getFocus(); + if (e.button === 2) { + if (state.getCellStatus(rowIndex, colIndex).isSelected) { + return; + } + } + if (shiftKey && focusedCell) { state.selectCellRange( focusedCell.y, @@ -76,6 +82,8 @@ export default function OptimizeTableCell({ rowIndex, colIndex ); + } else if (e.ctrlKey) { + state.addSelectionRange(rowIndex, colIndex, rowIndex, colIndex); } else { state.selectCell(rowIndex, colIndex); } diff --git a/src/components/gui/tabs-result/query-result-tab.tsx b/src/components/gui/tabs-result/query-result-tab.tsx index f87764e4..07e712fa 100644 --- a/src/components/gui/tabs-result/query-result-tab.tsx +++ b/src/components/gui/tabs-result/query-result-tab.tsx @@ -1,10 +1,11 @@ +import { useMemo } from "react"; import { MultipleQueryResult } from "../../lib/multiple-query"; import ExportResultButton from "../export/export-result-button"; import ResultTable from "../query-result-table"; import ResultStats from "../result-stat"; -import { useMemo } from "react"; import OptimizeTableState from "../table-optimized/OptimizeTableState"; import { useDatabaseDriver } from "@/context/driver-provider"; +import AggregateResultButton from "../aggregate-result/aggregate-result-button"; export default function QueryResult({ result, @@ -31,14 +32,16 @@ export default function QueryResult({ {stats && ( -
-
+
+
-
+
+ +
)}
diff --git a/src/components/gui/tabs/table-data-tab.tsx b/src/components/gui/tabs/table-data-tab.tsx index cb9d7904..6f37cb57 100644 --- a/src/components/gui/tabs/table-data-tab.tsx +++ b/src/components/gui/tabs/table-data-tab.tsx @@ -34,12 +34,12 @@ import OpacityLoading from "../loading-opacity"; import OptimizeTableState from "../table-optimized/OptimizeTableState"; import { useDatabaseDriver } from "@/context/driver-provider"; import ResultStats from "../result-stat"; -import isEmptyResultStats from "@/components/lib/empty-stats"; import useTableResultColumnFilter from "../table-result/filter-column"; import { AlertDialogTitle } from "@radix-ui/react-alert-dialog"; import { useCurrentTab } from "../windows-tab"; import { KEY_BINDING } from "@/lib/key-matcher"; import { Toolbar, ToolbarButton } from "../toolbar"; +import AggregateResultButton from "../aggregate-result/aggregate-result-button"; interface TableDataContentProps { tableName: string; @@ -368,10 +368,14 @@ export default function TableDataWindow({ /> ) : null}
- {stat && !isEmptyResultStats(stat) && ( -
- - + {stat && data && ( +
+
+ +
+
+ +
)}
diff --git a/src/lib/convertNumber.ts b/src/lib/convertNumber.ts new file mode 100644 index 00000000..be58c534 --- /dev/null +++ b/src/lib/convertNumber.ts @@ -0,0 +1,8 @@ +export const formatNumber = (n: number | undefined) => { + if (!n) return n; + return new Intl.NumberFormat("en-US", { + style: "decimal", + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }).format(n); +};