From 2e61a66dad8908f24fd486da448d55b06befbf35 Mon Sep 17 00:00:00 2001 From: sokphaladam Date: Fri, 31 Jan 2025 18:06:35 +0700 Subject: [PATCH] add board tool component and integrate with board layout --- src/app/storybook/board/page.tsx | 56 ++++- src/app/storybook/layout.tsx | 6 +- src/components/board/board-filter-dialog.tsx | 192 +++++++++++++++ src/components/board/board-filter.tsx | 240 +++++++++++++++++++ src/components/board/board-tool.tsx | 38 +++ src/components/board/index.tsx | 95 ++++---- src/components/gui/toolbar.tsx | 2 +- 7 files changed, 568 insertions(+), 61 deletions(-) create mode 100644 src/components/board/board-filter-dialog.tsx create mode 100644 src/components/board/board-filter.tsx create mode 100644 src/components/board/board-tool.tsx diff --git a/src/app/storybook/board/page.tsx b/src/app/storybook/board/page.tsx index dcf2dcd1..53305925 100644 --- a/src/app/storybook/board/page.tsx +++ b/src/app/storybook/board/page.tsx @@ -1,19 +1,59 @@ "use client"; import Board from "@/components/board"; +import { BoardFilter } from "@/components/board/board-filter"; +import { BoardFilterProps } from "@/components/board/board-filter-dialog"; +import { BoardTool } from "@/components/board/board-tool"; import { useState } from "react"; import ReactGridLayout from "react-grid-layout"; +interface DashboardProps { + layout: ReactGridLayout.Layout[]; + data: { + filters: BoardFilterProps[]; + }; +} + export default function StorybookBoardPage() { - const [layout, setLayout] = useState([ - { x: 0, y: 0, w: 1, h: 1, i: "0" }, - { x: 1, y: 0, w: 1, h: 1, i: "1" }, - { x: 2, y: 0, w: 1, h: 1, i: "2" }, - { x: 3, y: 0, w: 1, h: 1, i: "3" }, - ]); + const [editMode, setEditMode] = useState< + "ADD_CHART" | "REARRANGING_CHART" | null + >(null); + const [value, setValue] = useState({ + layout: [ + { x: 0, y: 0, w: 1, h: 1, i: "0" }, + { x: 1, y: 0, w: 1, h: 1, i: "1" }, + { x: 2, y: 0, w: 1, h: 1, i: "2" }, + { x: 3, y: 0, w: 1, h: 1, i: "3" }, + ], + data: { filters: [] }, + }); + + console.log(value); return ( -
- +
+ + setValue({ + ...value, + data: { + ...value.data, + filters: v, + }, + }) + } + /> + + + setValue({ + ...value, + layout: v, + }) + } + editMode={editMode} + />
); } diff --git a/src/app/storybook/layout.tsx b/src/app/storybook/layout.tsx index 5210e51c..c350c5b7 100644 --- a/src/app/storybook/layout.tsx +++ b/src/app/storybook/layout.tsx @@ -1,5 +1,6 @@ import { SidebarMenuHeader, SidebarMenuItem } from "@/components/sidebar-menu"; import { Separator } from "@/components/ui/separator"; +import { TooltipProvider } from "@/components/ui/tooltip"; import { Component, Layers2 } from "lucide-react"; import StorybookThemeSwitcher from "./storybook-theme-switcher"; @@ -55,8 +56,11 @@ export default function StorybookRootLayout({ text="Chart" href="/storybook/chart" /> + +
+
+ {children}
-
{children}
); diff --git a/src/components/board/board-filter-dialog.tsx b/src/components/board/board-filter-dialog.tsx new file mode 100644 index 00000000..de3efcce --- /dev/null +++ b/src/components/board/board-filter-dialog.tsx @@ -0,0 +1,192 @@ +import { useCallback } from "react"; +import { Button } from "../ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { Input } from "../ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; + +export interface BoardFilterProps { + type: string; + name: string; + default_value: string; + value: string; + new?: boolean; +} + +interface Props { + onClose?: () => void; + filter: BoardFilterProps; + onFilter: (v: BoardFilterProps) => void; + onAddFilter?: (v: BoardFilterProps) => void; +} + +const DEFAULT_EMPTY = { + type: "search", + name: "", + default_value: "", + value: "", +}; + +export const DEFAULT_DATE_FILTER = [ + "Not timeframe override", + "Custom date range", + "Last 24 hours", + "Today", + "Yesterday", + "This week", + "This month", + "Last 7 days", + "Last 30 days", + "Last 90 days", +]; + +export function BoardFilterDialog(props: Props) { + let default_value = [...DEFAULT_DATE_FILTER]; + + if (props.filter.type === "enum" && !!props.filter.value) { + default_value = [...props.filter.value.split(",")]; + } + + let allowAddFilter = !!props.filter.name; + + if (props.filter.type === "enum") { + allowAddFilter = !!props.filter.name && !!props.filter.value; + } + + const onAddFilter = useCallback(() => { + props.onAddFilter && props.onAddFilter(props.filter); + }, [props]); + + return ( + + + + New Filter + +
+
+
Select filter type
+ +
+
+
Filter name*
+ + props.onFilter({ ...props.filter, name: v.target.value }) + } + /> +
+ {props.filter.type === "enum" && ( +
+
+ Values* +
+ + Enter values separated by comma + +
+
+ + props.onFilter({ ...props.filter, value: v.target.value }) + } + /> +
+ )} +
+
+ Default value (optional) +
+ + If this field is left empty, no filter will be applied by + default + +
+
+ {props.filter.type === "search" ? ( + + props.onFilter({ + ...props.filter, + default_value: v.target.value, + }) + } + /> + ) : ( + + )} +
+ {props.filter.type !== "date" && !!props.filter.name && ( +
+ {`Use the variable {{ ${props.filter.name} }} in your charts SQL queries.`} +
+ )} +
+ + + + +
+
+ ); +} diff --git a/src/components/board/board-filter.tsx b/src/components/board/board-filter.tsx new file mode 100644 index 00000000..0a8603a4 --- /dev/null +++ b/src/components/board/board-filter.tsx @@ -0,0 +1,240 @@ +"use client"; +import { + CalendarDays, + Check, + Ellipsis, + ListFilter, + ListOrdered, + Search, +} from "lucide-react"; +import { useCallback, useState } from "react"; +import { Toolbar, ToolbarButton } from "../gui/toolbar"; +import { Checkbox } from "../ui/checkbox"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { + BoardFilterDialog, + BoardFilterProps, + DEFAULT_DATE_FILTER, +} from "./board-filter-dialog"; + +interface Props { + filters: BoardFilterProps[]; + onFilters: (f: BoardFilterProps[]) => void; +} + +export function BoardFilter(props: Props) { + const [open, setOpen] = useState(false); + const [selectIndex, setSelectIndex] = useState(undefined); + + const onFilter = useCallback(() => { + const data = [ + ...props.filters, + { + type: "search", + default_value: "", + value: "", + name: "", + new: true, + }, + ]; + props.onFilters(data); + setSelectIndex(data.length - 1); + setOpen(true); + }, [props]); + + const mapFilterItem = props.filters.map((x, i) => { + const icon = + x.type === "search" ? ( + + ) : x.type === "enum" ? ( + + ) : ( + + ); + const input = + x.type === "search" ? ( + { + const data = [...props.filters]; + data[i].default_value = v.target.value; + props.onFilters(data); + }} + className="max-w-14 outline-0" + /> + ) : x.type === "enum" ? ( +
+ + +
+
{x.default_value || `Select ${x.name}`}
+
+
+ + {x.value.split(",").map((v, idx) => { + return ( +
+ { + const value = x.default_value.split(","); + const data = [...props.filters]; + + if (checked) { + data[i].default_value = [...value, v] + .filter((f) => !!f) + .join(","); + } else { + value.filter((f) => f !== v); + data[i].default_value = value + .filter((f) => f !== v) + .join(","); + } + props.onFilters(data); + }} + /> + +
+ ); + })} +
+
+
+ ) : ( + + +
+
{x.default_value || `Select ${x.name}`}
+
+
+ + {DEFAULT_DATE_FILTER.map((date) => { + return ( + { + const data = [...props.filters]; + data[i].default_value = date; + props.onFilters(data); + }} + > +
+ {date} + {date === x.default_value && } +
+
+ ); + })} +
+
+ ); + return ( +
+
+ {icon} + {x.name} +
+
+ {input} +
+ + +
+ +
+
+ + { + setSelectIndex(i); + setOpen(true); + }} + > + Edit filter + + { + props.onFilters([ + ...props.filters.filter((_, idx) => idx !== i), + ]); + }} + > + Remove + + +
+
+ ); + }); + + return ( +
+ {open && selectIndex !== undefined && ( + { + setOpen(false); + if (props.filters[selectIndex].new === true) { + props.onFilters([ + ...props.filters.filter((_, i) => i !== selectIndex), + ]); + setSelectIndex(undefined); + } + }} + filter={props.filters[selectIndex]} + onFilter={(v) => { + const data = [...props.filters]; + data[selectIndex] = v; + props.onFilters(data); + }} + onAddFilter={() => { + const data = [...props.filters]; + data[selectIndex].new = false; + setOpen(false); + props.onFilters(data); + }} + /> + )} + + {mapFilterItem} + } + text="" + tooltip={"Filter"} + onClick={onFilter} + /> + +
+ ); +} diff --git a/src/components/board/board-tool.tsx b/src/components/board/board-tool.tsx new file mode 100644 index 00000000..14a2c58b --- /dev/null +++ b/src/components/board/board-tool.tsx @@ -0,0 +1,38 @@ +import { ChartLine, ImageUpscale } from "lucide-react"; +import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; + +interface Props { + editMode: "ADD_CHART" | "REARRANGING_CHART" | null; + setEditMode: (v: "ADD_CHART" | "REARRANGING_CHART") => void; +} + +export function BoardTool(props: Props) { + return ( +
+ + + + + + + Add Chart + + + + + + + + Rearranging charts + + + +
+ ); +} diff --git a/src/components/board/index.tsx b/src/components/board/index.tsx index fd8ad1db..83ce8978 100644 --- a/src/components/board/index.tsx +++ b/src/components/board/index.tsx @@ -17,18 +17,19 @@ export interface BoardChartLayout { interface BoardProps { layout: ReactGridLayout.Layout[]; onChange: (v: ReactGridLayout.Layout[]) => void; + editMode?: "ADD_CHART" | "REARRANGING_CHART" | null; } const ReactGridLayout = WidthProvider(RGL); export default function Board(props: BoardProps) { - const [layout, setLayout] = useState([]); + const [loading, setLoading] = useState(true); useEffect(() => { - if (props.layout) { - setLayout(props.layout); + if (props.layout.length > 0 && !!loading) { + setLoading(false); } - }, [props]); + }, [props, loading]); const sizes = [ { w: 1, h: 1, name: "1", icon: }, @@ -49,6 +50,7 @@ export default function Board(props: BoardProps) { const handleClickResize = useCallback( (w: number, h: number, index: number) => { + setLoading(true); const dummy = [...props.layout]; dummy[index].w = w; dummy[index].h = h; @@ -61,29 +63,31 @@ export default function Board(props: BoardProps) { return (
-
- {sizes.map((x, index) => { - return ( - - ); - })} -
+ {props.editMode === "REARRANGING_CHART" && ( +
+ {sizes.map((x, index) => { + return ( + + ); + })} +
+ )}
{i}
); @@ -91,32 +95,21 @@ export default function Board(props: BoardProps) { return (
-
-
Display as [x, y, w, h]:
-
- {props.layout.map((l) => { - return ( -
- {l.i} - {`: [${l.x}, ${l.y}, ${l.w}, ${l.h}]`} -
- ); - })} -
-
- - {mapItem} - + {!loading && ( + + {mapItem} + + )}
); } diff --git a/src/components/gui/toolbar.tsx b/src/components/gui/toolbar.tsx index a4d60eff..564e4514 100644 --- a/src/components/gui/toolbar.tsx +++ b/src/components/gui/toolbar.tsx @@ -51,7 +51,7 @@ export function ToolbarButton({ onClick={onClick} > {loading ? : icon} - {text} + {text && {text}} {badge && (