diff --git a/.github/workflows/cd-workflow.yml b/.github/workflows/cd-workflow.yml index 439779bf..2eba135a 100644 --- a/.github/workflows/cd-workflow.yml +++ b/.github/workflows/cd-workflow.yml @@ -1,10 +1,26 @@ name: PagerDuty Live Continuous Deployment Pipeline (Main) on: + # Runs on pushes targeting the default branch push: branches: - main + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + jobs: build-deploy: runs-on: ubuntu-latest @@ -48,8 +64,6 @@ jobs: with: target_branch: gh-pages build_dir: build - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} sync-branch: runs-on: ubuntu-latest @@ -61,4 +75,4 @@ jobs: type: now from_branch: main target_branch: develop - github_token: ${{ secrets.GH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/cypress/e2e/Settings/settings.spec.js b/cypress/e2e/Settings/settings.spec.js index dbcca15f..cbd8cd96 100644 --- a/cypress/e2e/Settings/settings.spec.js +++ b/cypress/e2e/Settings/settings.spec.js @@ -14,6 +14,7 @@ import { updateMaxRateLimit, updateAutoAcceptIncidentQuery, updateAutoRefreshInterval, + updateDarkMode, manageIncidentTableColumns, manageCustomAlertColumnDefinitions, activateButton, @@ -193,4 +194,16 @@ describe('Manage Settings', { failFast: { enabled: false } }, () => { cy.get('.btn').contains('Clear Local Cache').click(); cy.get('.modal-title').contains('Disclaimer & License').should('be.visible'); }); + + it('Update dark mode', () => { + [true, false].forEach((darkMode) => { + updateDarkMode(darkMode); + cy.window() + .its('store') + .invoke('getState') + .then((state) => expect( + state.settings.darkMode, + ).to.equal(darkMode)); + }); + }); }); diff --git a/cypress/support/util/common.js b/cypress/support/util/common.js index b683b5b7..c8bff941 100644 --- a/cypress/support/util/common.js +++ b/cypress/support/util/common.js @@ -305,6 +305,22 @@ export const updateAutoAcceptIncidentQuery = (autoAcceptIncidentsQuery = false) cy.get('.close').click(); }; +export const updateDarkMode = (darkMode = false) => { + cy.get('.settings-panel-dropdown').click(); + cy.get('.dropdown-item').contains('Settings').click(); + cy.get('.nav-item').contains('User Profile').click(); + + if (darkMode) { + cy.get('#user-profile-dark-mode-checkbox').check({ force: true }); + } else { + cy.get('#user-profile-dark-mode-checkbox').uncheck({ force: true }); + } + + cy.get('.btn').contains('Update User Profile').click(); + checkActionAlertsModalContent('Updated user profile settings'); + cy.get('.close').click(); +}; + export const priorityNames = ['--', 'P5', 'P4', 'P3', 'P2', 'P1']; /* diff --git a/package.json b/package.json index 9763c12f..66efe5f5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pd-live-react", "homepage": "https://giranm.github.io/pd-live-react", - "version": "0.6.0-beta.0", + "version": "0.7.0-beta.0", "private": true, "dependencies": { "@braintree/sanitize-url": "^6.0.2", @@ -18,7 +18,7 @@ "@testing-library/user-event": "^14.4.3", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", "autoprefixer": "10.4.13", - "axios": "^1.2.2", + "axios": "^1.2.4", "babel-eslint": "^10.1.0", "bootstrap": "^4.6.2", "bottleneck": "^2.19.5", @@ -36,6 +36,7 @@ "node-sass": "^7.0.3", "react": "^17.0.2", "react-bootstrap": "^1.6.6", + "react-contextmenu": "^2.14.0", "react-datepicker": "^4.10.0", "react-dom": "^17.0.2", "react-dual-listbox": "^4.0.0", @@ -49,7 +50,7 @@ "redux": "^4.2.1", "redux-persist": "^6.0.0", "redux-saga": "^1.2.1", - "styled-components": "^5.3.6", + "styled-components": "^5.3.7", "use-debounce": "^9.0.3", "validator": "^13.7.0", "web-vitals": "^3.1.0" diff --git a/src/App.js b/src/App.js index 9cbbee7c..4bafb02e 100644 --- a/src/App.js +++ b/src/App.js @@ -97,6 +97,14 @@ const App = ({ }) => { // Verify if session token is present const token = sessionStorage.getItem('pd_access_token'); + const { + autoRefreshInterval, darkMode, + } = state.settings; + + if (darkMode) { + document.body.classList.add('dark-mode'); + } + if (!token) { return (
@@ -109,9 +117,6 @@ const App = ({ const { userAuthorized, userAcceptedDisclaimer, currentUserLocale, } = state.users; - const { - autoRefreshInterval, - } = state.settings; const { fetchingIncidents, fetchingIncidentNotes, diff --git a/src/App.scss b/src/App.scss index a5c82ee1..87d4429d 100644 --- a/src/App.scss +++ b/src/App.scss @@ -5,12 +5,22 @@ body { background-color: $pd-brand-background !important; } +body.dark-mode { + background-color: $pd-brand-gray-dark !important; +} + .btn-outline-secondary { color: $pd-brand-black; background-color: $pd-brand-white; border-color: $pd-brand-secondary-light; } +body.dark-mode .btn-outline-secondary { + color: $pd-white; + background-color: $pd-brand-gray-dark; + border-color: $pd-brand-secondary-light; +} + .btn-outline-secondary:disabled { color: $pd-brand-secondary-light !important; } @@ -26,6 +36,10 @@ body { background-color: unset !important; } +body.dark-mode .btn-outline-secondary:not(:disabled):not(.disabled).active { + background-color: $pd-brand-gray-light !important; +} + .btn-outline-dark { color: $pd-brand-gray-dark; background-color: $pd-white; @@ -59,3 +73,54 @@ body { .modal-footer { display: unset !important; } + +body.dark-mode a { + color: $pd-brand-white; +} + +body.dark-mode .modal-content { + background-color: $pd-brand-black !important; + color: $pd-white !important; +} + +body.dark-mode .modal-title { + color: $pd-brand-white !important; +} + +body.dark-mode .modal-header .close { + color: $pd-brand-white !important; +} + +body.dark-mode .form-label, body.dark-mode .form-text { + color: $pd-brand-white !important; +} + +body.dark-mode .form-control { + background-color: $pd-brand-gray-dark !important; + color: $pd-white; +} + +body.dark-mode div.react-select__control, body.dark-mode div.react-select__menu { + background-color: $pd-brand-gray-dark !important; + color: $pd-brand-white !important; +} + +body.dark-mode div.react-select__multi-value { + background-color: $pd-brand-gray-light !important; + color: $pd-brand-white !important; +} + +body.dark-mode div.react-select__single-value { + color: $pd-brand-white !important; +} + +body.dark-mode div.react-select__option:hover { + background-color: $pd-brand-secondary-light !important; + color: $pd-brand-white !important; +} + +body.dark-mode div.react-select__option--is-focused { + background-color: $pd-brand-secondary-light !important; + color: $pd-brand-white !important; +} + diff --git a/src/assets/images/pd_logo_white.svg b/src/assets/images/pd_logo_white.svg new file mode 100644 index 00000000..1394a06d --- /dev/null +++ b/src/assets/images/pd_logo_white.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/components/AddResponderModal/AddResponderModalComponent.js b/src/components/AddResponderModal/AddResponderModalComponent.js index 21dc4fc1..9c571c76 100644 --- a/src/components/AddResponderModal/AddResponderModalComponent.js +++ b/src/components/AddResponderModal/AddResponderModalComponent.js @@ -86,6 +86,7 @@ const AddResponderModalComponent = ({ {t('Responders')} ({ escalationPolicies: state.escalationPolicies.escalationPolicies, extensions: state.extensions, responsePlays: state.responsePlays.responsePlays, + settings: state.settings, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/src/components/IncidentActions/IncidentActionsComponent.scss b/src/components/IncidentActions/IncidentActionsComponent.scss index 521dcc3f..2c2da7c9 100644 --- a/src/components/IncidentActions/IncidentActionsComponent.scss +++ b/src/components/IncidentActions/IncidentActionsComponent.scss @@ -11,6 +11,11 @@ background-color: $pd-white; } +body.dark-mode .incident-actions-ctr { + background-color: $pd-brand-gray-dark; + color: $pd-brand-white; +} + .action-button { margin-right: 10px; } diff --git a/src/components/IncidentActions/IncidentActionsComponent.test.js b/src/components/IncidentActions/IncidentActionsComponent.test.js index 29bc14e0..42508ef9 100644 --- a/src/components/IncidentActions/IncidentActionsComponent.test.js +++ b/src/components/IncidentActions/IncidentActionsComponent.test.js @@ -41,6 +41,9 @@ describe('IncidentActionsComponent', () => { serviceExtensionMap: {}, }, responsePlays: { responsePlays: [] }, + settings: { + darkMode: false, + }, }; const tempStoreMap = { ...baseStoreMap }; diff --git a/src/components/IncidentTable/IncidentTableComponent.js b/src/components/IncidentTable/IncidentTableComponent.js index 3932be79..b0910c37 100644 --- a/src/components/IncidentTable/IncidentTableComponent.js +++ b/src/components/IncidentTable/IncidentTableComponent.js @@ -35,6 +35,10 @@ import { getReactTableColumnSchemas, } from 'config/incident-table-columns'; +import { + ContextMenu, MenuItem, ContextMenuTrigger, +} from 'react-contextmenu'; + import CheckboxComponent from './subcomponents/CheckboxComponent'; import EmptyIncidentsComponent from './subcomponents/EmptyIncidentsComponent'; import QueryActiveComponent from './subcomponents/QueryActiveComponent'; @@ -71,6 +75,39 @@ const Delayed = ({ return isShown ? children : null; }; +const exportTableDataToCsv = (tableData) => { + // Create headers from table columns + const headers = tableData.columns.map((column) => column.Header); + + const rowsToMap = tableData.selectedFlatRows.length > 0 ? tableData.selectedFlatRows : tableData.rows; + const exportRows = rowsToMap.map((row) => { + tableData.prepareRow(row); + const cells = row.cells.slice(1); + const cleanCells = cells.map((cell) => { + let cellStr = `${cell.value?.props?.children || cell.value}`; + if (cellStr.match(/[,"\r\n]/)) { + cellStr = `"${cellStr.replace(/"/g, '""')}"`; + } + return cellStr; + }); + return cleanCells.join(','); + }); + + // Join headers and rows into CSV string + const csv = [headers.join(','), ...exportRows].join('\n'); + + // Download CSV file + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', 'table-data.csv'); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; + const IncidentTableComponent = ({ selectIncidentTableRows, updateIncidentTableState, @@ -138,19 +175,7 @@ const IncidentTableComponent = ({ const getRowId = useCallback((row) => row.id, []); // Create instance of react-table with options and plugins - const { - state: { - selectedRowIds, - }, - getTableProps, - getTableBodyProps, - headerGroups, - rows, - prepareRow, - selectedFlatRows, - toggleAllRowsSelected, - totalColumnsWidth, - } = useTable( + const tableHook = useTable( { columns: memoizedColumns, data: filteredIncidentsByQuery, // Potential issue with Memoization hook? @@ -209,6 +234,20 @@ const IncidentTableComponent = ({ }, ); + const { + state: { + selectedRowIds, + }, + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + selectedFlatRows, + toggleAllRowsSelected, + totalColumnsWidth, + } = tableHook; + // Custom component required for virtualized rows const RenderRow = useCallback( ({ @@ -283,28 +322,30 @@ const IncidentTableComponent = ({ {headerGroups.map((headerGroup) => ( - - {headerGroup.headers.map((column) => ( - - ))} - + + + {headerGroup.headers.map((column) => ( + + ))} + + ))} @@ -320,6 +361,19 @@ const IncidentTableComponent = ({
- {column.render('Header')} - {column.isSorted ? (column.isSortedDesc ? ' ▼' : ' ▲') : ''} - {column.canResize && ( -
{ - e.preventDefault(); - e.stopPropagation(); - }} - /> - )} -
+ {column.render('Header')} + {column.isSorted ? (column.isSortedDesc ? ' ▼' : ' ▲') : ''} + {column.canResize && ( +
{ + e.preventDefault(); + e.stopPropagation(); + }} + /> + )} +
+ + { + exportTableDataToCsv(tableHook); + }} + > + Export CSV + {tableHook.selectedFlatRows.length > 0 + ? ` (${tableHook.selectedFlatRows.length} rows)` + : ` (${tableHook.rows.length} rows)`} + +
); diff --git a/src/components/IncidentTable/IncidentTableComponent.scss b/src/components/IncidentTable/IncidentTableComponent.scss index 3081b401..c997ce5b 100644 --- a/src/components/IncidentTable/IncidentTableComponent.scss +++ b/src/components/IncidentTable/IncidentTableComponent.scss @@ -7,6 +7,10 @@ top: 5px; } +body.dark-mode .querying-incidents { + color: $pd-brand-gray-light; +} + /* Empty Incidents */ .empty-incidents { height: 500px; @@ -17,6 +21,10 @@ padding-top: 20px; } +body.dark-mode .empty-incidents-badge { + color: $pd-brand-gray-light; +} + /* Table Positioning */ .incident-table { margin-top: 10px; @@ -142,6 +150,11 @@ border-right: 1px $pd-brand-background-3 solid; } +body.dark-mode .th { + background-color: $pd-gray; + color: $pd-white; +} + .th-sorted { padding: 10px !important; background-color: white; @@ -150,6 +163,11 @@ border-right: 1px $pd-brand-background-3 solid; } +body.dark-mode .th-sorted { + background-color: $pd-brand-secondary-dark; + color: $pd-white; +} + .resizer { display: inline-block; width: 2px; @@ -175,16 +193,32 @@ background-color: $pd-brand-background-2 !important; } +body.dark-mode .tr { + background-color: $pd-gray !important; + color: $pd-white; +} + .tr-odd { font-size: 14px; background-color: $pd-brand-background-3 !important; } +body.dark-mode .tr-odd { + background-color: $pd-gray-dark !important; + color: $pd-white; +} + .tr:hover, .tr-odd:hover { background-color: $pd-gray-medium !important; } +body.dark-mode .tr:hover, +body.dark-mode .tr-odd:hover { + background-color: $pd-brand-secondary-light !important; + color: $pd-white; +} + .td { padding: 10px !important; padding-left: 10px !important; @@ -201,3 +235,9 @@ line-clamp: 2; -webkit-box-orient: vertical; } + +.react-contextmenu { + z-index: 1000; + background-color: $pd-white; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2); +} \ No newline at end of file diff --git a/src/components/MergeModal/MergeModalComponent.js b/src/components/MergeModal/MergeModalComponent.js index 2652053d..ad0dcbe4 100644 --- a/src/components/MergeModal/MergeModalComponent.js +++ b/src/components/MergeModal/MergeModalComponent.js @@ -93,6 +93,7 @@ const MergeModalComponent = ({ { const teamIdsInt = selectedTeams.map((team) => team.value); @@ -333,6 +341,7 @@ const QuerySettingsComponent = ({ { const escalationPolicyIdsInt = selectedEscalationPolicies.map( @@ -375,6 +385,7 @@ const QuerySettingsComponent = ({ { setAssignment(selectedAssignment); }} diff --git a/src/components/SettingsModal/SettingsModalComponent.js b/src/components/SettingsModal/SettingsModalComponent.js index 73ce86e2..5884c52b 100644 --- a/src/components/SettingsModal/SettingsModalComponent.js +++ b/src/components/SettingsModal/SettingsModalComponent.js @@ -39,6 +39,7 @@ import { setAutoAcceptIncidentsQuery as setAutoAcceptIncidentsQueryConnected, setAutoRefreshInterval as setAutoRefreshIntervalConnected, clearLocalCache as clearLocalCacheConnected, + setDarkMode as setDarkModeConnected, } from 'redux/settings/actions'; import { @@ -100,6 +101,7 @@ const SettingsModalComponent = ({ setMaxRateLimit, setAutoAcceptIncidentsQuery, setAutoRefreshInterval, + setDarkMode, clearLocalCache, updateActionAlertsModal, toggleDisplayActionAlertsModal, @@ -115,6 +117,7 @@ const SettingsModalComponent = ({ autoAcceptIncidentsQuery, autoRefreshInterval, alertCustomDetailFields, + darkMode, } = settings; const { incidentTableColumns, @@ -173,6 +176,8 @@ const SettingsModalComponent = ({ const [tempAutoAcceptQuery, setTempAutoAcceptQuery] = useState(autoAcceptIncidentsQuery); + const [tempDarkMode, setTempDarkMode] = useState(darkMode); + const [selectedColumns, setSelectedColumns] = useState( incidentTableColumns.map((column) => { // Recreate original value used from react-select in order to populate dual list @@ -253,6 +258,7 @@ const SettingsModalComponent = ({