diff --git a/.github/workflows/cd-workflow.yml b/.github/workflows/cd-workflow.yml
index 538c84e0..72102772 100644
--- a/.github/workflows/cd-workflow.yml
+++ b/.github/workflows/cd-workflow.yml
@@ -55,7 +55,7 @@ jobs:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Install project dependencies (via cache)
- run: yarn --prefer-offline
+ run: yarn --frozen-lockfile --prefer-offline --ignore-optional
- name: Build application bundle
run: yarn build
- name: Deploy
diff --git a/.github/workflows/lint-workflow.yml b/.github/workflows/lint-workflow.yml
index 82bf5ce8..ee887e1d 100644
--- a/.github/workflows/lint-workflow.yml
+++ b/.github/workflows/lint-workflow.yml
@@ -21,6 +21,6 @@ jobs:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Install project dependencies (via cache)
- run: yarn --prefer-offline
+ run: yarn --frozen-lockfile --prefer-offline --ignore-optional
- name: Run ESLint
run: npx eslint src --ext .js,.jsx,.ts,.tsx --exit-on-fatal-error
diff --git a/.github/workflows/test-workflow.yml b/.github/workflows/test-workflow.yml
index 1cac379c..d12d54c5 100644
--- a/.github/workflows/test-workflow.yml
+++ b/.github/workflows/test-workflow.yml
@@ -31,7 +31,7 @@ jobs:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Install project dependencies (via cache)
- run: yarn --prefer-offline
+ run: yarn --frozen-lockfile --prefer-offline --ignore-optional
jest:
needs: install
@@ -54,7 +54,7 @@ jobs:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Install project dependencies (via cache)
- run: yarn --prefer-offline
+ run: yarn --frozen-lockfile --prefer-offline --ignore-optional
- name: Run Jest Tests
run: yarn jest
@@ -62,7 +62,7 @@ jobs:
needs: install
runs-on: ubuntu-latest
container:
- image: cypress/browsers:node-20.5.0-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1
+ image: cypress/browsers:node-20.14.0-chrome-125.0.6422.141-1-ff-126.0.1-edge-125.0.2535.85-1
options: --user 1001
strategy:
fail-fast: false
@@ -102,7 +102,7 @@ jobs:
# Starts web server for E2E tests - replace with your own server invocation
# https://docs.cypress.io/guides/continuous-integration/introduction#Boot-your-server
start: yarn start
- wait-on: 'http://localhost:3000' # Waits for above
+ wait-on: 'http://127.0.0.1:3000' # Waits for above
browser: chrome
# Records to Cypress Cloud
# https://docs.cypress.io/guides/cloud/projects#Set-up-a-project-to-record
@@ -111,6 +111,7 @@ jobs:
spec: cypress/e2e/${{ matrix.e2e }}
parallel: false # Don't use Cypress in-built parallelization
install: true
+ install-command: yarn --frozen-lockfile --prefer-offline --ignore-optional
cache-key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
env:
# For recording and parallelization to work you must set your CYPRESS_RECORD_KEY
diff --git a/.tool-versions b/.tool-versions
index da05de7b..0751e80c 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,3 +1,3 @@
# MUST match .node-version
-nodejs 20.5.1
-yarn 1.22.19
+nodejs 20.14.0
+yarn 1.22.22
diff --git a/README.md b/README.md
index 9701f339..12e6421a 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ If you wish to maintain + deploy your own version of PagerDuty Live, we recommen
#### Local Development
-1. Install [NodeJS v20.5.1](https://nodejs.org/en/blog/release/v20.5.1/) via [`asdf install`](https://github.com/asdf-vm/asdf) / [`nvm`](https://github.com/nvm-sh/nvm)
+1. Install [NodeJS v20.14.0](https://nodejs.org/en/blog/release/v20.14.0/) via [`asdf install`](https://github.com/asdf-vm/asdf) / [`nvm`](https://github.com/nvm-sh/nvm)
2. `$ git clone` repo to desired destination and `$ cd pd-live-react` into directory
diff --git a/cypress.config.js b/cypress.config.js
index 5be05f0c..78b83e79 100644
--- a/cypress.config.js
+++ b/cypress.config.js
@@ -20,7 +20,7 @@ module.exports = defineConfig({
config.env.PD_USER_TOKEN = process.env.VITE_PD_USER_TOKEN;
return config;
},
- baseUrl: 'http://localhost:3000/pd-live-react',
+ baseUrl: 'http://127.0.0.1:3000/pd-live-react',
specPattern: 'cypress/e2e/**/*.spec.{js,ts,jsx,tsx}',
testIsolation: true,
},
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/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..01cc8002
--- /dev/null
+++ b/src/components/QuerySettings/subcomponents/ColumnFilterIndicatorComponent.jsx
@@ -0,0 +1,63 @@
+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(),
diff --git a/src/util/auth.test.js b/src/util/auth.test.js
index 04c21100..022cba78 100644
--- a/src/util/auth.test.js
+++ b/src/util/auth.test.js
@@ -22,7 +22,7 @@ const unmockedFetch = global.fetch;
describe('Authentication Helper Suite', () => {
const clientId = PD_OAUTH_CLIENT_ID;
const clientSecret = PD_OAUTH_CLIENT_SECRET;
- const redirectURL = 'http://localhost:3000/';
+ const redirectURL = 'http://127.0.0.1:3000/';
const code = 'SOME_REDIRECT_CODE';
const mockAccessToken = faker.string.alphanumeric();
let codeVerifier;
diff --git a/vite.config.js b/vite.config.js
index 1dfa1df9..8b0f5f1e 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -25,6 +25,7 @@ function fixAcceptHeader404() {
export default defineConfig(() => ({
base: '/pd-live-react',
server: {
+ host: '127.0.0.1',
port: 3000,
},
build: {