From d6896f4197812027ec16b76f3e979417f31045e3 Mon Sep 17 00:00:00 2001 From: Sokphal Adam <31284849+sokphaladam@users.noreply.github.com> Date: Sat, 1 Feb 2025 00:26:53 +0700 Subject: [PATCH] Board layout re-arrangement (#324) * feat: Add @types/react-grid-layout dependency * refactor: Simplify Board component props and update layout handling * add board tool component and integrate with board layout * remove unneccessary load * add board canvas component and implement animation for reveal menu --------- Co-authored-by: sokphaladam Co-authored-by: Visal .In --- package-lock.json | 75 +++++- package.json | 2 + src/app/storybook/board/page.tsx | 32 +++ src/app/storybook/layout.tsx | 10 +- src/components/board/board-canvas.tsx | 104 ++++++++ src/components/board/board-filter-dialog.tsx | 192 +++++++++++++++ src/components/board/board-filter.tsx | 246 +++++++++++++++++++ src/components/board/board-style.css | 139 +++++++++++ src/components/board/board-tool.tsx | 38 +++ src/components/board/index.tsx | 51 ++++ src/components/gui/toolbar.tsx | 2 +- 11 files changed, 884 insertions(+), 7 deletions(-) create mode 100644 src/app/storybook/board/page.tsx create mode 100644 src/components/board/board-canvas.tsx 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-style.css create mode 100644 src/components/board/board-tool.tsx create mode 100644 src/components/board/index.tsx diff --git a/package-lock.json b/package-lock.json index aa287fb5..83989cf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "@tiptap/core": "^2.3.0", "@tiptap/react": "^2.3.0", "@types/mdx": "^2.0.13", + "@types/react-grid-layout": "^1.3.5", "@uiw/codemirror-extensions-langs": "^4.21.24", "@uiw/codemirror-themes": "^4.21.21", "@uiw/react-codemirror": "^4.21.21", @@ -77,6 +78,7 @@ "oslo": "^1.1.3", "react": "19.0.0", "react-dom": "19.0.0", + "react-grid-layout": "^1.5.0", "react-resizable-panels": "^2.1.7", "sonner": "^1.4.41", "sql-formatter": "^15.3.2", @@ -6703,6 +6705,14 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-grid-layout": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz", + "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -10486,6 +10496,11 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -13114,7 +13129,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "devOptional": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -13684,7 +13698,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -14977,7 +14990,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15637,7 +15649,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -15649,7 +15660,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/property-information": { @@ -15991,6 +16001,44 @@ "react": "^19.0.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-grid-layout": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.0.tgz", + "integrity": "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==", + "dependencies": { + "clsx": "^2.0.0", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.5", + "react-resizable": "^3.0.5", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -16046,6 +16094,18 @@ } } }, + "node_modules/react-resizable": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", + "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + }, + "peerDependencies": { + "react": ">= 16.3" + } + }, "node_modules/react-resizable-panels": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", @@ -16312,6 +16372,11 @@ "dev": true, "license": "MIT" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", diff --git a/package.json b/package.json index a73f3918..3fb9fa50 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@tiptap/core": "^2.3.0", "@tiptap/react": "^2.3.0", "@types/mdx": "^2.0.13", + "@types/react-grid-layout": "^1.3.5", "@uiw/codemirror-extensions-langs": "^4.21.24", "@uiw/codemirror-themes": "^4.21.21", "@uiw/react-codemirror": "^4.21.21", @@ -97,6 +98,7 @@ "oslo": "^1.1.3", "react": "19.0.0", "react-dom": "19.0.0", + "react-grid-layout": "^1.5.0", "react-resizable-panels": "^2.1.7", "sonner": "^1.4.41", "sql-formatter": "^15.3.2", diff --git a/src/app/storybook/board/page.tsx b/src/app/storybook/board/page.tsx new file mode 100644 index 00000000..df14a4a0 --- /dev/null +++ b/src/app/storybook/board/page.tsx @@ -0,0 +1,32 @@ +"use client"; +import Board from "@/components/board"; +import { BoardFilterProps } from "@/components/board/board-filter-dialog"; +import { useState } from "react"; +import ReactGridLayout from "react-grid-layout"; + +interface DashboardProps { + layout: ReactGridLayout.Layout[]; + data: { + filters: BoardFilterProps[]; + }; +} + +export default function StorybookBoardPage() { + 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 ( +
+ +
+ ); +} diff --git a/src/app/storybook/layout.tsx b/src/app/storybook/layout.tsx index ca160b26..1cc1c998 100644 --- a/src/app/storybook/layout.tsx +++ b/src/app/storybook/layout.tsx @@ -1,6 +1,7 @@ import { SidebarMenuHeader, SidebarMenuItem } from "@/components/sidebar-menu"; import ThemeToggle from "@/components/theme-toggle"; import { Separator } from "@/components/ui/separator"; +import { TooltipProvider } from "@/components/ui/tooltip"; import { Component, Layers2 } from "lucide-react"; import ThemeLayout from "../(theme)/theme_layout"; @@ -56,8 +57,15 @@ export default function StorybookRootLayout({ text="Chart" href="/storybook/chart" /> + + +
+ {children}
-
{children}
); diff --git a/src/components/board/board-canvas.tsx b/src/components/board/board-canvas.tsx new file mode 100644 index 00000000..388a7fca --- /dev/null +++ b/src/components/board/board-canvas.tsx @@ -0,0 +1,104 @@ +"use client"; +import { cn } from "@/lib/utils"; +import { RectangleHorizontal, Square } from "lucide-react"; +import { useCallback } from "react"; +import RGL, { WidthProvider } from "react-grid-layout"; +import { buttonVariants } from "../ui/button"; +import "./board-style.css"; + +export interface BoardChartLayout { + x: number; + y: number; + w: number; + h: number; + i: number; +} + +interface BoardProps { + layout: ReactGridLayout.Layout[]; + onChange: (v: ReactGridLayout.Layout[]) => void; + editMode?: "ADD_CHART" | "REARRANGING_CHART" | null; +} + +const ReactGridLayout = WidthProvider(RGL); + +export function BoardCanvas(props: BoardProps) { + const sizes = [ + { w: 1, h: 1, name: "1", icon: }, + { + w: 2, + h: 1, + name: "2", + icon: , + }, + { w: 2, h: 2, name: "3", icon: }, + { + w: 4, + h: 2, + name: "4", + icon: , + }, + ]; + + const handleClickResize = useCallback( + (w: number, h: number, index: number) => { + const dummy = structuredClone(props.layout); + dummy[index].w = w; + dummy[index].h = h; + props.onChange(dummy); + }, + [props] + ); + + const mapItem: JSX.Element[] = [...Array(props.layout.length)].map((_, i) => { + return ( +
+ {props.editMode === "REARRANGING_CHART" && ( +
+ {sizes.map((x, index) => { + return ( + + ); + })} +
+ )} +
{i}
+
+ ); + }); + + return ( +
+ + {mapItem} + +
+ ); +} 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..368f2f96 --- /dev/null +++ b/src/components/board/board-filter.tsx @@ -0,0 +1,246 @@ +"use client"; +import { + CalendarDays, + Check, + Ellipsis, + ListFilter, + ListOrdered, + Search, +} from "lucide-react"; +import { useCallback, useState } from "react"; +import { buttonVariants } from "../ui/button"; +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"; +import { BoardTool } from "./board-tool"; + +interface Props { + filters: BoardFilterProps[]; + onFilters: (f: BoardFilterProps[]) => void; + editMode: "ADD_CHART" | "REARRANGING_CHART" | null; + setEditMode: (v: "ADD_CHART" | "REARRANGING_CHART") => 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 = structuredClone(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 = structuredClone(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 = structuredClone(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 = structuredClone(props.filters); + data[selectIndex] = v; + props.onFilters(data); + }} + onAddFilter={() => { + const data = structuredClone(props.filters); + data[selectIndex].new = false; + setOpen(false); + props.onFilters(data); + }} + /> + )} +
+ {mapFilterItem} + +
+
+ + ); +} diff --git a/src/components/board/board-style.css b/src/components/board/board-style.css new file mode 100644 index 00000000..8fa084e3 --- /dev/null +++ b/src/components/board/board-style.css @@ -0,0 +1,139 @@ +.react-grid-layout { + position: relative; + transition: height 200ms ease; +} +.react-grid-item { + transition: all 200ms ease; + transition-property: left, top, width, height; +} +.react-grid-item img { + pointer-events: none; + user-select: none; +} +.react-grid-item.cssTransforms { + transition-property: transform, width, height; +} +.react-grid-item.resizing { + transition: none; + z-index: 1; + will-change: width, height; +} + +.react-grid-item.react-draggable-dragging { + transition: none; + z-index: 3; + will-change: transform; +} + +.react-grid-item.dropping { + visibility: hidden; +} + +.react-grid-item.react-grid-placeholder { + background: #c9c9c9; + opacity: 0.2; + transition-duration: 100ms; + z-index: 2; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.react-grid-item.react-grid-placeholder.placeholder-resizing { + transition: none; +} + +.react-grid-item > .react-resizable-handle { + position: absolute; + width: 20px; + height: 20px; +} + +.react-grid-item > .react-resizable-handle::after { + content: ""; + position: absolute; + right: 3px; + bottom: 3px; + width: 5px; + height: 5px; + border-right: 2px solid rgba(0, 0, 0, 0.4); + border-bottom: 2px solid rgba(0, 0, 0, 0.4); +} + +.react-resizable-hide > .react-resizable-handle { + display: none; +} + +.react-grid-item > .react-resizable-handle.react-resizable-handle-sw { + bottom: 0; + left: 0; + cursor: sw-resize; + transform: rotate(90deg); +} +.react-grid-item > .react-resizable-handle.react-resizable-handle-se { + bottom: 0; + right: 0; + cursor: se-resize; +} +.react-grid-item > .react-resizable-handle.react-resizable-handle-nw { + top: 0; + left: 0; + cursor: nw-resize; + transform: rotate(180deg); +} +.react-grid-item > .react-resizable-handle.react-resizable-handle-ne { + top: 0; + right: 0; + cursor: ne-resize; + transform: rotate(270deg); +} +.react-grid-item > .react-resizable-handle.react-resizable-handle-w, +.react-grid-item > .react-resizable-handle.react-resizable-handle-e { + top: 50%; + margin-top: -10px; + cursor: ew-resize; +} +.react-grid-item > .react-resizable-handle.react-resizable-handle-w { + left: 0; + transform: rotate(135deg); +} +.react-grid-item > .react-resizable-handle.react-resizable-handle-e { + right: 0; + transform: rotate(315deg); +} +.react-grid-item > .react-resizable-handle.react-resizable-handle-n, +.react-grid-item > .react-resizable-handle.react-resizable-handle-s { + left: 50%; + margin-left: -10px; + cursor: ns-resize; +} +.react-grid-item > .react-resizable-handle.react-resizable-handle-n { + top: 0; + transform: rotate(225deg); +} +.react-grid-item > .react-resizable-handle.react-resizable-handle-s { + bottom: 0; + transform: rotate(45deg); +} + +.reveal { + opacity: 0; + transform: translateY(20px); +} + +.animate-revealMenu { + animation: revealMenu 1s cubic-bezier(0.7, 0, 0, 1.2) forwards; +} + +@keyframes revealMenu { + 0% { + opacity: 100%; + transform: translateY(100px) translateX(-40%); + } + 100% { + opacity: 100%; + transform: translateY(0) translateX(-40%); + } +} diff --git a/src/components/board/board-tool.tsx b/src/components/board/board-tool.tsx new file mode 100644 index 00000000..8c1a4ec6 --- /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 new file mode 100644 index 00000000..e625ef8d --- /dev/null +++ b/src/components/board/index.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { BoardCanvas } from "./board-canvas"; +import { BoardFilter } from "./board-filter"; +import { BoardFilterProps } from "./board-filter-dialog"; + +interface DashboardProps { + layout: ReactGridLayout.Layout[]; + data: { + filters: BoardFilterProps[]; + }; +} + +interface Props { + value: DashboardProps; + setValue: (value: DashboardProps) => void; +} + +export default function Board({ value, setValue }: Props) { + const [editMode, setEditMode] = useState< + "ADD_CHART" | "REARRANGING_CHART" | null + >(null); + + return ( +
+ + setValue({ + ...value, + data: { + ...value.data, + filters: v, + }, + }) + } + editMode={editMode} + setEditMode={setEditMode} + /> + { + setValue({ + ...value, + layout: v, + }); + }} + editMode={editMode} + /> +
+ ); +} 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 && (