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 (
+
+
+
+ Apply
+ Reset
+
+
+
+
+ {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} */}
- Apply
- Reset
-
-
- {/* 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 */}
-
-
-
+