diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1b38ea58..f8a8f39c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/snyk-security.yml b/.github/workflows/snyk-security.yml index 1a33d328..5780a377 100644 --- a/.github/workflows/snyk-security.yml +++ b/.github/workflows/snyk-security.yml @@ -60,6 +60,6 @@ jobs: # Push the Snyk Code results into GitHub Code Scanning tab - name: Upload result to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: snyk-code.sarif diff --git a/cypress.config.js b/cypress.config.js index e1e3d7ce..5be05f0c 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -22,8 +22,6 @@ module.exports = defineConfig({ }, 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, + testIsolation: true, }, }); diff --git a/cypress/e2e/Incidents/incidents.spec.js b/cypress/e2e/Incidents/incidents.spec.js index ef7733e4..14507255 100644 --- a/cypress/e2e/Incidents/incidents.spec.js +++ b/cypress/e2e/Incidents/incidents.spec.js @@ -25,10 +25,12 @@ import { manageIncidentTableColumns, priorityNames, selectAlert, + waitForAlerts, } from '../../support/util/common'; -describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { - before(() => { +describe('Manage Open Incidents', { failFast: { enabled: true } }, () => { + // We use beforeEach as each test will reload/clear the session + beforeEach(() => { acceptDisclaimer(); const columns = [ ['Responders', 'responders'], @@ -41,47 +43,28 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { waitForIncidentTable(); }); - // We use beforeEach as each test will reload/clear the session - beforeEach(() => { - // Handle failing tests by clearing cache - if (cy.state('test').currentRetry() > 1) { - acceptDisclaimer(); - const columns = [ - ['Responders', 'responders'], - ['Latest Log Entry Type', 'latest_log_entry_type'], - ]; - manageIncidentTableColumns( - 'add', - columns.map((column) => column[1]), - ); - } - waitForIncidentTable(); - }); - afterEach(() => { checkNoIncidentsSelected(); }); it('Select all incidents', () => { selectAllIncidents(); - cy.get('.selected-incidents-badge').then(($el) => { + cy.get('.selected-incidents-badge').should(($el) => { const text = $el.text(); const incidentNumbers = text.split(' ')[0].split('/'); expect(incidentNumbers[0]).to.equal(incidentNumbers[1]); }); - // Unselect all incidents for the next run + // Unselect all incidents selectAllIncidents(); - }); - - it('Shift-select multiple incidents', () => { + // Shift-select multiple incidents selectIncident(0); selectIncident(4, true); - cy.get('.selected-incidents-badge').then(($el) => { + cy.get('.selected-incidents-badge').should(($el) => { const text = $el.text(); const incidentNumbers = text.split(' ')[0].split('/'); expect(incidentNumbers[0]).to.equal('5'); }); - // Unselect all incidents for the next run + // Unselect all incidents selectAllIncidents(); selectAllIncidents(); }); @@ -97,41 +80,36 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { }); }); - it('Add note to singular incident', () => { + it('Add notes to singular incidents', () => { const note = 'All your base are belong to us'; - const incidentIdx = 0; - selectIncident(incidentIdx); + selectIncident(0); - cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { + cy.get('@selectedIncidentId_0').then((incidentId) => { addNote(note); checkActionAlertsModalContent('have been updated with a note'); checkIncidentCellContent(incidentId, 'Latest Note', note); checkIncidentCellContent(incidentId, 'Latest Log Entry Type', 'annotate'); }); - }); - it('Add a very long note to singular incident which overflows', () => { - const note = 'This note is so long that I gave up writing a novel and decided to quit!'; - const incidentIdx = 1; - selectIncident(incidentIdx); + // Add a very long note to singular incident which overflows + const noteLong = 'This note is so long that I gave up writing a novel and decided to quit!'; + selectIncident(1); - cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { - addNote(note); + cy.get('@selectedIncidentId_1').then((incidentId) => { + addNote(noteLong); checkActionAlertsModalContent('have been updated with a note'); - checkIncidentCellContent(incidentId, 'Latest Note', note); + checkIncidentCellContent(incidentId, 'Latest Note', noteLong); checkIncidentCellContent(incidentId, 'Latest Log Entry Type', 'annotate'); }); - }); - it('Add note with URL to singular incident', () => { - const note = 'This note has a URL example.com included'; - const incidentIdx = 2; - selectIncident(incidentIdx); + // 'Add note with URL to singular incident + const noteURL = 'This note has a URL example.com included'; + selectIncident(2); - cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { - addNote(note); + cy.get('@selectedIncidentId_2').then((incidentId) => { + addNote(noteURL); checkActionAlertsModalContent('have been updated with a note'); - checkIncidentCellContent(incidentId, 'Latest Note', note); + checkIncidentCellContent(incidentId, 'Latest Note', noteURL); checkIncidentCellContentHasLink( incidentId, 'Latest Note', @@ -139,17 +117,15 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { 'http://example.com', ); }); - }); - it('Add note with email to singular incident', () => { - const note = 'This note has an email test@example.com included'; - const incidentIdx = 3; - selectIncident(incidentIdx); + // Add note with email to singular incident + const noteEmail = 'This note has an email test@example.com included'; + selectIncident(3); - cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { - addNote(note); + cy.get('@selectedIncidentId_3').then((incidentId) => { + addNote(noteEmail); checkActionAlertsModalContent('have been updated with a note'); - checkIncidentCellContent(incidentId, 'Latest Note', note); + checkIncidentCellContent(incidentId, 'Latest Note', noteEmail); checkIncidentCellContentHasLink( incidentId, 'Latest Note', @@ -159,13 +135,13 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { }); }); - // Assumed environment has 3 levels on escalation policy - for (let escalationLevel = 1; escalationLevel < 4; escalationLevel++) { - it(`Escalate singular incident to level: ${escalationLevel}`, () => { - // Ensure that only high urgency incidents are visible - // deactivateButton('query-urgency-low-button'); - cy.get('.query-urgency-low-button').uncheck({ force: true }); - waitForIncidentTable(); + it('Escalate singular incident to multiple levels', () => { + // Ensure that only high urgency incidents are visible + cy.get('.query-urgency-low-button').uncheck({ force: true }); + waitForIncidentTable(); + + // Assumed environment has 3 levels on escalation policy + for (let escalationLevel = 1; escalationLevel < 4; escalationLevel++) { const incidentIdx = 0; selectIncident(incidentIdx); cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { @@ -173,75 +149,74 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { checkActionAlertsModalContent(`have been manually escalated to level ${escalationLevel}`); checkIncidentCellContent(incidentId, 'Latest Log Entry Type', /escalate|notify/); }); - }); - } + } + }); - it('Reassign singular incident to User A1', () => { - // activateButton('query-urgency-low-button'); // bring back low urgency incidents + it('Reassign singular incidents to User and Team', () => { cy.get('.query-urgency-low-button').check({ force: true }); - const assignment = 'User A1'; + let assignment = 'User A1'; selectIncident(0); reassign(assignment); checkActionAlertsModalContent(`have been reassigned to ${assignment}`); - }); - it('Reassign singular incident to Team A', () => { - const assignment = 'Team A'; + assignment = 'Team A'; selectIncident(1); reassign(assignment); checkActionAlertsModalContent(`have been reassigned to ${assignment}`); }); - it('Add responder (User A1) to singular incident', () => { - const responders = ['User A1']; + it('Add User and Team responders to singular incident', () => { + let responders = ['User A1']; const message = 'Need help with this incident'; - const incidentIdx = 0; + let incidentIdx = 0; selectIncident(incidentIdx); addResponders(responders, message); checkActionAlertsModalContent('Requested additional response for incident(s)'); cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { checkIncidentCellContent(incidentId, 'Responders', 'UA'); - checkPopoverContent(incidentId, 'Responders', 'user_a1@example.com'); + checkPopoverContent(incidentId, 'Responders', 'User A'); checkIncidentCellContent(incidentId, 'Latest Log Entry Type', 'responder_request'); }); - }); - it('Add responder (Team A) to singular incident', () => { - const responders = ['Team A']; - const message = 'Need help with this incident'; - selectIncident(0); + responders = ['Team A']; + incidentIdx = 1; + selectIncident(incidentIdx); addResponders(responders, message); checkActionAlertsModalContent('Requested additional response for incident(s)'); + cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { + checkIncidentCellContent(incidentId, 'Responders', 'UA'); + checkPopoverContent(incidentId, 'Responders', 'User A'); + checkIncidentCellContent(incidentId, 'Latest Log Entry Type', 'responder_request'); + }); }); it('Add multiple responders (Team A + Team B) to singular incident', () => { const responders = ['Team A', 'Team B']; const message = "Need everyone's help with this incident"; - selectIncident(0); + const incidentIdx = 0; + selectIncident(incidentIdx); addResponders(responders, message); checkActionAlertsModalContent('Requested additional response for incident(s)'); + cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { + checkIncidentCellContent(incidentId, 'Responders', 'UA'); + checkIncidentCellContent(incidentId, 'Responders', 'UB'); + checkPopoverContent(incidentId, 'Responders', 'User A'); + checkPopoverContent(incidentId, 'Responders', 'User B'); + checkIncidentCellContent(incidentId, 'Latest Log Entry Type', 'responder_request'); + }); }); - it('Snooze singular incident for specified duration (5 minutes)', () => { - const duration = '5 minutes'; + it('Snooze singular incidents with specified or custom durations', () => { selectIncident(0); - snooze(duration); + snooze('5 minutes'); checkActionAlertsModalContent('have been snoozed.'); - }); - it('Snooze singular incident for custom duration (2 hours)', () => { - const type = 'hours'; - const option = 2; - selectIncident(0); - snoozeCustom(type, option); + selectIncident(1); + snoozeCustom('hours', 2); checkActionAlertsModalContent('have been snoozed.'); - }); - it('Snooze singular incident for custom duration (tomorrow for 9:00 AM)', () => { - const type = 'tomorrow'; - const option = '9:00 AM'; - selectIncident(0); - snoozeCustom(type, option); + selectIncident(2); + snoozeCustom('tomorrow', '9:00 AM'); checkActionAlertsModalContent('have been snoozed.'); }); @@ -257,10 +232,13 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { selectIncident(0); cy.get('#incident-action-resolve-button').click(); checkActionAlertsModalContent('have been resolved'); + // and now show all resolved status (#380) + cy.get('.query-status-resolved-button').check({ force: true }); + waitForIncidentTable(); }); - priorityNames.forEach((priorityName, idx) => { - it(`Update priority of singular incident to ${priorityName}`, () => { + it('Update priority of singular incidents', () => { + priorityNames.forEach((priorityName, idx) => { const incidentIdx = idx; selectIncident(incidentIdx); cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { @@ -274,10 +252,6 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { }); it('Run external system sync on singular incident', () => { - // For some reason this doesn't work on first attempt - clearing cache as a workaround - // acceptDisclaimer(); - // waitForIncidentTable(); - const externalSystemName = 'ServiceNow'; selectIncident(0); runExternalSystemSync(externalSystemName); @@ -298,33 +272,35 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { runResponsePlay(responsePlayName); checkActionAlertsModalContent(`Ran "${responsePlayName}" response play for incident(s)`); }); +}); - it('Hovering over Num Alerts count should popup alert table', () => { - // Remove the other columns to make it easier to see the alert count without scrolling - const removeColumns = [ - ['Responders', 'responders'], - ['Latest Log Entry Type', 'latest_log_entry_type'], - ]; - manageIncidentTableColumns( - 'remove', - removeColumns.map((column) => column[1]), - ); - - const addColumns = [ - ['Num Alerts', 'num_alerts'], - ]; +describe('Manage Alerts', { failFast: { enabled: true } }, () => { + // We use beforeEach as each test will reload/clear the session + beforeEach(() => { + acceptDisclaimer(); + const addColumns = [['Num Alerts', 'num_alerts']]; manageIncidentTableColumns( 'add', addColumns.map((column) => column[1]), ); + waitForIncidentTable(); + waitForAlerts(); + }); + afterEach(() => { + checkNoIncidentsSelected(); + }); + + it('Hovering over Num Alerts count should popup alert table', () => { const incidentIdx = 0; cy.get(`[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${incidentIdx}"]`) .should('be.visible') .should('contain', '1'); - cy.get(`[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${incidentIdx}"]`).within(() => { + cy.get( + `[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${incidentIdx}"]`, + ).within(() => { cy.get('[aria-haspopup="dialog"]').realHover(); }); @@ -332,15 +308,14 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { cy.get('[data-popper-placement="bottom"]').should('contain', 'Created At'); cy.get('[data-popper-placement="bottom"]').should('contain', 'Status'); cy.get('[data-popper-placement="bottom"]').should('contain', 'Summary'); - - // Reset hover state - cy.get('body').realHover({ position: 'topLeft' }); }); it('Split/move alert from one incident to a new incident', () => { const incidentIdx = 0; - cy.get(`[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${incidentIdx}"]`).within(() => { + cy.get( + `[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${incidentIdx}"]`, + ).within(() => { cy.get('[aria-haspopup="dialog"]').click(); }); @@ -348,7 +323,9 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { cy.get('#alerts-modal-move-btn').click(); cy.get('#alerts-modal-move-select').type('Move all selected alerts to one new incident{enter}'); - cy.get('#alerts-modal-move-summary-input').clear().type('New incident created from split alert'); + cy.get('#alerts-modal-move-summary-input') + .clear() + .type('New incident created from split alert'); cy.get('#alerts-modal-complete-move-btn').click(); checkActionAlertsModalContent('Alerts moved'); @@ -368,7 +345,9 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { waitForIncidentTable(); const incidentIdx = 0; - cy.get(`[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${incidentIdx}"]`).within(() => { + cy.get( + `[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${incidentIdx}"]`, + ).within(() => { cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '2').click(); }); @@ -376,7 +355,9 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { selectAlert(1); cy.get('#alerts-modal-move-btn').click(); - cy.get('#alerts-modal-move-select').type('Move each selected alert to its own new incident{enter}'); + cy.get('#alerts-modal-move-select').type( + 'Move each selected alert to its own new incident{enter}', + ); cy.get('#alerts-modal-complete-move-btn').click(); checkActionAlertsModalContent('Alerts moved'); @@ -389,16 +370,20 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { selectIncident(targetIncidentIdx); cy.get(`@selectedIncidentId_${targetIncidentIdx}`).then((incidentId) => { - cy.get(`[data-incident-header="Num Alerts"][data-incident-cell-id="${incidentId}"]`).within(() => { - cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '1'); - }); + cy.get(`[data-incident-header="Num Alerts"][data-incident-cell-id="${incidentId}"]`).within( + () => { + cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '1'); + }, + ); cy.get(`[data-incident-header="Title"][data-incident-cell-id="${incidentId}"]`).within(() => { cy.get('a').invoke('text').as('targetIncidentTitle'); }); }); - cy.get(`[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${sourceIncidentIdx}"]`).within(() => { + cy.get( + `[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${sourceIncidentIdx}"]`, + ).within(() => { cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '1').click(); }); @@ -415,9 +400,11 @@ describe('Manage Open Incidents', { failFast: { enabled: false } }, () => { waitForIncidentTable(); cy.get(`@selectedIncidentId_${targetIncidentIdx}`).then((incidentId) => { - cy.get(`[data-incident-header="Num Alerts"][data-incident-cell-id="${incidentId}"]`).within(() => { - cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '2'); - }); + cy.get(`[data-incident-header="Num Alerts"][data-incident-cell-id="${incidentId}"]`).within( + () => { + cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '2'); + }, + ); }); // Tidy up by resolving the incident with two alerts diff --git a/cypress/e2e/Query/query.spec.js b/cypress/e2e/Query/query.spec.js index ca3ab36f..40da603b 100644 --- a/cypress/e2e/Query/query.spec.js +++ b/cypress/e2e/Query/query.spec.js @@ -19,28 +19,14 @@ import { registerLocale('en-GB', gb); moment.locale('en-GB'); -describe('Query Incidents', { failFast: { enabled: false } }, () => { - before(() => { +describe('Query Incidents', { failFast: { enabled: true } }, () => { + beforeEach(() => { acceptDisclaimer(); manageIncidentTableColumns('remove', ['latest_note']); manageIncidentTableColumns('add', ['urgency', 'teams', 'escalation_policy']); - // priorityNames.forEach((currentPriority) => { - // activateButton(`query-priority-${currentPriority}-button`); - // }); waitForIncidentTable(); }); - beforeEach(() => { - if (cy.state('test').currentRetry() > 1) { - acceptDisclaimer(); - manageIncidentTableColumns('remove', ['latest_note']); - manageIncidentTableColumns('add', ['urgency', 'teams', 'escalation_policy']); - } - // priorityNames.forEach((currentPriority) => { - // activateButton(`query-priority-${currentPriority}-button`); - // }); - }); - it('Query for incidents within T-1 since date', () => { // Limit dataset to high-urgency triggered, ackd and resolved incidents // activateButton('query-status-resolved-button'); @@ -68,9 +54,6 @@ describe('Query Incidents', { failFast: { enabled: false } }, () => { }); } }); - - // Reset query for next test - both high and low-urgency triggered, ackd and resolved incidents - cy.get('.query-urgency-low-button').check({ force: true }); }); it('Query for triggered incidents only', () => { @@ -100,11 +83,6 @@ describe('Query Incidents', { failFast: { enabled: false } }, () => { cy.get('.query-status-resolved-button').check({ force: true }); waitForIncidentTable(); checkIncidentCellIconAllRows('Status', 'fa-circle-check'); - - // Reset query for next test - cy.get('.query-status-triggered-button').check({ force: true }); - cy.get('.query-status-acknowledged-button').check({ force: true }); - cy.get('.query-status-resolved-button').uncheck({ force: true }); }); it('Query for high urgency incidents only', () => { @@ -119,9 +97,6 @@ describe('Query Incidents', { failFast: { enabled: false } }, () => { cy.get('.query-urgency-low-button').check({ force: true }); waitForIncidentTable(); checkIncidentCellContentAllRows('Urgency', ' Low'); - - // Reset query for next test - cy.get('.query-urgency-high-button').check({ force: true }); }); priorityNames.forEach((currentPriority) => { @@ -226,7 +201,7 @@ describe('Query Incidents', { failFast: { enabled: false } }, () => { cy.get('#query-user-select').click().type('{del}{del}{del}'); }); - it('Sort incident column "#" by ascending order', () => { + it('Sort incident columns', () => { cy.get('[data-column-name="#"]') .click() .then(($el) => { @@ -234,9 +209,7 @@ describe('Query Incidents', { failFast: { enabled: false } }, () => { expect(cls).to.contain('th-sorted'); cy.wrap($el).contains('# ▲'); }); - }); - it('Sort incident column "#" by descending order', () => { cy.get('[data-column-name="#"]') .click() .then(($el) => { @@ -244,9 +217,7 @@ describe('Query Incidents', { failFast: { enabled: false } }, () => { expect(cls).to.contain('th-sorted'); cy.wrap($el).contains('# ▼'); }); - }); - it('Clear sort on incident column "#"', () => { cy.get('[data-column-name="#"]') .click() .then(($el) => { diff --git a/cypress/e2e/Search/search.spec.js b/cypress/e2e/Search/search.spec.js index a52a5c4d..900df9ac 100644 --- a/cypress/e2e/Search/search.spec.js +++ b/cypress/e2e/Search/search.spec.js @@ -11,22 +11,14 @@ import { updateFuzzySearch, } from '../../support/util/common'; -describe('Search Incidents', { failFast: { enabled: false } }, () => { - before(() => { - acceptDisclaimer(); - waitForIncidentTable(); - }); - +describe('Search Incidents', { failFast: { enabled: true } }, () => { beforeEach(() => { - if (cy.state('test').currentRetry() > 1) { - acceptDisclaimer(); - } + acceptDisclaimer(); waitForIncidentTable(); }); it('Search for `Service A1` returns incidents only on Service A1', () => { cy.get('#global-search-input').clear().type('Service A1'); - cy.wait(1000); cy.get('[data-incident-header="Service"]').each(($el) => { cy.wrap($el).should('have.text', 'Service A1'); }); @@ -39,21 +31,18 @@ describe('Search Incidents', { failFast: { enabled: false } }, () => { cy.get(`@selectedIncidentId_${incidentIdx}`).then((incidentId) => { cy.get('#global-search-input').clear().type(incidentId); }); - cy.wait(1000); cy.get('.selected-incidents-badge').then(($el) => { 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(1000); cy.get('.empty-incidents-badge').should('be.visible'); cy.get('#global-search-input').clear(); }); @@ -67,13 +56,19 @@ describe('Search Incidents', { failFast: { enabled: false } }, () => { 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.wrap($el).find('*').should((subElements) => { - const elementWithFoobar = subElements.toArray().find((el) => el.textContent.includes('foobar')); - assert.isNotNull(elementWithFoobar, 'Expected to find a subelement containing "foobar"'); - }); + cy.wrap($el) + .find('*') + .should((subElements) => { + const elementWithFoobar = subElements + .toArray() + .find((el) => el.textContent.includes('foobar')); + assert.isNotNull( + elementWithFoobar, + 'Expected to find a subelement containing "foobar"', + ); + }); }); }); cy.get('#global-search-input').clear(); @@ -81,7 +76,6 @@ describe('Search Incidents', { failFast: { enabled: false } }, () => { 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(); }); @@ -93,12 +87,18 @@ describe('Search Incidents', { failFast: { enabled: false } }, () => { 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).find('*').should((subElements) => { - const elementWithFoobar = subElements.toArray().find((el) => el.textContent.includes('foobar')); - assert.isNotNull(elementWithFoobar, 'Expected to find a subelement containing "foobar"'); - }); + cy.wrap($el) + .find('*') + .should((subElements) => { + const elementWithFoobar = subElements + .toArray() + .find((el) => el.textContent.includes('foobar')); + assert.isNotNull( + elementWithFoobar, + 'Expected to find a subelement containing "foobar"', + ); + }); }); }); cy.get('#global-search-input').clear(); @@ -107,7 +107,6 @@ describe('Search Incidents', { failFast: { enabled: false } }, () => { 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'); }); diff --git a/cypress/e2e/Settings/settings.spec.js b/cypress/e2e/Settings/settings.spec.js index 75e06573..beca04ef 100644 --- a/cypress/e2e/Settings/settings.spec.js +++ b/cypress/e2e/Settings/settings.spec.js @@ -17,25 +17,12 @@ import { checkActionAlertsModalContent, } from '../../support/util/common'; -describe('Manage Settings', { failFast: { enabled: false } }, () => { +describe('Manage Settings', { failFast: { enabled: true } }, () => { const localeCode = 'en-US'; moment.locale(localeCode); - before(() => { - acceptDisclaimer(); - // priorityNames.forEach((currentPriority) => { - // activateButton(`query-priority-${currentPriority}-button`); - // }); - waitForIncidentTable(); - }); - beforeEach(() => { - if (cy.state('test').currentRetry() > 1) { - acceptDisclaimer(); - } - // priorityNames.forEach((currentPriority) => { - // activateButton(`query-priority-${currentPriority}-button`); - // }); + acceptDisclaimer(); waitForIncidentTable(); }); @@ -50,22 +37,29 @@ 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, 'Settings', '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) => { - 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'); - cy.get('#query-date-input').should('contain', expectedDate); - }); - }); + // 1 Day is the default + ['Today', '1 Day', '3 Days', '1 Week', '2 Weeks', '1 Month', '3 Months', '180 Days'].forEach( + (tenor) => { + it(`Update default since date lookback to ${tenor}`, () => { + let [sinceDateNum, sinceDateTenor] = tenor.split(' '); + if (tenor === 'Today') { + sinceDateNum = '0'; + sinceDateTenor = 'Day'; + } + const expectedDate = moment().subtract(Number(sinceDateNum), sinceDateTenor).format('L'); + updateDefaultSinceDateLookback(tenor); + updateUserLocale('English (United States)', 'Settings', 'Updated user profile settings'); + cy.get('#query-date-input').should('contain', expectedDate); + }); + }, + ); it('Update max rate limit', () => { const maxRateLimit = 600; @@ -216,6 +210,22 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { }); it('Save presets', () => { + updateDarkMode(); + const columns = [ + ['Teams', 'teams'], + ['Num Alerts', 'num_alerts'], + ['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'); + }); cy.get('.settings-panel-dropdown').click(); cy.get('.dropdown-item').contains('Load/Save Presets').click(); cy.get('#save-presets-button').click(); @@ -223,13 +233,6 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { cy.get('#close-button').click(); }); - it('Clear local cache', () => { - cy.get('.settings-panel-dropdown').click(); - cy.get('.dropdown-item').contains('Clear Local Cache').click(); - cy.get('.modal-title').contains('Disclaimer & License').should('be.visible'); - acceptDisclaimer(); - }); - it('Load presets', () => { cy.get('.settings-panel-dropdown').click(); cy.get('.dropdown-item').contains('Load/Save Presets').click(); diff --git a/cypress/e2e/app.spec.js b/cypress/e2e/app.spec.js index d9108cc9..f0f5ca8f 100644 --- a/cypress/e2e/app.spec.js +++ b/cypress/e2e/app.spec.js @@ -1,12 +1,15 @@ import moment from 'moment/min/moment-with-locales'; import { - acceptDisclaimer, waitForIncidentTable, pd, + acceptDisclaimer, + waitForIncidentTable, + clearLocalCache, + pd, } from '../support/util/common'; import packageConfig from '../../package.json'; -describe('Integration User Token', { failFast: { enabled: false } }, () => { +describe('Integration User Token', { failFast: { enabled: true } }, () => { before(() => { expect(Cypress.env('PD_USER_TOKEN')).to.be.a('string'); cy.intercept('GET', 'https://api.pagerduty.com/users/me').as('getCurrentUser'); @@ -28,19 +31,12 @@ describe('Integration User Token', { failFast: { enabled: false } }, () => { }); }); -describe('PagerDuty Live', () => { - before(() => { +describe('PagerDuty Live', { failFast: { enabled: true } }, () => { + beforeEach(() => { acceptDisclaimer(); waitForIncidentTable(); }); - beforeEach(() => { - if (cy.state('test').currentRetry() > 1) { - acceptDisclaimer(); - waitForIncidentTable(); - } - }); - it('Renders the main application page', () => { cy.get('#navbar-ctr').contains('Live Incidents Console'); }); @@ -62,7 +58,7 @@ describe('PagerDuty Live', () => { cy.intercept('https://api.pagerduty.com/abilities*', { abilities: ['teams', 'read_only_users', 'service_support_hours', 'urgencies'], }).as('getAbilities'); - cy.visit('http://localhost:3000/pd-live-react'); + clearLocalCache(); acceptDisclaimer(); cy.wait('@getAbilities', { timeout: 30000 }); @@ -75,12 +71,7 @@ describe('PagerDuty Live', () => { }); it('Application indicates when polling is disabled through url parameter disable-polling', () => { - cy.visit('http://localhost:3000/pd-live-react/?disable-polling=true'); - - // cy.get('.modal-title', { timeout: 30000 }).contains('Disclaimer & License'); - // cy.get('#disclaimer-agree-checkbox').click({ force: true }); - // cy.get('#disclaimer-accept-button').click({ force: true }); - + cy.visit('/?disable-polling=true'); cy.get('.status-beacon-ctr').realHover(); cy.get('[data-popper-placement="bottom"]').should('be.visible'); cy.get('[data-popper-placement="bottom"]').contains('Live updates disabled'); @@ -101,9 +92,7 @@ describe('PagerDuty Live', () => { ].join(''), ).as('getUrl'); - cy.visit( - `http://localhost:3000/pd-live-react/?disable-polling=true&since=${since}&until=${until}`, - ); + cy.visit(`/?disable-polling=true&since=${since}&until=${until}`); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(5000); @@ -114,4 +103,13 @@ describe('PagerDuty Live', () => { cy.get('#since-date-input').should('be.disabled'); cy.get('#until-date-input').should('be.disabled'); }); + + it('Application correctly loads iframe for extra buttons when configured', () => { + cy.visit('/?button=TestExtra,https://example.com'); + cy.get('button').contains('TestExtra').should('be.visible'); + cy.get('button').contains('TestExtra').click(); + cy.get('[data-popper-placement="top"]').should('be.visible'); + cy.get('iframe[title="TestExtra"]'); + // would need to enable cross-domain iframe javascript access to test further + }); }); diff --git a/cypress/support/util/common.js b/cypress/support/util/common.js index bfc24090..f8ca5d7d 100644 --- a/cypress/support/util/common.js +++ b/cypress/support/util/common.js @@ -1,4 +1,3 @@ -/* eslint-disable cypress/no-unnecessary-waiting */ /* eslint-disable cypress/unsafe-to-chain-command */ /* eslint-disable import/prefer-default-export */ import { @@ -11,9 +10,6 @@ export const pd = api({ token: Cypress.env('PD_USER_TOKEN') }); Cypress Helpers */ export const acceptDisclaimer = () => { - cy.clearLocalStorage(); - cy.clearAllSessionStorage(); - cy.clearCookies(); cy.visit('/'); cy.get('.modal-title', { timeout: 30000 }).contains('Disclaimer & License'); cy.get('#disclaimer-agree-checkbox').click({ force: true }); @@ -22,12 +18,23 @@ export const acceptDisclaimer = () => { export const waitForIncidentTable = () => { // Ref: https://stackoverflow.com/a/60065672/6480733 + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(3000); // Required for query debounce cy.get('#incident-table-ctr', { timeout: 60000 }).should('be.visible'); + cy.get('.selected-incidents-ctr', { timeout: 60000 }).should('not.include.text', 'Querying'); // will move on to next command even if table is not scrollable cy.get('.incident-table-fixed-list').scrollTo('top', { ensureScrollable: false }); }; +export const waitForAlerts = () => { + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(3000); // Required for query debounce + cy.get('.selected-incidents-ctr', { timeout: 60000 }).should( + 'not.include.text', + 'Fetching Alerts', + ); +}; + 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}`); @@ -45,7 +52,7 @@ export const selectAllIncidents = () => { }; export const checkNoIncidentsSelected = () => { - cy.get('.selected-incidents-badge').then(($el) => { + cy.get('.selected-incidents-badge').should(($el) => { const text = $el.text(); const incidentNumbers = text.split(' ')[0].split('/'); expect(incidentNumbers[0]).to.equal('0'); @@ -53,12 +60,10 @@ export const checkNoIncidentsSelected = () => { }; export const checkActionAlertsModalContent = (content) => { - cy.wait(2000); cy.get('.chakra-alert__title').contains(content, { timeout: 10000 }); }; export const checkPopoverContent = (incidentId, incidentHeader, content) => { - cy.wait(2000); cy.get( `[data-incident-header="${incidentHeader}"][data-incident-cell-id="${incidentId}"]`, ).within(() => { @@ -68,14 +73,12 @@ export const checkPopoverContent = (incidentId, incidentHeader, content) => { }; export const checkIncidentCellContent = (incidentId, incidentHeader, content) => { - cy.wait(2000); cy.get(`[data-incident-header="${incidentHeader}"][data-incident-cell-id="${incidentId}"]`) .should('be.visible') .contains(content); }; export const checkIncidentCellContentAllRows = (incidentHeader, content) => { - cy.wait(2000); cy.get('.incident-table-fixed-list').scrollTo('top', { ensureScrollable: true }); cy.get('.incident-table-fixed-list > div').then(($tbody) => { const visibleIncidentCount = $tbody.find('[role="row"]').length; @@ -101,7 +104,6 @@ export const checkIncidentCellIcon = (incidentIdx, incidentHeader, icon) => { }; export const checkIncidentCellIconAllRows = (incidentHeader, icon) => { - cy.wait(2000); cy.get('.incident-table-fixed-list > div').then(($tbody) => { const visibleIncidentCount = $tbody.find('[role="row"]').length; for (let incidentIdx = 0; incidentIdx < visibleIncidentCount; incidentIdx++) { @@ -111,7 +113,6 @@ export const checkIncidentCellIconAllRows = (incidentHeader, icon) => { }; export const checkIncidentCellContentHasLink = (incidentId, incidentHeader, text, link) => { - cy.wait(2000); cy.get(`[data-incident-header="${incidentHeader}"][data-incident-cell-id="${incidentId}"]`) .should('be.visible') .contains('a', text) @@ -144,8 +145,10 @@ export const escalate = (escalationLevel) => { export const reassign = (assignment) => { cy.get('#incident-action-reassign-button').click(); cy.get('#reassign-select').click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(200); cy.contains('.react-select__option', assignment).click({ force: true }); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(200); cy.get('#reassign-button').click({ force: true }); }; @@ -198,6 +201,7 @@ export const addNote = (note) => { }; const toggleRunAction = () => { + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(2000); // Unsure why we can't find DOM of action without wait cy.get('.incident-action-run-action-button').click(); }; @@ -285,8 +289,7 @@ export const updateDefaultSinceDateLookback = (tenor = '1 Day') => { cy.get('#save-settings-button').click(); checkActionAlertsModalContent('Updated user profile settings'); - cy.reload(); - acceptDisclaimer(); + cy.visit('/'); }; export const updateAutoRefreshInterval = (autoRefreshInterval = 5) => { @@ -345,6 +348,11 @@ export const updateDarkMode = () => { cy.get('[aria-label="Toggle Dark Mode"]').click(); }; +export const clearLocalCache = () => { + cy.get('.settings-panel-dropdown').click(); + cy.get('.dropdown-item').contains('Clear Local Cache').click(); +}; + export const priorityNames = ['--', 'P5', 'P4', 'P3', 'P2', 'P1']; /* diff --git a/jest.config.js b/jest.config.js index a844009b..f824c75a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,5 +14,7 @@ module.exports = { '^.+\\.(js|jsx|ts|tsx|mjs)$': 'babel-jest', '^.+\\.svg$': 'jest-transformer-svg', }, - transformIgnorePatterns: ['/node_modules/(?!(somePkg)|react-dnd|dnd-core|@react-dnd|jsonpath-plus)'], + transformIgnorePatterns: [ + '/node_modules/(?!(somePkg)|react-dnd|dnd-core|@react-dnd|jsonpath-plus)', + ], }; diff --git a/package.json b/package.json index a3f74fdb..a7aa4000 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pd-live-react", "homepage": "https://pagerduty.github.io/pd-live-react", - "version": "0.11.0-beta.0", + "version": "0.11.1-beta.0", "private": true, "dependencies": { "@chakra-ui/icons": "^2.1.1", @@ -16,10 +16,10 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@pagerduty/pdjs": "^2.2.3", "@types/jest": "^29.5.4", - "@types/node": "^20.5.7", + "@types/node": "^20.10.8", "@types/react": "^18.2.21", - "@types/react-dom": "^18.2.7", - "axios": "^1.6.0", + "@types/react-dom": "^18.2.17", + "axios": "^1.6.2", "bootstrap": "^4.6.2", "bottleneck": "^2.19.5", "chakra-react-select": "^4.7.0", @@ -31,21 +31,21 @@ "i18next-browser-languagedetector": "^7.1.0", "immer": "^10.0.2", "jsonpath-plus": "^7.2.0", - "linkify-react": "^4.1.1", - "linkifyjs": "^4.1.1", + "linkify-react": "^4.1.3", + "linkifyjs": "^4.1.3", "lodash": "^4.17.21", "moment": "^2.29.4", "pretty-print-error": "^1.1.1", "react": "^18", "react-bootstrap": "^2.9.1", "react-contextmenu": "^2.14.0", - "react-datepicker": "^4.16.0", + "react-datepicker": "^4.21.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18", "react-i18next": "^13.2.0", "react-icons": "^4.9.0", - "react-intersection-observer": "^9.5.2", + "react-intersection-observer": "^9.5.3", "react-minimal-pie-chart": "^8.4.0", "react-redux": "^8.1.2", "react-select": "^5.7.7", @@ -64,10 +64,10 @@ "build": "npx genversion src/config/version.js --semi --es6 && vite build", "genversion": "npx genversion src/config/version.js --semi --es6", "jest": "npx jest", - "cypress:open": "npx cypress open", - "cypress:run:local": "npx cypress run", - "cypress:run:record": "npx cypress run --record --key ${CYPRESS_RECORD_KEY}", - "cypress:run:ci": "npx cypress run --group 'e2e' --browser chrome --headless --parallel --record --key ${CYPRESS_RECORD_KEY} --ci-build-id $(date +'%s')", + "cypress:open": "npx cypress open --browser chrome --e2e", + "cypress:run:local": "npx cypress run --browser chrome --e2e", + "cypress:run:record": "npx cypress run --browser chrome --e2e --record --key ${CYPRESS_RECORD_KEY}", + "cypress:run:ci": "npx cypress run --browser chrome --e2e --headless --parallel --record --group 'e2e' --key ${CYPRESS_RECORD_KEY} --ci-build-id $(date +'%s')", "preview": "vite preview", "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx --exit-on-fatal-error", "format": "npx prettier-eslint --write '*.js' 'src/**/*.js' 'src/**/*.jsx' 'cypress/**/*.js'", @@ -95,32 +95,32 @@ ] }, "devDependencies": { - "@4tw/cypress-drag-drop": "^2.2.4", + "@4tw/cypress-drag-drop": "^2.2.5", "@babel/core": "^7.22.17", "@babel/eslint-parser": "^7.22.10", "@babel/preset-env": "^7.22.10", "@babel/preset-react": "^7.22.5", "@cypress/react": "^8.0.0", "@faker-js/faker": "^8.0.2", - "@testing-library/dom": "^9.3.0", + "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.1.4", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^14.1.2", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.4.3", "@vitejs/plugin-react": "^4.0.4", "@welldone-software/why-did-you-render": "^7.0.1", "babel-jest": "^29.6.3", - "cypress": "^12.17.1", - "cypress-fail-fast": "^7.0.0", - "cypress-real-events": "^1.10.3", + "cypress": "^13.5.1", + "cypress-fail-fast": "^7.0.3", + "cypress-real-events": "^1.11.0", "dotenv": "^16.3.1", "eslint": "^8.43.0", "eslint-config-airbnb": "^18.2.1", - "eslint-config-prettier": "^8.8.0", + "eslint-config-prettier": "^9.0.0", "eslint-config-react-app": "^7.0.1", "eslint-import-resolver-alias": "^1.1.2", - "eslint-plugin-cypress": "^2.14.0", - "eslint-plugin-import": "^2.28.1", + "eslint-plugin-cypress": "^2.15.1", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-jsx": "^0.1.0", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-prettier": "^4.2.1", @@ -135,21 +135,21 @@ "jest": "^29.6.3", "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^29.7.0", - "jest-environment-node": "^29.6.3", + "jest-environment-node": "^29.7.0", "jest-location-mock": "^1.0.10", "jest-transformer-svg": "^2.0.1", - "prettier": "^2.8.0", - "prettier-eslint": "^15.0.1", - "prettier-eslint-cli": "^7.1.0", + "prettier": "^3.1.0", + "prettier-eslint": "^16.1.2", + "prettier-eslint-cli": "^8.0.1", "redux-mock-store": "^1.5.4", "redux-saga-test-plan": "^4.0.6", "sass": "^1.66.1", "string.prototype.replaceall": "^1.0.6", - "vite": "^4.4.9", + "vite": "^4.4.12", "vite-plugin-environment": "^1.1.3", "vite-plugin-eslint": "^1.8.1", "vite-plugin-svgr": "^3.2.0", - "wait-on": "^7.0.1", - "yarn-audit-fix": "^10.0.0" + "wait-on": "^7.2.0", + "yarn-audit-fix": "^10.0.5" } } diff --git a/src/App.jsx b/src/App.jsx index e63b45a5..92abc5d4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -206,11 +206,14 @@ const App = () => { // Setup log entry clearing useEffect(() => { - const clearingInterval = setInterval(() => { - if (userAuthorized) { - cleanRecentLogEntriesAsync(); - } - }, 60 * 60 * 1000); + const clearingInterval = setInterval( + () => { + if (userAuthorized) { + cleanRecentLogEntriesAsync(); + } + }, + 60 * 60 * 1000, + ); return () => clearInterval(clearingInterval); }, [userAuthorized]); diff --git a/src/components/Auth/AuthComponent.jsx b/src/components/Auth/AuthComponent.jsx index b2f730de..038c78be 100644 --- a/src/components/Auth/AuthComponent.jsx +++ b/src/components/Auth/AuthComponent.jsx @@ -30,6 +30,7 @@ const AuthComponent = (props) => { const code = urlParams.get('code'); const subdomain = urlParams.get('subdomain'); const region = urlParams.get('service_region') || 'us'; + const buttons = urlParams.getAll('button'); let codeVerifier = sessionStorage.getItem('code_verifier'); let { @@ -50,12 +51,23 @@ const AuthComponent = (props) => { (token) => { sessionStorage.removeItem('code_verifier'); sessionStorage.setItem('pd_access_token', token); - window.location.assign(redirectURL); + // if there were button params on the first load, load the button params and put them back on the URL + const savedButtonsStr = sessionStorage.getItem('pd_buttons'); + const savedButtons = savedButtonsStr ? JSON.parse(savedButtonsStr) : []; + const buttonParams = savedButtons ? `?button=${savedButtons.join('&button=')}` : ''; + window.location.assign(redirectURL + buttonParams); }, ); } else if (!accessToken) { codeVerifier = createCodeVerifier(); sessionStorage.setItem('code_verifier', codeVerifier); + // if the user wants to use a button, save the button params in session storage + // (because we can't pass them through the OAuth flow) + if (buttons.length > 0) { + sessionStorage.setItem('pd_buttons', JSON.stringify(buttons)); + } else { + sessionStorage.removeItem('pd_buttons'); + } getAuthURL(clientId, clientSecret, redirectURL, codeVerifier).then((url) => { const subdomainParams = subdomain ? `&subdomain=${subdomain}&service_region=${region}` : ''; setAuthURL(`${url}${subdomainParams}`); diff --git a/src/components/IncidentActions/IncidentActionsComponent.jsx b/src/components/IncidentActions/IncidentActionsComponent.jsx index cc89063a..69e21c2b 100644 --- a/src/components/IncidentActions/IncidentActionsComponent.jsx +++ b/src/components/IncidentActions/IncidentActionsComponent.jsx @@ -4,6 +4,10 @@ import { Box, Flex, } from '@chakra-ui/react'; +import { + EXTRA_BUTTONS, +} from 'src/config/constants'; + import SelectedIncidentsComponent from './subcomponents/SelectedIncidentsComponent'; import AcknowledgeButton from './subcomponents/AcknowledgeButton'; @@ -16,6 +20,7 @@ import EscalateMenu from './subcomponents/EscalateMenu'; import SnoozeMenu from './subcomponents/SnoozeMenu'; import PriorityMenu from './subcomponents/PriorityMenu'; import RunActionMenu from './subcomponents/RunActionMenu'; +import ExtraButton from './subcomponents/ExtraButton'; import './IncidentActionsComponent.scss'; @@ -46,6 +51,14 @@ const IncidentActionsComponent = () => ( + {EXTRA_BUTTONS + && EXTRA_BUTTONS.map(({ + label, url, width, height, + }) => ( + <> + + + ))} ); diff --git a/src/components/IncidentActions/subcomponents/ExtraButton.jsx b/src/components/IncidentActions/subcomponents/ExtraButton.jsx new file mode 100644 index 00000000..1c73a5bf --- /dev/null +++ b/src/components/IncidentActions/subcomponents/ExtraButton.jsx @@ -0,0 +1,74 @@ +import React, { + useState, +} from 'react'; +import { + Box, + Button, + Popover, + PopoverTrigger, + PopoverContent, + PopoverArrow, + PopoverCloseButton, + PopoverBody, + PopoverHeader, +} from '@chakra-ui/react'; + +const isInt = (value) => { + const x = parseInt(value, 10); + return !Number.isNaN(x) && x.toString() === value; +}; + +const ExtraButton = ({ + label, url, width, height, +}) => { + // State to control the open state of the popover + const [isOpen, setIsOpen] = useState(false); + + // Function to open the popover + const openPopover = () => setIsOpen(true); + + // Function to close the popover + const closePopover = () => setIsOpen(false); + + return ( + + + + + + + {label} + + + + + +