mirror of
https://github.com/uroni/urbackup_backend.git
synced 2025-10-26 11:36:50 +00:00
Refactor out Search components and hooks
This commit is contained in:
parent
835b9a20fa
commit
dba614fcc9
68
urbackupserver/www2/src/components/SearchBox.tsx
Normal file
68
urbackupserver/www2/src/components/SearchBox.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import {
|
||||
Field,
|
||||
SearchBox as FUISearchBox,
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import { useState } from "react";
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
search: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
},
|
||||
searchBox: {
|
||||
width: "28ch",
|
||||
},
|
||||
};
|
||||
|
||||
export function useFilteredBySearch<T>(
|
||||
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 (
|
||||
<Field label="Search" style={styles.search}>
|
||||
<FUISearchBox
|
||||
autoComplete="off"
|
||||
onChange={(_, data) => {
|
||||
const search = data.value.toLowerCase();
|
||||
|
||||
onSearch(search);
|
||||
}}
|
||||
style={styles.searchBox}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export function filterBySearch(
|
||||
search: string,
|
||||
searchableFields: Record<string, string>,
|
||||
): 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;
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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<StatusClientItem>[] = [
|
||||
];
|
||||
|
||||
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<StatusClientItem>(
|
||||
data,
|
||||
filterClientData,
|
||||
);
|
||||
|
||||
const { itemsPerPage, setItemsPerPage, pageData, page, setPage } =
|
||||
usePagination(filteredItems);
|
||||
@ -165,18 +158,8 @@ const Status = () => {
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<TableWrapper>
|
||||
<h3>Status page</h3>
|
||||
<div className={classes.topFilters}>
|
||||
<Field label="Search" className={classes.search}>
|
||||
<SearchBox
|
||||
autoComplete="off"
|
||||
className={classes.searchBox}
|
||||
onChange={(_, data) => {
|
||||
const search = data.value.toLowerCase();
|
||||
|
||||
setSearch(search);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<div className="cluster">
|
||||
<SearchBox onSearch={setSearch} />
|
||||
<PaginationItemsPerPageSelector
|
||||
itemsPerPage={itemsPerPage}
|
||||
setItemsPerPage={setItemsPerPage}
|
||||
@ -289,34 +272,22 @@ function transformSelectedRows(selectedRows: Set<TableRowId>) {
|
||||
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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user