diff --git a/.eslintignore b/.eslintignore index b7dab5e9..d08ff890 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ node_modules -build \ No newline at end of file +build +src/config/version.js diff --git a/.eslintrc.js b/.eslintrc.js index 69c061cf..b1515240 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,6 @@ -const { OFF, WARN, ERROR } = { +const { + OFF, WARN, ERROR, +} = { OFF: 0, WARN: 1, ERROR: 2, @@ -10,14 +12,27 @@ module.exports = { browser: true, es2021: true, }, - extends: ['plugin:react/recommended', 'airbnb', 'plugin:cypress/recommended'], + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + 'airbnb', + 'plugin:cypress/recommended', + ], parser: '@babel/eslint-parser', parserOptions: { ecmaFeatures: { jsx: true }, - ecmaVersion: 12, + ecmaVersion: 'latest', sourceType: 'module', }, - plugins: ['react', 'prettier', 'cypress'], + plugins: [ + 'react', + 'prettier', + 'cypress', + 'jsx', + 'react-refresh', + ], rules: { 'max-len': [WARN, { code: 120, ignorePattern: '^import\\W.*', ignoreTrailingComments: true }], 'object-curly-newline': [ @@ -25,12 +40,13 @@ module.exports = { { ObjectPattern: { multiline: true, minProperties: 1 }, ImportDeclaration: 'always' }, ], 'react/prop-types': OFF, // To be done in another refactor - 'react/react-in-jsx-scope': OFF, - 'react/jsx-filename-extension': [ERROR, { extensions: ['.js', '.jsx'] }], + // 'react/jsx-filename-extension': [ERROR, { extensions: ['.js', '.jsx'] }], 'no-param-reassign': [ERROR, { props: true, ignorePropertyModificationsFor: ['draft'] }], 'no-use-before-define': [ERROR, { functions: false }], 'no-plusplus': [ERROR, { allowForLoopAfterthoughts: true }], - 'jest/expect-expect': OFF, + 'react-refresh/only-export-components': OFF, // To be done in another refactor + 'react-hooks/exhaustive-deps': OFF, // To be done in another refactor + 'react-hooks/rules-of-hooks': OFF, // To be done in another refactor }, overrides: [ { @@ -46,7 +62,14 @@ module.exports = { }, ], settings: { + react: { version: '18.2' }, 'import/resolver': { + alias: { + map: [ + ['src', './src'], + ], + extensions: ['.ts', '.js', '.jsx', '.json'], + }, node: { extensions: ['.ts', '.js', '.jsx', '.json'], paths: ['node_modules/', 'node_modules/@types', 'src/'], diff --git a/.github/workflows/cd-workflow.yml b/.github/workflows/cd-workflow.yml index 37819a1c..e2fd3f6a 100644 --- a/.github/workflows/cd-workflow.yml +++ b/.github/workflows/cd-workflow.yml @@ -26,17 +26,17 @@ jobs: runs-on: ubuntu-latest env: CI: false - REACT_APP_PD_ENV: ${{ secrets.PD_ENV }} - REACT_APP_PD_SUBDOMAIN_ALLOW_LIST: '*' - REACT_APP_PD_OAUTH_CLIENT_ID: ${{ secrets.PD_OAUTH_CLIENT_ID }} - REACT_APP_PD_OAUTH_CLIENT_SECRET: ${{ secrets.PD_OAUTH_CLIENT_SECRET }} - REACT_APP_PD_REQUIRED_ABILITY: ${{ secrets.PD_REQUIRED_ABILITY }} - REACT_APP_DD_APPLICATION_ID: ${{ secrets.DD_APPLICATION_ID }} - REACT_APP_DD_CLIENT_TOKEN: ${{ secrets.DD_CLIENT_TOKEN }} - REACT_APP_DD_SITE: ${{ secrets.DD_SITE }} - REACT_APP_DD_SAMPLE_RATE: ${{ secrets.DD_SAMPLE_RATE }} - REACT_APP_DD_TRACK_INTERACTIONS: ${{ secrets.DD_TRACK_INTERACTIONS }} - REACT_APP_DD_DEFAULT_PRIVACY_LEVEL: ${{ secrets.DD_DEFAULT_PRIVACY_LEVEL }} + VITE_PD_ENV: ${{ secrets.PD_ENV }} + VITE_PD_SUBDOMAIN_ALLOW_LIST: '*' + VITE_PD_OAUTH_CLIENT_ID: ${{ secrets.PD_OAUTH_CLIENT_ID }} + VITE_PD_OAUTH_CLIENT_SECRET: ${{ secrets.PD_OAUTH_CLIENT_SECRET }} + VITE_PD_REQUIRED_ABILITY: ${{ secrets.PD_REQUIRED_ABILITY }} + VITE_DD_APPLICATION_ID: ${{ secrets.DD_APPLICATION_ID }} + VITE_DD_CLIENT_TOKEN: ${{ secrets.DD_CLIENT_TOKEN }} + VITE_DD_SITE: ${{ secrets.DD_SITE }} + VITE_DD_SAMPLE_RATE: ${{ secrets.DD_SAMPLE_RATE }} + VITE_DD_TRACK_INTERACTIONS: ${{ secrets.DD_TRACK_INTERACTIONS }} + VITE_DD_DEFAULT_PRIVACY_LEVEL: ${{ secrets.DD_DEFAULT_PRIVACY_LEVEL }} steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/test-workflow.yml b/.github/workflows/test-workflow.yml index 47b844ab..c0c0a1a7 100644 --- a/.github/workflows/test-workflow.yml +++ b/.github/workflows/test-workflow.yml @@ -14,6 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: .tool-versions + - name: Node Version + run: node -v - name: Install Yarn run: npm install -g yarn # https://github.com/actions/cache/blob/main/examples.md#node---yarn @@ -33,6 +38,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: .tool-versions + - name: Node Version + run: node -v - name: Install Yarn run: npm install -g yarn - name: Get yarn cache directory path @@ -52,7 +62,7 @@ jobs: needs: install runs-on: ubuntu-latest container: - image: cypress/browsers:node-18.16.0-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1 + image: cypress/browsers:node-20.5.0-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1 options: --user 1001 strategy: fail-fast: false @@ -64,16 +74,16 @@ jobs: - Settings/settings.spec.js - app.spec.js env: - REACT_APP_PD_ENV: 'github-ci' - REACT_APP_PD_SUBDOMAIN_ALLOW_LIST: '*' - REACT_APP_PD_USER_TOKEN: ${{ secrets.PD_INTEGRATION_USER_TOKEN }} - REACT_APP_PD_REQUIRED_ABILITY: ${{ secrets.PD_REQUIRED_ABILITY }} - REACT_APP_DD_APPLICATION_ID: ${{ secrets.DD_APPLICATION_ID }} - REACT_APP_DD_CLIENT_TOKEN: ${{ secrets.DD_CLIENT_TOKEN }} - REACT_APP_DD_SITE: ${{ secrets.DD_SITE }} - REACT_APP_DD_SAMPLE_RATE: ${{ secrets.DD_SAMPLE_RATE }} - REACT_APP_DD_TRACK_INTERACTIONS: ${{ secrets.DD_TRACK_INTERACTIONS }} - REACT_APP_DD_DEFAULT_PRIVACY_LEVEL: ${{ secrets.DD_DEFAULT_PRIVACY_LEVEL }} + VITE_PD_ENV: 'github-ci' + VITE_PD_SUBDOMAIN_ALLOW_LIST: '*' + VITE_PD_USER_TOKEN: ${{ secrets.PD_INTEGRATION_USER_TOKEN }} + VITE_PD_REQUIRED_ABILITY: ${{ secrets.PD_REQUIRED_ABILITY }} + VITE_DD_APPLICATION_ID: ${{ secrets.DD_APPLICATION_ID }} + VITE_DD_CLIENT_TOKEN: ${{ secrets.DD_CLIENT_TOKEN }} + VITE_DD_SITE: ${{ secrets.DD_SITE }} + VITE_DD_SAMPLE_RATE: ${{ secrets.DD_SAMPLE_RATE }} + VITE_DD_TRACK_INTERACTIONS: ${{ secrets.DD_TRACK_INTERACTIONS }} + VITE_DD_DEFAULT_PRIVACY_LEVEL: ${{ secrets.DD_DEFAULT_PRIVACY_LEVEL }} CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -87,7 +97,7 @@ jobs: run: node -v - name: Cypress run # Uses the official Cypress GitHub action https://github.com/cypress-io/github-action - uses: cypress-io/github-action@v5 + uses: cypress-io/github-action@v6 with: # Starts web server for E2E tests - replace with your own server invocation # https://docs.cypress.io/guides/continuous-integration/introduction#Boot-your-server diff --git a/.tool-versions b/.tool-versions index 776f9f96..da05de7b 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ # MUST match .node-version -nodejs 18.16.1 +nodejs 20.5.1 yarn 1.22.19 diff --git a/README.md b/README.md index ae85ae23..9701f339 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 v18.16.1](https://nodejs.org/en/blog/release/v18.16.1) via [`asdf install`](https://github.com/asdf-vm/asdf) / [`nvm`](https://github.com/nvm-sh/nvm) +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) 2. `$ git clone` repo to desired destination and `$ cd pd-live-react` into directory @@ -43,18 +43,18 @@ If you wish to maintain + deploy your own version of PagerDuty Live, we recommen The following _optional_ parameters can be used in a `.env` file to override PagerDuty Live during `$ yarn start`: | Parameter | Usage | | ----------- | ----------- | -| `REACT_APP_PD_ENV` | PagerDuty Live Environment Tag; defaults to `localhost-dev` if not set | -| `REACT_APP_PD_OAUTH_CLIENT_ID` | PagerDuty OAuth App client ID (created upon registering app) | -| `REACT_APP_PD_OAUTH_CLIENT_SECRET` | PagerDuty OAuth App client secret (created upon registering app) | -| `REACT_APP_PD_USER_TOKEN` | PagerDuty [Personal API Token](https://support.pagerduty.com/docs/generating-api-keys#generating-a-personal-rest-api-key); this will override OAuth login workflow if set and should be used for integration tests| -| `REACT_APP_PD_SUBDOMAIN_ALLOW_LIST` | Comma separated list of allowed subdomains (e.g. `acme-prod,acme-dev`) | -| `REACT_APP_PD_REQUIRED_ABILITY` | PagerDuty account-level [ability](https://developer.pagerduty.com/api-reference/b3A6Mjc0ODEwMg-list-abilities) required to use application | -| `REACT_APP_DD_APPLICATION_ID` | Datadog [RUM Application ID](https://docs.datadoghq.com/real_user_monitoring/browser/#setup) | -| `REACT_APP_DD_CLIENT_TOKEN` | Datadog [RUM Client Token](https://docs.datadoghq.com/account_management/api-app-keys/#client-tokens) | -| `REACT_APP_DD_SITE` | Datadog [site](https://docs.datadoghq.com/agent/basic_agent_usage/?tab=agentv6v7#datadog-site) (e.g. `datadoghq.com`) | -| `REACT_APP_DD_SAMPLE_RATE` | Datadog [RUM Sample Rate](https://docs.datadoghq.com/real_user_monitoring/browser/#browser-and-session-replay-sampling-configuration) (e.g. `100`) | -| `REACT_APP_DD_TRACK_INTERACTIONS` | Datadog [RUM Track Interactions](https://docs.datadoghq.com/real_user_monitoring/browser/tracking_user_actions/?tab=npm) (e.g. `true`) | -| `REACT_APP_DD_DEFAULT_PRIVACY_LEVEL` | Datadog [RUM Default Privacy Level](https://docs.datadoghq.com/real_user_monitoring/session_replay/privacy_options/?tab=maskuserinput) (e.g. `mask-user-input`) | +| `VITE_PD_ENV` | PagerDuty Live Environment Tag; defaults to `localhost-dev` if not set | +| `VITE_PD_OAUTH_CLIENT_ID` | PagerDuty OAuth App client ID (created upon registering app) | +| `VITE_PD_OAUTH_CLIENT_SECRET` | PagerDuty OAuth App client secret (created upon registering app) | +| `VITE_PD_USER_TOKEN` | PagerDuty [Personal API Token](https://support.pagerduty.com/docs/generating-api-keys#generating-a-personal-rest-api-key); this will override OAuth login workflow if set and should be used for integration tests| +| `VITE_PD_SUBDOMAIN_ALLOW_LIST` | Comma separated list of allowed subdomains (e.g. `acme-prod,acme-dev`) | +| `VITE_PD_REQUIRED_ABILITY` | PagerDuty account-level [ability](https://developer.pagerduty.com/api-reference/b3A6Mjc0ODEwMg-list-abilities) required to use application | +| `VITE_DD_APPLICATION_ID` | Datadog [RUM Application ID](https://docs.datadoghq.com/real_user_monitoring/browser/#setup) | +| `VITE_DD_CLIENT_TOKEN` | Datadog [RUM Client Token](https://docs.datadoghq.com/account_management/api-app-keys/#client-tokens) | +| `VITE_DD_SITE` | Datadog [site](https://docs.datadoghq.com/agent/basic_agent_usage/?tab=agentv6v7#datadog-site) (e.g. `datadoghq.com`) | +| `VITE_DD_SAMPLE_RATE` | Datadog [RUM Sample Rate](https://docs.datadoghq.com/real_user_monitoring/browser/#browser-and-session-replay-sampling-configuration) (e.g. `100`) | +| `VITE_DD_TRACK_INTERACTIONS` | Datadog [RUM Track Interactions](https://docs.datadoghq.com/real_user_monitoring/browser/tracking_user_actions/?tab=npm) (e.g. `true`) | +| `VITE_DD_DEFAULT_PRIVACY_LEVEL` | Datadog [RUM Default Privacy Level](https://docs.datadoghq.com/real_user_monitoring/session_replay/privacy_options/?tab=maskuserinput) (e.g. `mask-user-input`) | ## Testing @@ -63,7 +63,7 @@ The following scripts have been created to run unit, component, and integration - `$ yarn jest` (Jest Unit/Component) - `$ yarn cypress:run:local` (Cypress Integration with headless Chromedriver) -Please note that running integration tests will require environment variable `REACT_APP_PD_USER_TOKEN` set. +Please note that running integration tests will require environment variable `VITE_PD_USER_TOKEN` set. The integration tests also assume the PagerDuty account associated with the above user token has been setup with the following [Terraform environment](https://github.com/pagerduty/pd-live-integration-test-environment). diff --git a/craco.config.js b/craco.config.js deleted file mode 100644 index 6527ad2a..00000000 --- a/craco.config.js +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path'); - -module.exports = { - module: { - rules: [ - { - test: /\.scss$/, - loader: 'sass-loader', - options: { - sassOptions: { - includePaths: [path.resolve(__dirname, 'src/assets/images')], - }, - }, - }, - ], - }, - // FIX: https://github.com/facebook/create-react-app/discussions/11767#discussioncomment-2421668 - webpack: { - configure: { - ignoreWarnings: [ - function ignoreSourcemapsloaderWarnings(warning) { - return ( - warning.module - && warning.module.resource.includes('node_modules') - && warning.details - && warning.details.includes('source-map-loader') - ); - }, - ], - }, - }, -}; diff --git a/cypress.config.js b/cypress.config.js index a1d5f0b3..eb24ae27 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,6 +1,9 @@ -const { defineConfig } = require("cypress"); -const dotenv = require("dotenv"); -const cypressFailFast = require("cypress-fail-fast/plugin"); +/* eslint-disable import/no-extraneous-dependencies */ +const { + defineConfig, +} = require('cypress'); +const dotenv = require('dotenv'); +const cypressFailFast = require('cypress-fail-fast/plugin'); module.exports = defineConfig({ video: false, @@ -13,25 +16,14 @@ module.exports = defineConfig({ setupNodeEvents(on, config) { dotenv.config(); cypressFailFast(on, config); - config.env.PD_USER_TOKEN = process.env.REACT_APP_PD_USER_TOKEN; + // eslint-disable-next-line no-param-reassign + config.env.PD_USER_TOKEN = process.env.VITE_PD_USER_TOKEN; return config; }, - baseUrl: "http://localhost:3000/pd-live-react", - specPattern: "cypress/e2e/**/*.spec.{js,ts,jsx,tsx}", + baseUrl: 'http://localhost:3000/pd-live-react', + specPattern: 'cypress/e2e/**/*.spec.{js,ts,jsx,tsx}', // Cypress 12 introduces Test Isolation by default which breaks our current tests // https://docs.cypress.io/guides/references/migration-guide#Test-Isolation - testIsolation: false - }, - - component: { - setupNodeEvents(on, config) {}, - specPattern: "src/**/*.spec.{js,ts,jsx,tsx}", - }, - - component: { - devServer: { - framework: "create-react-app", - bundler: "webpack", - }, + testIsolation: false, }, }); diff --git a/cypress/e2e/Incidents/incidents.spec.js b/cypress/e2e/Incidents/incidents.spec.js index 43ff6e06..0fe9d6b8 100644 --- a/cypress/e2e/Incidents/incidents.spec.js +++ b/cypress/e2e/Incidents/incidents.spec.js @@ -31,7 +31,10 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { ['Responders', 'responders'], ['Latest Log Entry Type', 'latest_log_entry_type'], ]; - manageIncidentTableColumns('add', columns.map((column) => column[1])); + manageIncidentTableColumns( + 'add', + columns.map((column) => column[1]), + ); waitForIncidentTable(); }); @@ -44,7 +47,10 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { ['Responders', 'responders'], ['Latest Log Entry Type', 'latest_log_entry_type'], ]; - manageIncidentTableColumns('add', columns.map((column) => column[1])); + manageIncidentTableColumns( + 'add', + columns.map((column) => column[1]), + ); } waitForIncidentTable(); }); @@ -64,6 +70,19 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { selectAllIncidents(); }); + it('Shift-select multiple incidents', () => { + selectIncident(0); + selectIncident(4, true); + cy.get('.selected-incidents-badge').then(($el) => { + const text = $el.text(); + const incidentNumbers = text.split(' ')[0].split('/'); + expect(incidentNumbers[0]).to.equal('5'); + }); + // Unselect all incidents for the next run + selectAllIncidents(); + selectAllIncidents(); + }); + it('Acknowledge singular incident', () => { const incidentIdx = 0; selectIncident(incidentIdx); @@ -110,7 +129,12 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { addNote(note); checkActionAlertsModalContent('have been updated with a note'); checkIncidentCellContent(incidentId, 'Latest Note', note); - checkIncidentCellContentHasLink(incidentId, 'Latest Note', 'example.com', 'http://example.com'); + checkIncidentCellContentHasLink( + incidentId, + 'Latest Note', + 'example.com', + 'http://example.com', + ); }); }); @@ -123,7 +147,12 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { addNote(note); checkActionAlertsModalContent('have been updated with a note'); checkIncidentCellContent(incidentId, 'Latest Note', note); - checkIncidentCellContentHasLink(incidentId, 'Latest Note', 'test@example.com', 'mailto:test@example.com'); + checkIncidentCellContentHasLink( + incidentId, + 'Latest Note', + 'test@example.com', + 'mailto:test@example.com', + ); }); }); diff --git a/cypress/e2e/Query/query.spec.js b/cypress/e2e/Query/query.spec.js index f9a2146f..ca3ab36f 100644 --- a/cypress/e2e/Query/query.spec.js +++ b/cypress/e2e/Query/query.spec.js @@ -1,5 +1,5 @@ /* eslint-disable cypress/unsafe-to-chain-command */ -import moment from 'moment'; +import moment from 'moment/min/moment-with-locales'; import gb from 'date-fns/locale/en-GB'; import { @@ -53,10 +53,7 @@ describe('Query Incidents', { failFast: { enabled: false } }, () => { .subtract(1, 'days') .set({ hour: 0, minute: 0, second: 0, millisecond: 0 }); cy.get('#query-date-input').click(); - cy.get('#since-date-input') - .clear() - .type(queryDate.format('DD/MM/yyyy')) - .type('{enter}'); + cy.get('#since-date-input').clear().type(queryDate.format('DD/MM/yyyy')).type('{enter}'); cy.get('#query-date-submit').click(); waitForIncidentTable(); diff --git a/cypress/e2e/Search/search.spec.js b/cypress/e2e/Search/search.spec.js index bb77e1a3..8dd9bd4b 100644 --- a/cypress/e2e/Search/search.spec.js +++ b/cypress/e2e/Search/search.spec.js @@ -4,9 +4,11 @@ import { acceptDisclaimer, waitForIncidentTable, - // activateButton, - // priorityNames, + addNote, + checkActionAlertsModalContent, selectIncident, + selectAllIncidents, + updateFuzzySearch, } from '../../support/util/common'; describe('Search Incidents', { failFast: { enabled: false } }, () => { @@ -24,10 +26,11 @@ describe('Search Incidents', { failFast: { enabled: false } }, () => { it('Search for `Service A1` returns incidents only on Service A1', () => { cy.get('#global-search-input').clear().type('Service A1'); - cy.wait(5000); + cy.wait(1000); cy.get('[data-incident-header="Service"]').each(($el) => { cy.wrap($el).should('have.text', 'Service A1'); }); + cy.get('#global-search-input').clear(); }); it('Search for 2nd selected incident returns exactly 1 incident only', () => { @@ -41,11 +44,67 @@ describe('Search Incidents', { failFast: { enabled: false } }, () => { const text = $el.text().split(' ')[0]; expect(text).to.equal('1/1'); }); + // Click the select all checkbox twice to unselect all + cy.get('#global-search-input').clear(); + cy.wait(1000); + selectAllIncidents(); + selectAllIncidents(); }); it('Search for `zzzzzz` returns no incidents', () => { cy.get('#global-search-input').clear().type('zzzzzz'); - cy.wait(5000); + cy.wait(1000); + cy.get('.empty-incidents-badge').should('be.visible'); + cy.get('#global-search-input').clear(); + }); + + it('Fuzzy search disabled returns incident with note exact match', () => { + const incidentIdx = 0; + selectIncident(incidentIdx); + + cy.get(`@selectedIncidentId_${incidentIdx}`).then(() => { + addNote('foobar'); + checkActionAlertsModalContent('have been updated with a note'); + selectIncident(incidentIdx); + cy.get('#global-search-input').clear().type('foobar'); + cy.wait(1000); + cy.get('[data-incident-header="Latest Note"]').each(($el) => { + cy.wrap($el).should('have.text', 'foobar'); + }); + }); + cy.get('#global-search-input').clear(); + }); + + it('Fuzzy search disabled does not return incident with note fuzzy match', () => { + cy.get('#global-search-input').clear().type('foobaz'); + cy.wait(1000); cy.get('.empty-incidents-badge').should('be.visible'); + cy.get('#global-search-input').clear(); + }); + + it('Fuzzy search enabled returns incident with note fuzzy match', () => { + updateFuzzySearch(true); + const incidentIdx = 0; + selectIncident(incidentIdx); + + cy.get(`@selectedIncidentId_${incidentIdx}`).then(() => { + cy.get('#global-search-input').clear().type('foobaz'); + cy.wait(1000); + cy.get('[data-incident-header="Latest Note"]').each(($el) => { + cy.wrap($el).should('have.text', 'foobar'); + }); + }); + cy.get('#global-search-input').clear(); + }); + + it('Column filtering on Service column for `A1` returns incidents only on Service A1', () => { + cy.get('#service-filter-icon').realHover(); + cy.get('input[placeholder="Filter"]').filter(':visible').click().type('A1'); + cy.wait(1000); + cy.get('[data-incident-header="Service"]').each(($el) => { + cy.wrap($el).should('have.text', 'Service A1'); + }); + cy.get('#service-filter-icon').realHover(); + cy.get('button[aria-label="Clear Filter"]').filter(':visible').click(); }); }); diff --git a/cypress/e2e/Settings/settings.spec.js b/cypress/e2e/Settings/settings.spec.js index 729de3b2..75e06573 100644 --- a/cypress/e2e/Settings/settings.spec.js +++ b/cypress/e2e/Settings/settings.spec.js @@ -1,6 +1,6 @@ /* eslint-disable cypress/unsafe-to-chain-command */ -import moment from 'moment'; +import moment from 'moment/min/moment-with-locales'; import 'moment/min/locales.min'; import { @@ -42,11 +42,7 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { it('Change user locale to fr', () => { const localeName = 'Français'; - updateUserLocale( - localeName, - 'Settings', - 'Paramètres du profil utilisateur mis à jour', - ); + updateUserLocale(localeName, 'Settings', 'Paramètres du profil utilisateur mis à jour'); }); it('Change user locale to en-US', () => { @@ -54,35 +50,19 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { const expectedSinceDateFormat = moment().subtract(1, 'days').format('L'); const expectedIncidentDateFormat = moment().format('LL'); - updateUserLocale( - localeName, - 'Paramètres', - 'Updated user profile settings', - ); + updateUserLocale(localeName, 'Paramètres', 'Updated user profile settings'); cy.get('#query-date-input').should('contain', expectedSinceDateFormat); cy.get('[data-incident-header="Created At"][data-incident-row-cell-idx="0"]') .should('be.visible') .should('contain', expectedIncidentDateFormat); }); - [ - '1 Day', - '3 Days', - '1 Week', - '2 Weeks', - '1 Month', - '3 Months', - '6 Months', - ].forEach((tenor) => { + ['1 Day', '3 Days', '1 Week', '2 Weeks', '1 Month', '3 Months', '6 Months'].forEach((tenor) => { it(`Update default since date lookback to ${tenor}`, () => { const [sinceDateNum, sinceDateTenor] = tenor.split(' '); const expectedDate = moment().subtract(Number(sinceDateNum), sinceDateTenor).format('L'); updateDefaultSinceDateLookback(tenor); - updateUserLocale( - 'English (United States)', - 'Settings', - 'Updated user profile settings', - ); + updateUserLocale('English (United States)', 'Settings', 'Updated user profile settings'); cy.get('#query-date-input').should('contain', expectedDate); }); }); @@ -93,9 +73,7 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { cy.window() .its('store') .invoke('getState') - .then((state) => expect( - Number(state.settings.maxRateLimit), - ).to.equal(maxRateLimit)); + .then((state) => expect(Number(state.settings.maxRateLimit)).to.equal(maxRateLimit)); }); it('Add standard columns to incident table', () => { @@ -105,10 +83,15 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { ['Group', 'service_group'], ['Component', 'source_component'], ]; - manageIncidentTableColumns('add', columns.map((column) => column[1])); - columns.map((column) => column[0]).forEach((columnName) => { - cy.get(`[data-column-name="${columnName}"]`).scrollIntoView().should('be.visible'); - }); + manageIncidentTableColumns( + 'add', + columns.map((column) => column[1]), + ); + columns + .map((column) => column[0]) + .forEach((columnName) => { + cy.get(`[data-column-name="${columnName}"]`).scrollIntoView().should('be.visible'); + }); }); it('Remove standard columns from incident table', () => { @@ -116,13 +99,18 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { ['Service', 'service'], ['Latest Note', 'latest_note'], ]; - manageIncidentTableColumns('remove', columns.map((column) => column[1])); + manageIncidentTableColumns( + 'remove', + columns.map((column) => column[1]), + ); // Assert against DOM to see if element has been removed cy.get('body').then((body) => { - columns.map((column) => column[0]).forEach((columnName) => { - expect(body.find(`[data-column-name="${columnName}"]`).length).to.equal(0); - }); + columns + .map((column) => column[0]) + .forEach((columnName) => { + expect(body.find(`[data-column-name="${columnName}"]`).length).to.equal(0); + }); }); }); @@ -131,8 +119,9 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { const targetColumn = ['Priority', 'priority']; let newColumnWidth; - cy.get(`[data-column-name="${columnToResize[0]}"] > .resizer`) - .trigger('mousedown', { which: 1 }); + cy.get(`[data-column-name="${columnToResize[0]}"] > .resizer`).trigger('mousedown', { + which: 1, + }); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(`[data-column-name="${targetColumn[0]}"] > .resizer`) @@ -195,9 +184,7 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { cy.window() .its('store') .invoke('getState') - .then((state) => expect( - state.settings.darkMode, - ).to.equal(!currentDarkMode)); + .then((state) => expect(state.settings.darkMode).to.equal(!currentDarkMode)); }); it('Update relative dates', () => { @@ -206,9 +193,7 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { cy.window() .its('store') .invoke('getState') - .then((state) => expect( - state.settings.relativeDates, - ).to.equal(relativeDates)); + .then((state) => expect(state.settings.relativeDates).to.equal(relativeDates)); if (relativeDates) { checkIncidentCellContentAllRows('Created At', /second[s]? ago|minute[s]? ago|hour[s]? ago/); @@ -217,13 +202,16 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { }); it('Add age column to incident table', () => { - const columns = [ - ['Age', 'age'], - ]; - manageIncidentTableColumns('add', columns.map((column) => column[1])); - columns.map((column) => column[0]).forEach((columnName) => { - cy.get(`[data-column-name="${columnName}"]`).scrollIntoView().should('be.visible'); - }); + const columns = [['Age', 'age']]; + manageIncidentTableColumns( + 'add', + columns.map((column) => column[1]), + ); + columns + .map((column) => column[0]) + .forEach((columnName) => { + cy.get(`[data-column-name="${columnName}"]`).scrollIntoView().should('be.visible'); + }); checkIncidentCellContentAllRows('Age', /second[s]?|minute[s]?|hour[s]?/); }); @@ -246,7 +234,9 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { cy.get('.settings-panel-dropdown').click(); cy.get('.dropdown-item').contains('Load/Save Presets').click(); // cy.get('#load-presets-button').click(); - cy.get('input[type=file]#load-presets-file').selectFile('cypress/downloads/presets.json', { force: true }); + cy.get('input[type=file]#load-presets-file').selectFile('cypress/downloads/presets.json', { + force: true, + }); checkActionAlertsModalContent('Presets loaded'); waitForIncidentTable(); // Check some settings configured above have been restored @@ -256,14 +246,14 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { ['Group', 'service_group'], ['Component', 'source_component'], ]; - columns.map((column) => column[0]).forEach((columnName) => { - cy.get(`[data-column-name="${columnName}"]`).scrollIntoView().should('be.visible'); - }); + columns + .map((column) => column[0]) + .forEach((columnName) => { + cy.get(`[data-column-name="${columnName}"]`).scrollIntoView().should('be.visible'); + }); cy.window() .its('store') .invoke('getState') - .then((state) => expect( - state.settings.darkMode, - ).to.equal(true)); + .then((state) => expect(state.settings.darkMode).to.equal(true)); }); }); diff --git a/cypress/e2e/app.spec.js b/cypress/e2e/app.spec.js index 2483a02c..d9108cc9 100644 --- a/cypress/e2e/app.spec.js +++ b/cypress/e2e/app.spec.js @@ -1,4 +1,4 @@ -import moment from 'moment'; +import moment from 'moment/min/moment-with-locales'; import { acceptDisclaimer, waitForIncidentTable, pd, @@ -92,11 +92,14 @@ describe('PagerDuty Live', () => { .set({ hour: 0, minute: 0, second: 0, millisecond: 0 }) .toISOString(); const until = moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).toISOString(); - cy.intercept('GET', [ - 'https://api.pagerduty.com/incidents', - '?limit=100&total=true&offset=0', - `&since=${since}&until=${until}*`, - ].join('')).as('getUrl'); + cy.intercept( + 'GET', + [ + 'https://api.pagerduty.com/incidents', + '?limit=100&total=true&offset=0', + `&since=${since}&until=${until}*`, + ].join(''), + ).as('getUrl'); cy.visit( `http://localhost:3000/pd-live-react/?disable-polling=true&since=${since}&until=${until}`, diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 545a0e88..19175943 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. @@ -21,4 +22,4 @@ import './commands'; import 'cypress-fail-fast'; import '@4tw/cypress-drag-drop'; -import "cypress-real-events"; +import 'cypress-real-events'; diff --git a/cypress/support/util/common.js b/cypress/support/util/common.js index c598c51a..e909e089 100644 --- a/cypress/support/util/common.js +++ b/cypress/support/util/common.js @@ -28,14 +28,14 @@ export const waitForIncidentTable = () => { cy.get('.incident-table-fixed-list').scrollTo('top', { ensureScrollable: false }); }; -export const selectIncident = (incidentIdx = 0) => { +export const selectIncident = (incidentIdx = 0, shiftKey = false) => { const selector = `[data-incident-row-idx="${incidentIdx}"]`; cy.get(selector).invoke('attr', 'data-incident-id').as(`selectedIncidentId_${incidentIdx}`); - cy.get(selector).click(); + cy.get(selector).click({ shiftKey }); }; export const selectAllIncidents = () => { - cy.get('#select-all', { timeout: 20000 }).click(); + cy.get('#select-all', { timeout: 20000 }).click({ force: true }); }; export const checkNoIncidentsSelected = () => { @@ -53,7 +53,9 @@ export const checkActionAlertsModalContent = (content) => { export const checkPopoverContent = (incidentId, incidentHeader, content) => { cy.wait(2000); - cy.get(`[data-incident-header="${incidentHeader}"][data-incident-cell-id="${incidentId}"]`).within(() => { + cy.get( + `[data-incident-header="${incidentHeader}"][data-incident-cell-id="${incidentId}"]`, + ).within(() => { cy.get('.chakra-avatar__group').realHover(); cy.get('.chakra-popover__popper').should('be.visible').contains(content, { timeout: 10000 }); }); @@ -237,10 +239,9 @@ export const manageCustomAlertColumnDefinitions = (customAlertColumnDefinitions) cy.get('.settings-panel-dropdown').click(); cy.get('.dropdown-item').contains('Columns').click(); - cy.get('#custom-columns-card-body .chakra-icon') - .each(($el) => { - cy.wrap($el).click(); - }); + cy.get('#custom-columns-card-body .chakra-icon').each(($el) => { + cy.wrap($el).click(); + }); customAlertColumnDefinitions.forEach((customAlertColumnDefinition) => { const [header, accessorPath] = customAlertColumnDefinition.split(':'); @@ -319,6 +320,20 @@ export const updateRelativeDates = (relativeDates = false) => { checkActionAlertsModalContent('Updated user profile settings'); }; +export const updateFuzzySearch = (fuzzySearch = false) => { + cy.get('.settings-panel-dropdown').click(); + cy.get('.dropdown-item').contains('Settings').click(); + + if (fuzzySearch) { + cy.get('#fuzzy-search-switch').check({ force: true }); + } else { + cy.get('#fuzzy-search-switch').uncheck({ force: true }); + } + + cy.get('#save-settings-button').click(); + checkActionAlertsModalContent('Updated user profile settings'); +}; + export const updateDarkMode = () => { cy.get('[aria-label="Toggle Dark Mode"]').click(); }; diff --git a/i18next-parser.config.js b/i18next-parser.config.js index 6635ae66..6198a562 100644 --- a/i18next-parser.config.js +++ b/i18next-parser.config.js @@ -8,6 +8,7 @@ module.exports = { defaultNamespace: 'translation', // Default namespace used in your i18next config + // eslint-disable-next-line no-unused-vars defaultValue: (locale, namespace, key, value) => key, // Default value to give to empty keys // You may also specify a function accepting the locale, namespace, and key as arguments @@ -21,7 +22,10 @@ module.exports = { keySeparator: false, // Key separator used in your translation keys - // If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance. + // If you want to use plain english keys, separators such as `.` and `:` will conflict. + // You might want to set `keySeparator: false` and `namespaceSeparator: false`. + // That way, `t('Status: Loading...')` will not think that there are a namespace and + // three separator dots for instance. // see below for more details lexers: { @@ -48,7 +52,10 @@ module.exports = { namespaceSeparator: false, // Namespace separator used in your translation keys - // If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance. + // If you want to use plain english keys, separators such as `.` and `:` will conflict. + // You might want to set `keySeparator: false` and `namespaceSeparator: false`. + // That way, `t('Status: Loading...')` will not think that there are a namespace and + // three separator dots for instance. output: 'src/locales/$LOCALE/$NAMESPACE.json', // Supports $LOCALE and $NAMESPACE injection @@ -57,14 +64,16 @@ module.exports = { pluralSeparator: '_', // Plural separator used in your translation keys - // If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys. + // If you want to use plain english keys, separators such as `_` might conflict. + // You might want to set `pluralSeparator` to a different string that does not occur in your keys. input: undefined, // An array of globs that describe where to look for source files // relative to the location of the configuration file sort: true, - // Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters) + // Whether or not to sort the catalog. Can also be a compareFunction + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters) verbose: false, // Display info about the parsing including some stats diff --git a/public/index.html b/index.html similarity index 62% rename from public/index.html rename to index.html index 44610a34..45b6c3b6 100644 --- a/public/index.html +++ b/index.html @@ -3,31 +3,23 @@
- + - + - - +