From d621b186292f735a53ba7e2f718581407b924510 Mon Sep 17 00:00:00 2001 From: andres Date: Sun, 7 Jul 2024 15:34:22 +0200 Subject: [PATCH] add settings and sidebar toggle, refactor a bit --- frontend/bun.lockb | Bin 129925 -> 132417 bytes frontend/package.json | 5 +- .../components/db-table-view/data-table.tsx | 14 +- frontend/src/components/settings-dialog.tsx | 132 +++++++++++++ .../components/ui/data-table-pagination.tsx | 4 +- frontend/src/components/ui/dialog.tsx | 120 ++++++++++++ frontend/src/components/ui/index.ts | 4 +- frontend/src/components/ui/input.tsx | 25 +++ frontend/src/components/ui/mode-toggle.tsx | 24 ++- frontend/src/components/ui/switch.tsx | 27 +++ frontend/src/components/ui/theme-provider.tsx | 71 ------- frontend/src/index.css | 16 +- frontend/src/lib/create-selectors.ts | 17 ++ frontend/src/main.tsx | 17 +- frontend/src/routes/__root.tsx | 181 +++++++++++------- frontend/src/state/index.ts | 2 + frontend/src/state/settings-store.ts | 47 +++++ frontend/src/state/ui-store.ts | 32 ++++ 18 files changed, 571 insertions(+), 167 deletions(-) create mode 100644 frontend/src/components/settings-dialog.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/switch.tsx delete mode 100644 frontend/src/components/ui/theme-provider.tsx create mode 100644 frontend/src/lib/create-selectors.ts create mode 100644 frontend/src/state/index.ts create mode 100644 frontend/src/state/settings-store.ts create mode 100644 frontend/src/state/ui-store.ts diff --git a/frontend/bun.lockb b/frontend/bun.lockb index e9442af5eb521a03154419864da264bc900e93b0..eea2418f63f4ed42b1c551c41fad6effab22e802 100644 GIT binary patch delta 25590 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 delta 24186 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 diff --git a/frontend/package.json b/frontend/package.json index bd58e03..0cb2a08 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,9 +12,11 @@ }, "dependencies": { "@it-incubator/prettier-config": "^0.1.2", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", "@tanstack/react-query": "^5.50.1", "@tanstack/react-router": "^1.43.12", "@tanstack/react-table": "^8.19.2", @@ -26,7 +28,8 @@ "react-dom": "^18.3.1", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^4.5.4" }, "devDependencies": { "@biomejs/biome": "1.8.3", diff --git a/frontend/src/components/db-table-view/data-table.tsx b/frontend/src/components/db-table-view/data-table.tsx index 973e11a..2e901e7 100644 --- a/frontend/src/components/db-table-view/data-table.tsx +++ b/frontend/src/components/db-table-view/data-table.tsx @@ -14,6 +14,7 @@ import { } from "@/components/ui"; import { cn } from "@/lib/utils"; import { useTableColumnsQuery, useTableDataQuery } from "@/services/db"; +import { useSettingsStore } from "@/state"; import { type ColumnDef, type OnChangeFn, @@ -55,6 +56,9 @@ export const DataTable = ({ onPageIndexChange: (pageIndex: number) => void; onPageSizeChange: (pageSize: number) => void; }) => { + const formatDates = useSettingsStore.use.formatDates(); + const showImagesPreview = useSettingsStore.use.showImagesPreview(); + const [sorting, setSorting] = useState([]); const [columnVisibility, setColumnVisibility] = useState({}); @@ -108,10 +112,10 @@ export const DataTable = ({ cell: ({ row }) => { const value = row.getValue(column_name) as any; let finalValue = value; - if (udt_name === "timestamp") { + if (formatDates && udt_name === "timestamp") { finalValue = new Date(value as string).toLocaleString(); } - if (typeof value === "string" && isUrl(value)) { + if (showImagesPreview && typeof value === "string" && isUrl(value)) { const isImage = isImageUrl(value); return ( -
+
{value} {isImage && ( []; - }, [details]); + }, [details, formatDates, showImagesPreview]); const table = useReactTable({ data: data?.data ?? [], diff --git a/frontend/src/components/settings-dialog.tsx b/frontend/src/components/settings-dialog.tsx new file mode 100644 index 0000000..844cd0d --- /dev/null +++ b/frontend/src/components/settings-dialog.tsx @@ -0,0 +1,132 @@ +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + Input, + Switch, + Table, + TableBody, + TableCell, + TableRow, +} from "@/components/ui"; +import { useSettingsStore } from "@/state"; +import { Settings, Trash } from "lucide-react"; +import type { FormEventHandler } from "react"; + +export function SettingsDialog() { + const addPaginationOption = useSettingsStore.use.addPaginationOption(); + const removePaginationOption = useSettingsStore.use.removePaginationOption(); + const paginationOptions = useSettingsStore.use.paginationOptions(); + + const formatDates = useSettingsStore.use.formatDates(); + const setFormatDates = useSettingsStore.use.setFormatDates(); + + const showImagesPreview = useSettingsStore.use.showImagesPreview(); + const setShowImagesPreview = useSettingsStore.use.setShowImagesPreview(); + + const handleSubmit: FormEventHandler = (e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const stringValue = formData.get("option"); + if (!stringValue || typeof stringValue !== "string") { + return; + } + + const value = Number.parseInt(stringValue, 10); + if (Number.isNaN(value)) { + return; + } + + addPaginationOption(value); + e.currentTarget.reset(); + }; + + return ( + + + + + + + Global Settings + + Update global settings for the app. Changes will be applied + immediately and persisted to local storage. + + +
+
+

Pagination options

+

Add or remove options for the amount of rows per page

+ + + {paginationOptions.map((option) => ( + + + {option} + + + + + + ))} + +
+
+ + +
+
+
+

Automatically format dates

+

+ When turned on, will show timestamp cells in tables in a human + readable format +

+ +
+
+

Show previews for images

+

+ When turned on, will automatically detect image URL's in tables + and add a preview alongside. +

+

+ Might significantly increase load on you CDN, use with caution. +

+ +
+
+ + + + + +
+
+ ); +} diff --git a/frontend/src/components/ui/data-table-pagination.tsx b/frontend/src/components/ui/data-table-pagination.tsx index 7633ca2..2ada205 100644 --- a/frontend/src/components/ui/data-table-pagination.tsx +++ b/frontend/src/components/ui/data-table-pagination.tsx @@ -6,6 +6,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui"; +import { useSettingsStore } from "@/state"; import type { Table } from "@tanstack/react-table"; import { ChevronLeftIcon, @@ -21,6 +22,7 @@ interface DataTablePaginationProps { export function DataTablePagination({ table, }: DataTablePaginationProps) { + const paginationOptions = useSettingsStore.use.paginationOptions(); return (
@@ -40,7 +42,7 @@ export function DataTablePagination({ - {[10, 20, 30, 40, 50, 1000].map((pageSize) => ( + {paginationOptions.map((pageSize) => ( {pageSize} diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..c23630e --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index de391d7..cc8ee6b 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -1,7 +1,9 @@ export * from "./button"; export * from "./data-table-pagination"; +export * from "./dialog"; export * from "./dropdown-menu"; +export * from "./input"; export * from "./mode-toggle"; export * from "./select"; +export * from "./switch"; export * from "./table"; -export * from "./theme-provider"; diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..677d05f --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/frontend/src/components/ui/mode-toggle.tsx b/frontend/src/components/ui/mode-toggle.tsx index 75fd84b..6df9e2c 100644 --- a/frontend/src/components/ui/mode-toggle.tsx +++ b/frontend/src/components/ui/mode-toggle.tsx @@ -6,11 +6,31 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - useTheme, } from "@/components/ui"; +import { useUiStore } from "@/state"; +import { useLayoutEffect } from "react"; export function ModeToggle() { - const { setTheme } = useTheme(); + const theme = useUiStore.use.theme(); + const setTheme = useUiStore.use.setTheme(); + + useLayoutEffect(() => { + const root = window.document.documentElement; + + root.classList.remove("light", "dark"); + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); return ( diff --git a/frontend/src/components/ui/switch.tsx b/frontend/src/components/ui/switch.tsx new file mode 100644 index 0000000..aa58baa --- /dev/null +++ b/frontend/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/frontend/src/components/ui/theme-provider.tsx b/frontend/src/components/ui/theme-provider.tsx deleted file mode 100644 index 4847d98..0000000 --- a/frontend/src/components/ui/theme-provider.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { createContext, useContext, useEffect, useState } from 'react' - -type Theme = 'dark' | 'light' | 'system' - -type ThemeProviderProps = { - children: React.ReactNode - defaultTheme?: Theme - storageKey?: string -} - -type ThemeProviderState = { - theme: Theme - setTheme: (theme: Theme) => void -} - -const initialState: ThemeProviderState = { - theme: 'system', - setTheme: () => null, -} - -const ThemeProviderContext = createContext(initialState) - -export function ThemeProvider({ - children, - defaultTheme = 'system', - storageKey = 'vite-ui-theme', - ...props -}: ThemeProviderProps) { - const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme - ) - - useEffect(() => { - const root = window.document.documentElement - - root.classList.remove('light', 'dark') - - if (theme === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' - - root.classList.add(systemTheme) - return - } - - root.classList.add(theme) - }, [theme]) - - const value = { - theme, - setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme) - setTheme(theme) - }, - } - - return ( - - {children} - - ) -} - -export const useTheme = () => { - const context = useContext(ThemeProviderContext) - - if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider') - - return context -} diff --git a/frontend/src/index.css b/frontend/src/index.css index 8862324..0943dc8 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,6 +3,9 @@ @tailwind utilities; @layer base { + input[type=number] { + -moz-appearance:textfield; + } :root { --background: 0 0% 100%; --foreground: 240 10% 3.9%; @@ -69,21 +72,28 @@ @layer base { :root { text-underline-position: under; + --sidebar-width: 264px; } + + .sidebar-closed { + --sidebar-width: 0; + } + .grid-rows-layout { grid-template-rows: 60px 1fr; } .grid-cols-layout { - grid-template-columns: 264px 1fr; + grid-template-columns: var(--sidebar-width) 1fr; } + .max-w-layout { - max-width: calc(100vw - 264px); + max-width: calc(100vw - var(--sidebar-width)); } .w-layout { - width: calc(100vw - 264px); + width: calc(100vw - var(--sidebar-width)); } .resizer { position: absolute; diff --git a/frontend/src/lib/create-selectors.ts b/frontend/src/lib/create-selectors.ts new file mode 100644 index 0000000..106e8af --- /dev/null +++ b/frontend/src/lib/create-selectors.ts @@ -0,0 +1,17 @@ +import type { StoreApi, UseBoundStore } from "zustand"; + +type WithSelectors = S extends { getState: () => infer T } + ? S & { use: { [K in keyof T]: () => T[K] } } + : never; + +export const createSelectors = >>( + _store: S, +) => { + const store = _store as WithSelectors; + store.use = {}; + for (const k of Object.keys(store.getState())) { + (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]); + } + + return store; +}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 1fbb748..34064fa 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,7 +2,6 @@ import { RouterProvider, createRouter } from "@tanstack/react-router"; import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; -import { ThemeProvider } from "@/components/ui"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // Import the generated route tree @@ -25,15 +24,13 @@ if (rootElement && !rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( - - - - - - + + + + , ); } diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index fc4e4fd..0a8ac22 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,4 +1,6 @@ +import { SettingsDialog } from "@/components/settings-dialog"; import { + Button, ModeToggle, Select, SelectContent, @@ -9,6 +11,7 @@ import { } from "@/components/ui"; import { cn } from "@/lib/utils"; import { useDatabasesListQuery, useTablesListQuery } from "@/services/db"; +import { useUiStore } from "@/state"; import { Link, Outlet, @@ -17,15 +20,23 @@ import { useParams, } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/router-devtools"; -import { Database, Rows3, Table2 } from "lucide-react"; +import { + Database, + PanelLeft, + PanelLeftClose, + Rows3, + Table2, +} from "lucide-react"; export const Route = createRootRoute({ component: Root, }); function Root() { - const { data } = useDatabasesListQuery(); + const showSidebar = useUiStore.use.showSidebar(); + const toggleSidebar = useUiStore.use.toggleSidebar(); + const { data } = useDatabasesListQuery(); const params = useParams({ strict: false }); const dbName = params.dbName ?? ""; const navigate = useNavigate({ from: Route.fullPath }); @@ -38,81 +49,103 @@ function Root() { return ( <> -
-
- - - Home - +
+
+
+ + + Home + +
+
+ + +
+
diff --git a/frontend/src/state/index.ts b/frontend/src/state/index.ts new file mode 100644 index 0000000..95cd937 --- /dev/null +++ b/frontend/src/state/index.ts @@ -0,0 +1,2 @@ +export * from "./settings-store"; +export * from "./ui-store"; diff --git a/frontend/src/state/settings-store.ts b/frontend/src/state/settings-store.ts new file mode 100644 index 0000000..17dd5ed --- /dev/null +++ b/frontend/src/state/settings-store.ts @@ -0,0 +1,47 @@ +import { createSelectors } from "@/lib/create-selectors"; +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +type SettingsState = { + paginationOptions: Array; + addPaginationOption: (option: number) => void; + removePaginationOption: (option: number) => void; + formatDates: boolean; + setFormatDates: (value: boolean) => void; + showImagesPreview: boolean; + setShowImagesPreview: (value: boolean) => void; +}; + +const useSettingsStoreBase = create()( + persist( + (set) => ({ + paginationOptions: [10, 20, 50, 100], + addPaginationOption: (option) => + set((state) => { + if (state.paginationOptions.includes(option)) { + return state; + } + return { + paginationOptions: [...state.paginationOptions, option].sort( + (a, b) => a - b, + ), + }; + }), + removePaginationOption: (option) => + set((state) => ({ + paginationOptions: state.paginationOptions.filter( + (o) => o !== option, + ), + })), + formatDates: true, + setFormatDates: (value) => set({ formatDates: value }), + showImagesPreview: true, + setShowImagesPreview: (value) => set({ showImagesPreview: value }), + }), + { + name: "settings-storage", + }, + ), +); + +export const useSettingsStore = createSelectors(useSettingsStoreBase); diff --git a/frontend/src/state/ui-store.ts b/frontend/src/state/ui-store.ts new file mode 100644 index 0000000..46e70b2 --- /dev/null +++ b/frontend/src/state/ui-store.ts @@ -0,0 +1,32 @@ +import { createSelectors } from "@/lib/create-selectors"; +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +type Theme = "dark" | "light" | "system"; + +type UiState = { + showSidebar: boolean; + toggleSidebar: () => void; + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const useUiStoreBase = create()( + persist( + (set) => { + return { + showSidebar: false, + toggleSidebar: () => { + set((state) => ({ showSidebar: !state.showSidebar })); + }, + theme: "system", + setTheme: (theme) => set({ theme }), + }; + }, + { + name: "ui-storage", + }, + ), +); + +export const useUiStore = createSelectors(useUiStoreBase);