diff --git a/src/components/records/RecordList.js b/src/components/records/RecordList.js new file mode 100644 index 0000000000000000000000000000000000000000..4d9afb92031aa378032022f815aac3d27ca1a2c8 --- /dev/null +++ b/src/components/records/RecordList.js @@ -0,0 +1,35 @@ +import React from "react"; +import { List, ListItem, Paper } from "@mui/material"; +import { Link } from "react-router-dom"; +import { RecordBadge } from "./RecordBadge"; + +export function RecordListItem({ record }) { + return ( + <Paper> + <ListItem + button + component={Link} + to={record.url} + > + <RecordBadge record={record} /> + </ListItem> + </Paper> + ); +} + +export function RecordList({ records }) { + return ( + <List> + {records && + records.map((record) => { + record.url = `/records/${record.name}`; + return ( + <RecordListItem + key={record.name} + record={record} + /> + ); + })} + </List> + ); +} diff --git a/src/components/records/RecordTable.js b/src/components/records/RecordTable.js index 159c9f0d8ec73f0bb17cec42417b783c30db6c98..59a083a3e5c9229156687ddc0b06023c4f991469 100644 --- a/src/components/records/RecordTable.js +++ b/src/components/records/RecordTable.js @@ -1,21 +1,39 @@ import React from "react"; -import { CustomTable } from "../common/table/CustomTable"; +import { Table } from "@ess-ics/ce-ui-common"; import { RecordStatusIcon } from "./RecordIcons"; import { Grid, Tooltip, Typography } from "@mui/material"; import { useRedirect } from "../../hooks/Redirect"; +import { noWrapText } from "../common/Helper"; const recordsColumns = [ - { id: "bulb", label: "Status", width: "5ch", textAlign: "center" }, - { id: "name", label: "Record", width: "15ch" }, - { id: "description", label: "Description", width: "15ch" }, - { id: "iocName", label: "IOC name", width: "10ch" }, - { id: "hostName", label: "Host", width: "10ch" } + { + field: "bulb", + headerName: "Status", + flex: 0, + headerAlign: "center", + align: "center" + }, + { field: "name", headerName: "Record", width: "15ch", sortable: false }, + { + field: "description", + headerName: "Description", + width: "20ch", + sortable: false + }, + { field: "iocName", headerName: "IOC name", width: "10ch", sortable: false }, + { field: "hostName", headerName: "Host", width: "10ch", sortable: false } ]; const iocDetailsColumns = [ - { id: "bulb", label: "Status", width: "5ch", textAlign: "center" }, - { id: "name", label: "Record", width: "15ch" }, - { id: "iocVersion", label: "Version", width: "15ch" } + { + field: "bulb", + headerName: "Status", + flex: 0, + headerAlign: "center", + align: "center" + }, + { field: "name", headerName: "Record", width: "15ch", sortable: false }, + { field: "iocVersion", headerName: "Version", width: "15ch", sortable: false } ]; function createRecordDescription(description) { @@ -57,7 +75,7 @@ export function createRecordsRow(record) { <RecordStatusIcon record={record} /> </Grid> ), - name: record.name, + name: noWrapText(record.name), description: createRecordDescription(record.description), iocName: record.iocName, hostName: record.hostName @@ -85,42 +103,32 @@ export function createIocDetailsRow(record) { export function RecordTable({ records, rowType = "records", - lazyParams, - setLazyParams, - rowsPerPage, - loading + loading, + pagination, + onPage }) { const tableTypeSpecifics = { records: [recordsColumns, createRecordsRow], iocDetails: [iocDetailsColumns, createIocDetailsRow] }; - const [columns, createRow] = tableTypeSpecifics[rowType]; - const redirect = useRedirect(); + const [columns, createRow] = tableTypeSpecifics[rowType]; + const rows = (records?.recordList ?? []).map((record) => createRow(record)); const itemLink = (name) => `/records/${encodeURIComponent(name)}`; - - const onRowClicked = (id) => { - redirect(itemLink(id)); + const onRowClick = (record) => { + redirect(itemLink(record?.id)); }; return ( - <CustomTable + <Table columns={columns} - rows={records.recordList.map((record) => createRow(record))} - // Total count must be practically infinity for now (till total count can be retrieved from API) - totalCount={ - records.listSize < records.limit - ? records.listSize + records.limit * records.pageNumber - : 1000000 - } - lazyParams={lazyParams} - setLazyParams={setLazyParams} - rowsPerPage={rowsPerPage} + rows={rows} + onRowClick={onRowClick} + pagination={pagination} + onPage={onPage} loading={loading} - paginatorTemplate="FirstPageLink PrevPageLink NextPageLink RowsPerPageDropdown" - handleRowClick={onRowClicked} /> ); } diff --git a/src/views/records/RecordListView.js b/src/views/records/RecordListView.js index 879b8b3d8299fc55d33308e976b773c12583ba68..19b360730dc1132c7e0260fb5111df0827a02445 100644 --- a/src/views/records/RecordListView.js +++ b/src/views/records/RecordListView.js @@ -1,30 +1,199 @@ -import React, { useContext, useEffect } from "react"; -import { Paper, Grid } from "@mui/material"; -import { GlobalAppBarContext } from "@ess-ics/ce-ui-common"; -import { RootContainer } from "../../components/common/Container/RootContainer"; -import { RecordSearch } from "../../components/records/RecordSearch"; -import { applicationTitle } from "../../components/common/Helper"; +import React, { + useState, + useMemo, + useCallback, + useContext, + useEffect +} from "react"; +import { useRecordSearch } from "../../api/SwaggerApi"; +import { + Container, + Grid, + useMediaQuery, + Tabs, + Tab, + Typography +} from "@mui/material"; +import { GlobalAppBarContext, RootPaper } from "@ess-ics/ce-ui-common"; +import { + applicationTitle, + initRequestParams +} from "../../components/common/Helper"; +import { SearchBar } from "../../components/common/SearchBar/SearchBar"; +import useUrlState from "@ahooksjs/use-url-state"; +import { + serialize, + deserialize +} from "../../components/common/URLState/URLState"; +import { RecordTable } from "../../components/records/RecordTable"; +import { RecordList } from "../../components/records/RecordList"; +import { usePagination } from "../../hooks/pagination"; export function RecordListView() { const { setTitle } = useContext(GlobalAppBarContext); useEffect(() => setTitle(applicationTitle("Records")), [setTitle]); + const [records, getRecords /* reset*/, , loading] = useRecordSearch(); + + const [urlState, setUrlState] = useUrlState( + { + tab: "0", + rows: "20", + page: "0", + query: "" + }, + { navigateMode: "replace" } + ); + const [recordFilter, setRecordFilter] = useState(); + + const handleTabChange = useCallback( + (event, tab) => { + setUrlState((s) => + serialize(s.tab) === serialize(tab) + ? { tab: serialize(tab) } + : { tab: serialize(tab), page: "0" } + ); + + changeTab(tab); + }, + [setUrlState] + ); + + const changeTab = (tab) => { + if (tab === 0) { + setRecordFilter(null); + } else if (tab === 1) { + setRecordFilter("ACTIVE"); + } else if (tab === 2) { + setRecordFilter("INACTIVE"); + } + }; + + useEffect(() => { + urlState.tab && changeTab(deserialize(urlState.tab)); + }, [urlState]); + + const urlPagination = useMemo(() => { + return { + rows: deserialize(urlState.rows), + page: deserialize(urlState.page) + }; + }, [urlState.rows, urlState.page]); + + const setUrlPagination = useCallback( + ({ rows, page }) => { + setUrlState({ + rows: serialize(rows), + page: serialize(page) + }); + }, + [setUrlState] + ); + + const rowsPerPage = [20, 50]; + + const { pagination, setPagination } = usePagination({ + rowsPerPageOptions: rowsPerPage, + initLimit: urlPagination.rows ?? rowsPerPage[0], + initPage: urlPagination.page ?? 0 + }); + + // update pagination whenever search result total pages change + useEffect(() => { + setPagination({ totalCount: records.totalCount ?? 0 }); + }, [setPagination, records.totalCount]); + + // whenever url state changes, update pagination + useEffect(() => { + setPagination({ ...urlPagination }); + }, [setPagination, urlPagination]); + + // whenever table pagination internally changes (user clicks next page etc) + // update the row params + useEffect(() => { + setUrlPagination(pagination); + }, [pagination, setUrlPagination]); + + // Request new search results whenever search or pagination changes + useEffect(() => { + let requestParams = initRequestParams( + pagination, + deserialize(urlState.query) + ); + requestParams.filter = recordFilter; + getRecords(requestParams); + }, [getRecords, recordFilter, urlState.query, pagination]); + + // Callback for searchbar, called whenever user updates search + const setSearch = useCallback( + (query) => { + setUrlState({ query: serialize(query) }); + }, + [setUrlState] + ); + + // Invoked by Table on change to pagination + const onPage = (params) => { + setPagination(params); + }; + + const smUp = useMediaQuery((theme) => theme.breakpoints.up("sm")); + const smDown = useMediaQuery((theme) => theme.breakpoints.down("sm")); + + let content = ( + <SearchBar + search={setSearch} + query={deserialize(urlState.query)} + loading={loading} + placeholder="Search in Record" + > + {smDown ? <RecordList records={records} /> : null} + {smUp ? ( + <RecordTable + records={records} + loading={loading} + pagination={pagination} + onPage={onPage} + /> + ) : null} + </SearchBar> + ); + return ( - <RootContainer> - <Paper> + <RootPaper> + <Grid + container + spacing={1} + > <Grid container spacing={1} + component={Container} + justifyContent="space-between" + alignItems="center" + style={{ display: "flex" }} > - <Grid - item - xs={12} - md={12} - > - <RecordSearch /> + <Grid item> + <Tabs + value={deserialize(urlState.tab)} + onChange={handleTabChange} + > + <Tab label={<Typography variant="h5">All</Typography>} /> + <Tab label={<Typography variant="h5">Only active</Typography>} /> + <Tab + label={<Typography variant="h5">Only inactive</Typography>} + /> + </Tabs> </Grid> </Grid> - </Paper> - </RootContainer> + <Grid + item + xs={12} + md={12} + > + {content} + </Grid> + </Grid> + </RootPaper> ); }