From dba614fcc9029c263c510d2cfbb39d35420b5fae Mon Sep 17 00:00:00 2001 From: Kazi Date: Fri, 6 Dec 2024 08:00:01 +0600 Subject: [PATCH] Refactor out Search components and hooks --- .../www2/src/components/SearchBox.tsx | 68 +++++++++++++++ urbackupserver/www2/src/css/global.css | 41 +++++++++ urbackupserver/www2/src/pages/Status.tsx | 85 ++++++------------- 3 files changed, 137 insertions(+), 57 deletions(-) create mode 100644 urbackupserver/www2/src/components/SearchBox.tsx diff --git a/urbackupserver/www2/src/components/SearchBox.tsx b/urbackupserver/www2/src/components/SearchBox.tsx new file mode 100644 index 00000000..75b58d8b --- /dev/null +++ b/urbackupserver/www2/src/components/SearchBox.tsx @@ -0,0 +1,68 @@ +import { + Field, + SearchBox as FUISearchBox, + tokens, +} from "@fluentui/react-components"; +import { useState } from "react"; + +const styles: Record = { + search: { + display: "flex", + alignItems: "center", + gap: tokens.spacingHorizontalS, + }, + searchBox: { + width: "28ch", + }, +}; + +export function useFilteredBySearch( + data: T[], + filterFn: (d: T, search: string) => boolean, +) { + const [search, setSearch] = useState(""); + + const filteredItems = data.filter((d) => filterFn(d, search)); + + return { + search, + setSearch, + filteredItems, + }; +} + +export function SearchBox({ + onSearch, +}: { + onSearch: (search: string) => void; +}) { + return ( + + { + const search = data.value.toLowerCase(); + + onSearch(search); + }} + style={styles.searchBox} + /> + + ); +} + +export function filterBySearch( + search: string, + searchableFields: Record, +): boolean { + if (search.length) { + // Find matching search term in data values + const match = Object.values(searchableFields).some((v) => + String(v).toLowerCase().includes(search), + ); + + return match; + } + + return true; +} diff --git a/urbackupserver/www2/src/css/global.css b/urbackupserver/www2/src/css/global.css index bcca9ceb..1e249d96 100644 --- a/urbackupserver/www2/src/css/global.css +++ b/urbackupserver/www2/src/css/global.css @@ -1,5 +1,22 @@ @import url("reset.css") layer(reset); +:root { + /* FluentUI Spacing - https://react.fluentui.dev/?path=/docs/theme-spacing--docs */ + --spacingXXS: 2px; + --spacingXS: 4px; + --spacingSNudge: 6px; + --spacingS: 8px; + --spacingMNudge: 10px; + --spacingM: 12px; + --spacingL: 16px; + --spacingXL: 20px; + --spacingXXL: 24px; + --spacingXXXL: 32px; + + /* App variables */ + --gutter: var(--spacingM); +} + @layer composition { /* FLOW COMPOSITION @@ -9,6 +26,30 @@ .flow > * + * { margin-top: var(--flow-space, 1em); } + + /* + CLUSTER + More info: https://every-layout.dev/layouts/cluster/ + A layout that lets you distribute items with consitent + spacing, regardless of their size + + CUSTOM PROPERTIES AND CONFIGURATION + --gutter: This defines the space + between each item. + + --cluster-horizontal-alignment (flex-start) How items should align + horizontally. Can be any acceptable flexbox aligmnent value. + + --cluster-vertical-alignment How items should align vertically. + Can be any acceptable flexbox alignment value. + */ + .cluster { + display: flex; + flex-wrap: wrap; + gap: var(--gutter); + justify-content: var(--cluster-horizontal-alignment, flex-start); + align-items: var(--cluster-vertical-alignment, center); + } } .table-wrapper { diff --git a/urbackupserver/www2/src/pages/Status.tsx b/urbackupserver/www2/src/pages/Status.tsx index 665ab64f..6a321518 100644 --- a/urbackupserver/www2/src/pages/Status.tsx +++ b/urbackupserver/www2/src/pages/Status.tsx @@ -7,11 +7,9 @@ import { DataGridHeader, DataGridHeaderCell, DataGridRow, - Field, makeStyles, MenuButton, MenuItem, - SearchBox, Spinner, TableCellLayout, TableColumnDefinition, @@ -37,6 +35,11 @@ import { PaginationItemsPerPageSelector, usePagination, } from "../components/Pagination"; +import { + filterBySearch, + SearchBox, + useFilteredBySearch, +} from "../components/SearchBox"; const compareNum = (a: number, b: number) => { return a == b ? 0 : a < b ? 1 : -1; @@ -113,18 +116,7 @@ const columns: TableColumnDefinition[] = [ ]; const useStyles = makeStyles({ - topFilters: { - display: "flex", - gap: tokens.spacingHorizontalM, - }, - search: { - display: "flex", - alignItems: "center", - gap: tokens.spacingHorizontalS, - }, - searchBox: { - width: "28ch", - }, + // TODO: remove following unused style pagination: { marginInlineStart: "auto", }, @@ -151,11 +143,12 @@ const Status = () => { const classes = useStyles(); - const [search, setSearch] = useState(""); + const data = statusResult.data!.status; - const dataItems = statusResult.data!.status; - - const filteredItems = filterClientData(dataItems, search); + const { setSearch, filteredItems } = useFilteredBySearch( + data, + filterClientData, + ); const { itemsPerPage, setItemsPerPage, pageData, page, setPage } = usePagination(filteredItems); @@ -165,18 +158,8 @@ const Status = () => { }>

Status page

-
- - { - const search = data.value.toLowerCase(); - - setSearch(search); - }} - /> - +
+ ) { return clientIds; } -function filterClientData(dataItems: StatusClientItem[], search: string) { - return dataItems.filter((d) => { - // Hide items scheduled for delete - if (d.delete_pending === "1") { - return false; - } +function filterClientData(item: StatusClientItem, search: string) { + // Hide items scheduled for delete + if (item.delete_pending === "1") { + return false; + } - // If there's a search term, filter by search term within object values - if (search.length) { - const { id, name, lastbackup, lastbackup_image } = d; + const { id, name, lastbackup, lastbackup_image } = item; - // Search in fields as displayed in the table - const searchableFields = { - id, - name, - lastbackup: lastbackup === 0 ? "Never" : formatDatetime(lastbackup), - lastbackup_image: - lastbackup === 0 ? "Never" : formatDatetime(lastbackup_image), - }; + // Search in fields as displayed in the table + const searchableFields = { + id: String(id), + name, + lastbackup: lastbackup === 0 ? "Never" : formatDatetime(lastbackup), + lastbackup_image: + lastbackup === 0 ? "Never" : formatDatetime(lastbackup_image), + }; - // Find matching search term in data values - const match = Object.values(searchableFields).some((v) => - String(v).toLowerCase().includes(search), - ); - - return match; - } - - return true; - }); + return filterBySearch(search, searchableFields); }