Skip to content

Commit

Permalink
feat:
Browse files Browse the repository at this point in the history
- Add aggregated result
- Merge selection
- Improve selection loop
  • Loading branch information
keppere committed Dec 24, 2024
1 parent 09acbaa commit 3288bc8
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 38 deletions.
129 changes: 129 additions & 0 deletions src/components/gui/aggregate-result/aggregate-result-button.tsx
Original file line number Diff line number Diff line change
@@ -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<AggregateResult>({
sum: undefined,
avg: undefined,
min: undefined,
max: undefined,
count: undefined,
});

const [defaultFunction, setDefaultFunction] = useState<AggregateFunction>(
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 (
<Popover>
<PopoverTrigger>
<div className={buttonVariants({ variant: "ghost", size: "sm" })}>
{displayResult}{" "}
{!!displayResult && <LucideChevronDown className="w-4 h-4 ml-2" />}
</div>
</PopoverTrigger>
<PopoverContent className="p-0">
<div className="flex flex-col p-4">
{!!result.sum && (
<ListButtonItem
text={"SUM: " + result.sum}
icon={defaultFunction === "sum" ? LucideCheck : undefined}
onClick={function (): void {
handleSetDefaultFunction("sum");
}}
/>
)}
{!!result.avg && (
<ListButtonItem
text={"AVG: " + result.avg}
icon={defaultFunction === "avg" ? LucideCheck : undefined}
onClick={function (): void {
handleSetDefaultFunction("avg");
}}
/>
)}
{!!result.max && (
<ListButtonItem
text={"MAX: " + result.max}
icon={defaultFunction === "max" ? LucideCheck : undefined}
onClick={function (): void {
handleSetDefaultFunction("max");
}}
/>
)}
{!!result.min && (
<ListButtonItem
text={"MIN: " + result.min}
icon={defaultFunction === "min" ? LucideCheck : undefined}
onClick={function (): void {
handleSetDefaultFunction("min");
}}
/>
)}
{!!result.count && (
<ListButtonItem
text={"COUNT: " + result.count}
icon={defaultFunction === "count" ? LucideCheck : undefined}
onClick={function (): void {
handleSetDefaultFunction("count");
}}
/>
)}
</div>
</PopoverContent>
</Popover>
);
}
8 changes: 6 additions & 2 deletions src/components/gui/list-button-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function ListButtonItem({
}: Readonly<{
selected?: boolean;
text: string;
icon: Icon;
icon?: Icon;
onClick: () => void;
}>) {
return (
Expand All @@ -25,7 +25,11 @@ export default function ListButtonItem({
"cursor-pointer"
)}
>
<Icon className="w-4 h-4 mr-2" />
{Icon ? (
<Icon className="w-4 h-4 mr-2" />
) : (
<div className="w-4 h-4 mr-2"></div>
)}
{text}
</button>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/gui/result-stat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function ResultStats({ stats }: { stats: DatabaseResultStat }) {

{!!stats.rowsAffected && (
<div className="px-2 border-r">
<span className="font-semibold">Affected Rows</span>:{" "}
<span className="font-semibold text-xs">Affected Rows</span>:{" "}
{stats.rowsAffected}
</div>
)}
Expand Down
92 changes: 73 additions & 19 deletions src/components/gui/table-optimized/OptimizeTableState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
Expand All @@ -18,6 +19,8 @@ export interface OptimizeTableRowValue {
isRemoved?: boolean;
}

export type AggregateFunction = "sum" | "avg" | "min" | "max" | "count";

type TableChangeEventCallback = (state: OptimizeTableState) => void;

interface TableSelectionRange {
Expand Down Expand Up @@ -54,6 +57,7 @@ export default class OptimizeTableState {

protected changeCounter = 1;
protected changeLogs: Record<number, OptimizeTableRowValue> = {};
protected defaultAggregateFunction: AggregateFunction = "sum";

static createFromResult(
driver: BaseDriver,
Expand Down Expand Up @@ -194,6 +198,36 @@ 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();
}

// ------------------------------------------------
// Handle headers and data
// ------------------------------------------------
Expand Down Expand Up @@ -571,26 +605,28 @@ export default class OptimizeTableState {

isFullSelectionRow(y: number) {
for (const range of this.selectionRanges) {
for (let i = range.y1; i <= range.y2; i++) {
if (
i === y &&
range.x1 === 0 &&
range.x2 === this.getHeaderCount() - 1
) {
return true;
}
}
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) {
for (let i = range.x1; i <= range.x2; i++) {
if (i === x && range.y1 === 0 && range.y2 === this.getRowsCount() - 1) {
return true;
}
}
if (
range.x1 <= x &&
range.x2 >= x &&
range.y1 === 0 &&
range.y2 === this.getRowsCount() - 1
)
return true;
}
return false;
}

selectRow(y: number) {
Expand Down Expand Up @@ -648,6 +684,7 @@ export default class OptimizeTableState {

if (!this.findSelectionRange(newRange)) {
this.selectionRanges.push(newRange);
this.mergeSelectionRanges();
this.broadcastChange();
}
}
Expand All @@ -662,6 +699,7 @@ export default class OptimizeTableState {

if (!this.findSelectionRange(newRange)) {
this.selectionRanges.push(newRange);
this.mergeSelectionRanges();
this.broadcastChange();
}
}
Expand All @@ -676,6 +714,7 @@ export default class OptimizeTableState {

if (!this.findSelectionRange(newRange)) {
this.selectionRanges.push(newRange);
this.mergeSelectionRanges();
this.broadcastChange();
}
}
Expand Down Expand Up @@ -755,9 +794,17 @@ export default class OptimizeTableState {
let min = undefined;
let max = undefined;
let count = 0;

const selectedCell = new Set<string>();
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);

Expand All @@ -774,11 +821,18 @@ export default class OptimizeTableState {
avg = sum / count;
}
return {
sum,
avg,
min,
max,
count,
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;
}
}
18 changes: 7 additions & 11 deletions src/components/gui/tabs-result/query-result-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +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 { useEffect, useMemo, useState } from "react";
import OptimizeTableState from "../table-optimized/OptimizeTableState";
import { useDatabaseDriver } from "@/context/driver-provider";

export interface AggregateResult {
sum: number | undefined;
avg: number | undefined;
min: number | undefined;
max: number | undefined;
count: number | undefined;
}
import AggregateResultButton from "../aggregate-result/aggregate-result-button";

export default function QueryResult({
result,
Expand All @@ -39,13 +32,16 @@ export default function QueryResult({
<ResultTable data={data} />
</div>
{stats && (
<div className="shrink-0">
<div className="flex p-1 border-t">
<div className="flex justify-between border-t shrink-0">
<div className="flex p-1 ">
<ResultStats stats={stats} />
<div>
<ExportResultButton data={data} />
</div>
</div>
<div className="p-1 pr-3">
<AggregateResultButton data={data} />
</div>
</div>
)}
</div>
Expand Down
Loading

0 comments on commit 3288bc8

Please sign in to comment.