diff --git a/components/aggregation/mat/Filters.js b/components/aggregation/mat/Filters.js new file mode 100644 index 000000000..388852ec5 --- /dev/null +++ b/components/aggregation/mat/Filters.js @@ -0,0 +1,429 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTable, useFlexLayout, useRowSelect, useSortBy, useGlobalFilter, useAsyncDebounce } from 'react-table' +import { FormattedMessage, useIntl } from 'react-intl' +import { defaultRangeExtractor, useVirtual } from 'react-virtual' +import styled from 'styled-components' +import { Flex, Box, Button, Text } from 'ooni-components' + +import GridChart, { prepareDataForGridChart } from './GridChart' +import { ResizableBox } from './Resizable' +import { DetailsBox } from '../../measurement/DetailsBox' +import { sortRows } from './computations' + +const TableContainer = styled.div` + ${'' /* These styles are suggested for the table fill all available space in its containing element */} + flex: 1; +` + +const Table = styled.div` + border-spacing: 0; + border: 1px solid black; +` + +const Cell = styled.div` + padding: 8px; +` + +const TableRow = styled(Flex)` + height: 35px; + border-bottom: 1px solid black; + &:last-child { + border-bottom: 0; + } +` + +const TableHeader = styled.div` + ${TableRow} { + height: auto; + margin-bottom: 8px; + border-bottom: 1px solid black; + } + &:last-child { + border-bottom: 2px solid black; + } + & ${Cell} { + border-right: 1px solid black; + font-weight: bold; + &:last-child { + border-right: 0; + } + } +` + +const TableBody = styled.div` + height: 250px; + overflow: auto; +` + +const IndeterminateCheckbox = React.forwardRef( + ({ indeterminate, ...rest }, ref) => { + const defaultRef = React.useRef() + const resolvedRef = ref || defaultRef + + React.useEffect(() => { + resolvedRef.current.indeterminate = indeterminate + }, [resolvedRef, indeterminate]) + + return ( + <> + + + ) + } +) +IndeterminateCheckbox.displayName = 'IndeterminateCheckbox' + +const SearchFilter = ({ + column: { filterValue, preFilteredRows, setFilter }, + groupedRows, +}) => { + const count = groupedRows.length + + return ( + { + setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely + }} + placeholder={`Search ${count} records...`} + /> + ) +} + +const StyledGlobalFilter = styled(Box)` + margin: 16px; + margin-top: 10px; + input { + border: 0; + outline: 0; + } +` + +function GlobalFilter({ + preGlobalFilteredRows, + globalFilter, + setGlobalFilter, +}) { + const count = preGlobalFilteredRows.length + const [value, setValue] = React.useState(globalFilter) + const onChange = useAsyncDebounce(value => { + setGlobalFilter(value || '') + }, 200) + + useEffect(() => { + if (!globalFilter || globalFilter === '') { + setValue('') + } + }, [globalFilter]) + + return ( + + Search:{' '} + { + setValue(e.target.value) + onChange(e.target.value) + }} + placeholder={`Search ${count} records...`} + /> + + ) +} + +const SortHandle = ({ isSorted, isSortedDesc }) => { + return ( + + {isSorted ? ( + isSortedDesc ? '▼' : '▲' + ) : ( +   + )} + ) +} + +// This same reference is passed to GridChart when there are no rows to filter out +// Maybe this can also be `[]` +const noRowsSelected = null + +const Filters = ({ data = [], tableData, setDataForCharts, query }) => { + const intl = useIntl() + const resetTableRef = useRef(false) + const yAxis = query.axis_y + + const defaultColumn = React.useMemo( + () => ({ + // When using the useFlexLayout: + width: 70, // width is used for both the flex-basis and flex-grow + Filter: SearchFilter, + Cell: ({ value }) => { + const intl = useIntl() + return typeof value === 'number' ? intl.formatNumber(value, {}) : String(value) + } + }), + [] + ) + + // Aggregate by the first column + const initialState = React.useMemo(() => ({ + hiddenColumns: ['yAxisCode'], + sortBy: [{ id: 'yAxisLabel', desc: false }] + }),[]) + + const getRowId = React.useCallback(row => row[query.axis_y], []) + + const columns = useMemo(() => [ + { + Header: intl.formatMessage({ id: `MAT.Table.Header.${yAxis}`}), + Cell: ({ value, row }) => ( + + {value} + + ), + id: 'yAxisLabel', + accessor: 'rowLabel', + filter: 'text', + style: { + width: '35%' + } + }, + { + id: 'yAxisCode', + accessor: yAxis, + disableFilters: true, + }, + { + Header: , + accessor: 'anomaly_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end' + } + }, + { + Header: , + accessor: 'confirmed_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end' + } + }, + { + Header: , + accessor: 'failure_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end' + } + }, + { + Header: , + accessor: 'measurement_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end' + } + } + ], [intl, yAxis]) + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, // contains filtered rows + toggleAllRowsSelected, + selectedFlatRows, + prepareRow, + state, + setGlobalFilter, + preGlobalFilteredRows, + globalFilteredRows, + } = useTable( + { + columns, + data: tableData, + initialState, + defaultColumn, + getRowId, + }, + useFlexLayout, + useGlobalFilter, + useSortBy, + useRowSelect, + (hooks) => { + hooks.visibleColumns.push((columns) => [ + // Pseudo column for selection checkboxes + { + id: 'selection', + width: 30, + // The header can use the table's getToggleAllRowsSelectedProps method + // to render a checkbox + // eslint-disable-next-line react/display-name + Header: ({ getToggleAllRowsSelectedProps }) => ( +
+ +
+ ), + // The cell can use the individual row's getToggleRowSelectedProps method + // to the render a checkbox + // eslint-disable-next-line react/display-name + Cell: ({ row }) => ( +
+ +
+ ) + }, + ...columns + ]) + } + ) + + const updateCharts = useCallback(() => { + const selectedRows = Object.keys(state.selectedRowIds).sort((a,b) => sortRows(a, b, query.axis_y)) + + if (selectedRows.length > 0 && selectedRows.length !== preGlobalFilteredRows.length) { + setDataForCharts(selectedRows) + } else { + setDataForCharts(noRowsSelected) + } + }, [preGlobalFilteredRows.length, query.axis_y, state.selectedRowIds, setDataForCharts]) + + /** + * Reset the table filter + * Note: doesn't reset the sort state + */ + const resetFilter = useCallback(() => { + // toggleAllRowsSelected() doesn't work after calling setGlobalFilter('') + // so if globalFilter is set, then use resetTableRef to make it a two-step + // reset (step 2 in the below useEffect) + // otherwise, just toggle the selected rows and the reset is done + if (!state.globalFilter) { + toggleAllRowsSelected(false) + } else { + resetTableRef.current = true + setGlobalFilter('') + } + setDataForCharts(noRowsSelected) + }, [setGlobalFilter, state.globalFilter, toggleAllRowsSelected, setDataForCharts]) + + useEffect(() => { + if (state.globalFilter == undefined && resetTableRef.current === true) { + resetTableRef.current = false + toggleAllRowsSelected(false) + } + }, [state.globalFilter, toggleAllRowsSelected]) + + const parentRef = React.useRef() + + const { virtualItems: virtualRows, totalSize } = useVirtual({ + size: rows.length, + parentRef, + overscan: 10, + estimateSize: React.useCallback(() => 35, []), + }) + + return ( + + + + + + + + + + {headerGroups.map((headerGroup, i) => ( + + {headerGroup.headers.map((column, i) => { + return ( + + + {column.render('Header')} + {column.canSort && + + } + + + )} + )} + + ))} + + + + +
+ + {virtualRows.map(virtualRow => { + const row = rows[virtualRow.index] + prepareRow(row) + return ( + + {row.cells.map((cell, i) => { + return ( + + {cell.render('Cell')} + + ) + })} + + ) + })} + +
+
+
+
+
+ ) +} + +export default Filters \ No newline at end of file diff --git a/components/aggregation/mat/TableView.js b/components/aggregation/mat/TableView.js index eb80bd540..66632a9a3 100644 --- a/components/aggregation/mat/TableView.js +++ b/components/aggregation/mat/TableView.js @@ -8,141 +8,7 @@ import GridChart, { prepareDataForGridChart } from './GridChart' import { ResizableBox } from './Resizable' import { DetailsBox } from '../../measurement/DetailsBox' import { sortRows } from './computations' - -const TableContainer = styled.div` - ${'' /* These styles are suggested for the table fill all available space in its containing element */} - flex: 1; - ${'' /* These styles are required for a horizontaly scrollable table overflow */} - overflow: auto; -` - -const Table = styled.div` - border-spacing: 0; - border: 1px solid black; -` - -const Cell = styled.div` - padding: 8px; -` - -const TableRow = styled(Flex)` - border-bottom: 1px solid black; - &:last-child { - border-bottom: 0; - } -` - -const TableHeader = styled.div` - ${TableRow} { - margin-bottom: 8px; - border-bottom: 1px solid black; - } - &:last-child { - border-bottom: 2px solid black; - } - & ${Cell} { - border-right: 1px solid black; - font-weight: bold; - &:last-child { - border-right: 0; - } - } - -` - -const TableBody = styled.div` - ${'' /* These styles are required for a scrollable table body */} - overflow-y: scroll; - overflow-x: hidden; - height: 250px; -` - -const IndeterminateCheckbox = React.forwardRef( - ({ indeterminate, ...rest }, ref) => { - const defaultRef = React.useRef() - const resolvedRef = ref || defaultRef - - React.useEffect(() => { - resolvedRef.current.indeterminate = indeterminate - }, [resolvedRef, indeterminate]) - - return ( - <> - - - ) - } -) -IndeterminateCheckbox.displayName = 'IndeterminateCheckbox' - -const SearchFilter = ({ - column: { filterValue, preFilteredRows, setFilter }, - groupedRows, -}) => { - const count = groupedRows.length - - return ( - { - setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely - }} - placeholder={`Search ${count} records...`} - /> - ) -} - -const StyledGlobalFilter = styled(Box)` - margin: 16px; - margin-top: 10px; - input { - border: 0; - outline: 0; - } -` - -function GlobalFilter({ - preGlobalFilteredRows, - globalFilter, - setGlobalFilter, -}) { - const count = preGlobalFilteredRows.length - const [value, setValue] = React.useState(globalFilter) - const onChange = useAsyncDebounce(value => { - setGlobalFilter(value || undefined) - }, 200) - - useEffect(() => { - if (!globalFilter || globalFilter === '') { - setValue('') - } - }, [globalFilter]) - - return ( - - Search:{' '} - { - setValue(e.target.value) - onChange(e.target.value) - }} - placeholder={`Search ${count} records...`} - /> - - ) -} - -const SortHandle = ({ isSorted, isSortedDesc }) => { - return ( - - {isSorted ? ( - isSortedDesc ? '▼' : '▲' - ) : ( -   - )} - ) -} +import Filters from './Filters' const prepareDataforTable = (data, query) => { const table = [] @@ -182,89 +48,6 @@ const TableView = ({ data, query }) => { const resetTableRef = useRef(false) const yAxis = query.axis_y - const defaultColumn = React.useMemo( - () => ({ - // When using the useFlexLayout: - width: 70, // width is used for both the flex-basis and flex-grow - Filter: SearchFilter, - Cell: ({ value }) => { - const intl = useIntl() - return typeof value === 'number' ? intl.formatNumber(value, {}) : String(value) - } - }), - [] - ) - - // Aggregate by the first column - const initialState = React.useMemo(() => ({ - hiddenColumns: ['yAxisCode'], - sortBy: [{ id: 'yAxisLabel', desc: false }] - }),[]) - - const getRowId = React.useCallback(row => row[query.axis_y], []) - - const columns = useMemo(() => [ - { - Header: intl.formatMessage({ id: `MAT.Table.Header.${yAxis}`}), - Cell: ({ value, row }) => ( - - {value} - - ), - id: 'yAxisLabel', - accessor: 'rowLabel', - filter: 'text', - style: { - width: '35%' - } - }, - { - id: 'yAxisCode', - accessor: yAxis, - disableFilters: true, - }, - { - Header: , - accessor: 'anomaly_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'confirmed_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'failure_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'measurement_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - } - ], [intl, yAxis]) - // The incoming data is reshaped to generate: // - reshapedData: holds the full set that will be used by GridChart // to then filter out rows based on `selectedRows` generated by the table @@ -279,168 +62,15 @@ const TableView = ({ data, query }) => { } }, [query, data]) - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, // contains filtered rows - toggleAllRowsSelected, - selectedFlatRows, - prepareRow, - state, - setGlobalFilter, - preGlobalFilteredRows, - globalFilteredRows, - } = useTable( - { - columns, - data: tableData, - initialState, - defaultColumn, - getRowId, - }, - useFlexLayout, - useGlobalFilter, - useSortBy, - useRowSelect, - (hooks) => { - hooks.visibleColumns.push((columns) => [ - // Pseudo column for selection checkboxes - { - id: 'selection', - width: 30, - // The header can use the table's getToggleAllRowsSelectedProps method - // to render a checkbox - // eslint-disable-next-line react/display-name - Header: ({ getToggleAllRowsSelectedProps }) => ( -
- -
- ), - // The cell can use the individual row's getToggleRowSelectedProps method - // to the render a checkbox - // eslint-disable-next-line react/display-name - Cell: ({ row }) => ( -
- -
- ) - }, - ...columns - ]) - } - ) - - // const [chartPanelHeight, setChartPanelHeight] = useState(800) - - // const onPanelResize = useCallback((width, height) => { - // // Panel height - (height of ChartHeader + XAxis) = Height of RowCharts - // setChartPanelHeight(height - (90 + 62)) - // }, []) - const [dataForCharts, setDataForCharts] = useState(noRowsSelected) - - const updateCharts = useCallback(() => { - const selectedRows = Object.keys(state.selectedRowIds).sort((a,b) => sortRows(a, b, query.axis_y)) - - if (selectedRows.length > 0 && selectedRows.length !== preGlobalFilteredRows.length) { - setDataForCharts(selectedRows) - } else { - setDataForCharts(noRowsSelected) - } - }, [preGlobalFilteredRows.length, query.axis_y, state.selectedRowIds]) - - /** - * Reset the table filter - * Note: doesn't reset the sort state - */ - const resetFilter = useCallback(() => { - // toggleAllRowsSelected() doesn't work after calling setGlobalFilter('') - // so if globalFilter is set, then use resetTableRef to make it a two-step - // reset (step 2 in the below useEffect) - // otherwise, just toggle the selected rows and the reset is done - if (!state.globalFilter) { - toggleAllRowsSelected(false) - } else { - resetTableRef.current = true - setGlobalFilter('') - } - setDataForCharts(noRowsSelected) - }, [setGlobalFilter, state.globalFilter, toggleAllRowsSelected]) - - useEffect(() => { - if (state.globalFilter == undefined && resetTableRef.current === true) { - resetTableRef.current = false - toggleAllRowsSelected(false) - } - }, [state.globalFilter, toggleAllRowsSelected]) return ( - - - - {/* {chartsButton} */} - - - - - {/* eslint-disable react/jsx-key */} - - - {headerGroups.map(headerGroup => ( - - {headerGroup.headers.map(column => { - return ( - - - {column.render('Header')} - {column.canSort && - - } - - - )} - )} - - ))} - - - - - - {rows.map(row => { - prepareRow(row) - return ( - - {row.cells.map(cell => { - return ( - - {cell.render('Cell')} - - ) - })} - - ) - })} - -
- {/* eslint-enable react/jsx-key */} -
-
-
+