From c35128ce14246f839fa52e18957584a970e2cda5 Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Wed, 5 Jun 2024 20:59:44 -0400 Subject: [PATCH 1/5] add an indicator with a clear button when there are column filters; indicate when no incidents match filters; stop polling when too many incidents --- .../IncidentTable/IncidentTableComponent.jsx | 55 +++++++++------- .../QuerySettings/QuerySettingsComponent.jsx | 22 ++++++- .../ColumnFilterIndicatorComponent.jsx | 62 +++++++++++++++++++ src/redux/incident_table/actions.js | 7 +++ src/redux/incident_table/reducers.js | 14 +++++ src/redux/incident_table/sagas.js | 12 ++++ src/redux/incidents/reducers.js | 1 + src/redux/log_entries/sagas.js | 12 +++- src/redux/rootSaga.js | 2 + 9 files changed, 159 insertions(+), 28 deletions(-) create mode 100644 src/components/QuerySettings/subcomponents/ColumnFilterIndicatorComponent.jsx diff --git a/src/components/IncidentTable/IncidentTableComponent.jsx b/src/components/IncidentTable/IncidentTableComponent.jsx index 6649d9cf..192f14fd 100644 --- a/src/components/IncidentTable/IncidentTableComponent.jsx +++ b/src/components/IncidentTable/IncidentTableComponent.jsx @@ -46,6 +46,7 @@ import { import { selectIncidentTableRows as selectIncidentTableRowsConnected, updateIncidentTableState as updateIncidentTableStateConnected, + CLEAR_INCIDENT_TABLE_FILTERS_COMPLETED, } from 'src/redux/incident_table/actions'; import EmptyIncidentsComponent from './subcomponents/EmptyIncidentsComponent'; @@ -120,7 +121,7 @@ const doCsvExport = (tableData) => { const IncidentTableComponent = () => { const { - incidentTableState, incidentTableColumns, + incidentTableState, incidentTableColumns, status: incidentTableStatus, } = useSelector((state) => state.incidentTable); const { status: incidentActionsStatus, @@ -221,7 +222,8 @@ const IncidentTableComponent = () => { // Debouncing for table state const debouncedUpdateIncidentTableState = useDebouncedCallback((state, action) => { // Only update store with sorted and column resizing state - if (action.type === 'toggleSortBy' || action.type === 'columnDoneResizing') { + // and filter state + if (action.type === 'toggleSortBy' || action.type === 'columnDoneResizing' || action.type === 'setFilter') { updateIncidentTableState(state); } }, 100); @@ -251,7 +253,7 @@ const IncidentTableComponent = () => { // Set initial state from store initialState: incidentTableState, // Handle updates to table - stateReducer: (newState, action) => debouncedUpdateIncidentTableState(newState, action), + stateReducer: debouncedUpdateIncidentTableState, }, // Plugins useFilters, @@ -327,19 +329,17 @@ const IncidentTableComponent = () => { }, ); - // save filters when the user changes them - useEffect(() => { - updateIncidentTableState({ - ...incidentTableState, - filters: tableInstance.state.filters, - }); - }, [tableInstance.state.filters]); - // Update table filters when columns change useEffect(() => { tableInstance.setAllFilters(incidentTableState.filters); }, [columns]); + useEffect(() => { + if (incidentTableStatus === CLEAR_INCIDENT_TABLE_FILTERS_COMPLETED) { + tableInstance.setAllFilters(incidentTableState.filters); + } + }, [incidentTableStatus]); + const { getTableProps, getTableBodyProps, @@ -536,19 +536,26 @@ const IncidentTableComponent = () => { - - rows[index].id} - itemData={rows} - width={totalColumnsWidth + scrollBarSize} - > - {MyIncidentRow} - - + { rows.length > 0 && ( + + rows[index].id} + itemData={rows} + width={totalColumnsWidth + scrollBarSize} + > + {MyIncidentRow} + + + )} + { rows.length === 0 && ( + + )} setDisplayGetAllModal(false)} diff --git a/src/components/QuerySettings/QuerySettingsComponent.jsx b/src/components/QuerySettings/QuerySettingsComponent.jsx index 36d5da8b..8146eae9 100644 --- a/src/components/QuerySettings/QuerySettingsComponent.jsx +++ b/src/components/QuerySettings/QuerySettingsComponent.jsx @@ -1,4 +1,6 @@ -import React from 'react'; +import React, { + useMemo, +} from 'react'; import { useSelector, useDispatch, @@ -28,6 +30,7 @@ import DatePickerComponent from './subcomponents/DatePickerComponent'; import StatusQueryComponent from './subcomponents/StatusQueryComponent'; import UrgencyQueryComponent from './subcomponents/UrgencyQueryComponent'; import PriorityQueryComponent from './subcomponents/PriorityQueryComponent'; +import ColumnFilterIndicatorComponent from './subcomponents/ColumnFilterIndicatorComponent'; import './QuerySettingsComponent.scss'; @@ -52,6 +55,10 @@ const QuerySettingsComponent = () => { } = useSelector( (state) => state.querySettings, ); + const { + filters, + } = useSelector((state) => state.incidentTable.incidentTableState); + const dispatch = useDispatch(); const updateQuerySettingsServices = (newServiceIds) => { dispatch(updateQuerySettingsServicesConnected(newServiceIds)); @@ -66,6 +73,13 @@ const QuerySettingsComponent = () => { dispatch(updateQuerySettingsTeamsConnected(newTeamIds)); }; + const filterCount = useMemo(() => { + if (filters instanceof Array) { + return filters.length; + } + return 0; + }, [filters]); + return ( { isMulti /> - {/* */} + { filterCount > 0 && ( + + + + )} ); diff --git a/src/components/QuerySettings/subcomponents/ColumnFilterIndicatorComponent.jsx b/src/components/QuerySettings/subcomponents/ColumnFilterIndicatorComponent.jsx new file mode 100644 index 00000000..437fa04f --- /dev/null +++ b/src/components/QuerySettings/subcomponents/ColumnFilterIndicatorComponent.jsx @@ -0,0 +1,62 @@ +import React, { + useMemo, +} from 'react'; + +import { + useSelector, + useDispatch, +} from 'react-redux'; + +import { + useTranslation, +} from 'react-i18next'; + +import { + Flex, + Button, + Tag, +} from '@chakra-ui/react'; + +import { + clearIncidentTableFilters as clearIncidentTableFiltersConnected, +} from 'src/redux/incident_table/actions'; + +const ColumnFilterIndicatorComponent = () => { + const { + t, + } = useTranslation(); + const { + filters, + } = useSelector((state) => state.incidentTable.incidentTableState); + + const dispatch = useDispatch(); + const clearIncidentTableFilters = () => { + dispatch(clearIncidentTableFiltersConnected()); + }; + + const filterCount = useMemo(() => { + if (filters instanceof Array) { + return filters.length; + } + return 0; + }, [filters]); + + return ( + + + {filterCount} + + + + ); +}; + +export default ColumnFilterIndicatorComponent; diff --git a/src/redux/incident_table/actions.js b/src/redux/incident_table/actions.js index f96e0f48..6895c487 100644 --- a/src/redux/incident_table/actions.js +++ b/src/redux/incident_table/actions.js @@ -12,6 +12,9 @@ export const UPDATE_INCIDENT_TABLE_STATE_COMPLETED = 'UPDATE_INCIDENT_TABLE_STAT export const SELECT_INCIDENT_TABLE_ROWS_REQUESTED = 'SELECT_INCIDENT_TABLE_ROWS_REQUESTED'; export const SELECT_INCIDENT_TABLE_ROWS_COMPLETED = 'SELECT_INCIDENT_TABLE_ROWS_COMPLETED'; +export const CLEAR_INCIDENT_TABLE_FILTERS_REQUESTED = 'CLEAR_INCIDENT_TABLE_FILTERS_REQUESTED'; +export const CLEAR_INCIDENT_TABLE_FILTERS_COMPLETED = 'CLEAR_INCIDENT_TABLE_FILTERS_COMPLETED'; + // Define Actions export const saveIncidentTable = (updatedIncidentTableColumns) => ({ @@ -35,3 +38,7 @@ export const selectIncidentTableRows = (allSelected, selectedCount, selectedRows selectedCount, selectedRows, }); + +export const clearIncidentTableFilters = () => ({ + type: CLEAR_INCIDENT_TABLE_FILTERS_REQUESTED, +}); diff --git a/src/redux/incident_table/reducers.js b/src/redux/incident_table/reducers.js index 652115ad..a7f8d3f8 100644 --- a/src/redux/incident_table/reducers.js +++ b/src/redux/incident_table/reducers.js @@ -12,6 +12,8 @@ import { UPDATE_INCIDENT_TABLE_STATE_COMPLETED, SELECT_INCIDENT_TABLE_ROWS_REQUESTED, SELECT_INCIDENT_TABLE_ROWS_COMPLETED, + CLEAR_INCIDENT_TABLE_FILTERS_REQUESTED, + CLEAR_INCIDENT_TABLE_FILTERS_COMPLETED, } from './actions'; const defaultColumns = [ @@ -70,6 +72,18 @@ const incidentTable = produce( draft.status = SELECT_INCIDENT_TABLE_ROWS_COMPLETED; break; + case CLEAR_INCIDENT_TABLE_FILTERS_REQUESTED: + draft.status = CLEAR_INCIDENT_TABLE_FILTERS_REQUESTED; + break; + + case CLEAR_INCIDENT_TABLE_FILTERS_COMPLETED: + draft.incidentTableState = { + ...draft.incidentTableState, + filters: [], + }; + draft.status = CLEAR_INCIDENT_TABLE_FILTERS_COMPLETED; + break; + default: break; } diff --git a/src/redux/incident_table/sagas.js b/src/redux/incident_table/sagas.js index 36641803..dcf69ada 100644 --- a/src/redux/incident_table/sagas.js +++ b/src/redux/incident_table/sagas.js @@ -16,6 +16,8 @@ import { UPDATE_INCIDENT_TABLE_STATE_COMPLETED, SELECT_INCIDENT_TABLE_ROWS_REQUESTED, SELECT_INCIDENT_TABLE_ROWS_COMPLETED, + CLEAR_INCIDENT_TABLE_FILTERS_REQUESTED, + CLEAR_INCIDENT_TABLE_FILTERS_COMPLETED, } from './actions'; import selectIncidentTable from './selectors'; @@ -122,3 +124,13 @@ export function* selectIncidentTableRowsImpl(action) { selectedRows, }); } + +export function* clearIncidentTableFilters() { + yield takeLatest(CLEAR_INCIDENT_TABLE_FILTERS_REQUESTED, clearIncidentTableFiltersImpl); +} + +export function* clearIncidentTableFiltersImpl() { + yield put({ + type: CLEAR_INCIDENT_TABLE_FILTERS_COMPLETED, + }); +} diff --git a/src/redux/incidents/reducers.js b/src/redux/incidents/reducers.js index edd6a2d2..4a6114bf 100644 --- a/src/redux/incidents/reducers.js +++ b/src/redux/incidents/reducers.js @@ -56,6 +56,7 @@ const incidents = produce( case FETCH_INCIDENTS_REQUESTED: draft.fetchingIncidents = true; draft.status = FETCH_INCIDENTS_REQUESTED; + draft.error = null; break; case FETCH_INCIDENTS_COMPLETED: diff --git a/src/redux/log_entries/sagas.js b/src/redux/log_entries/sagas.js index 5125af1f..089d9f12 100644 --- a/src/redux/log_entries/sagas.js +++ b/src/redux/log_entries/sagas.js @@ -169,9 +169,17 @@ export function* pollLogEntriesTask() { }, incidents: { fetchingIncidents, + error: incidentsError, }, } = yield select(); - if (userAuthorized && userAcceptedDisclaimer && !fetchingIncidents && !DEBUG_DISABLE_POLLING) { + + const tooManyIncidentsError = ( + incidentsError + && typeof incidentsError === 'string' + && incidentsError.startsWith('Too many records') + ); + + if (userAuthorized && userAcceptedDisclaimer && !fetchingIncidents && !DEBUG_DISABLE_POLLING && !tooManyIncidentsError) { const lastPollStarted = new Date(); yield put({ type: UPDATE_LOG_ENTRIES_POLLING, @@ -199,7 +207,7 @@ export function* pollLogEntriesTask() { yield delay((LOG_ENTRIES_POLLING_INTERVAL_SECONDS * 1000) - timeTaken); } else { // eslint-disable-next-line no-console - console.log('skipping poll', { userAuthorized, userAcceptedDisclaimer, fetchingIncidents, DEBUG_DISABLE_POLLING }); + console.log('skipping poll', { userAuthorized, userAcceptedDisclaimer, fetchingIncidents, DEBUG_DISABLE_POLLING, tooManyIncidentsError }); yield delay(LOG_ENTRIES_POLLING_INTERVAL_SECONDS * 1000); } } diff --git a/src/redux/rootSaga.js b/src/redux/rootSaga.js index 629370c5..cc38aa32 100644 --- a/src/redux/rootSaga.js +++ b/src/redux/rootSaga.js @@ -48,6 +48,7 @@ import { updateIncidentTableColumns, updateIncidentTableState, selectIncidentTableRows, + clearIncidentTableFilters, } from './incident_table/sagas'; import { @@ -185,6 +186,7 @@ export default function* rootSaga() { updateIncidentTableColumns(), updateIncidentTableState(), selectIncidentTableRows(), + clearIncidentTableFilters(), // Incident Actions doAction(), From ffcabc3c2906b29d038ddee593f42c060639b64a Mon Sep 17 00:00:00 2001 From: Gavin Reynolds Date: Thu, 13 Jun 2024 10:48:00 +0100 Subject: [PATCH 2/5] Add e2e test for Column Filters returning no incidents and clear filters button Signed-off-by: Gavin Reynolds --- cypress/e2e/Search/search.spec.js | 8 ++++++++ .../subcomponents/ColumnFilterIndicatorComponent.jsx | 1 + 2 files changed, 9 insertions(+) diff --git a/cypress/e2e/Search/search.spec.js b/cypress/e2e/Search/search.spec.js index 900df9ac..41b63d7e 100644 --- a/cypress/e2e/Search/search.spec.js +++ b/cypress/e2e/Search/search.spec.js @@ -113,4 +113,12 @@ describe('Search Incidents', { failFast: { enabled: true } }, () => { cy.get('#service-filter-icon').realHover(); cy.get('button[aria-label="Clear Filter"]').filter(':visible').click(); }); + + it('Column filtering on Service column for `zzzzzz` returns no incidents and clear filters button', () => { + cy.get('#service-filter-icon').realHover(); + cy.get('input[placeholder="Filter"]').filter(':visible').click().type('zzzzzz'); + cy.get('.empty-incidents-badge').should('be.visible'); + cy.get('#clear-filters-button').filter(':visible').click(); + waitForIncidentTable(); + }); }); diff --git a/src/components/QuerySettings/subcomponents/ColumnFilterIndicatorComponent.jsx b/src/components/QuerySettings/subcomponents/ColumnFilterIndicatorComponent.jsx index 437fa04f..01cc8002 100644 --- a/src/components/QuerySettings/subcomponents/ColumnFilterIndicatorComponent.jsx +++ b/src/components/QuerySettings/subcomponents/ColumnFilterIndicatorComponent.jsx @@ -47,6 +47,7 @@ const ColumnFilterIndicatorComponent = () => { {filterCount}