diff --git a/cypress/e2e/Settings/settings.spec.js b/cypress/e2e/Settings/settings.spec.js index beca04ef..7b408cba 100644 --- a/cypress/e2e/Settings/settings.spec.js +++ b/cypress/e2e/Settings/settings.spec.js @@ -12,7 +12,7 @@ import { updateDarkMode, updateRelativeDates, manageIncidentTableColumns, - manageCustomAlertColumnDefinitions, + manageCustomColumnDefinitions, checkIncidentCellContentAllRows, checkActionAlertsModalContent, } from '../../support/util/common'; @@ -141,27 +141,85 @@ describe('Manage Settings', { failFast: { enabled: true } }, () => { }); it('Add valid custom alert column to incident table', () => { - const customAlertColumnDefinitions = ['Quote:details.quote']; - manageCustomAlertColumnDefinitions(customAlertColumnDefinitions); - customAlertColumnDefinitions.forEach((columnName) => { - const header = columnName.split(':')[0]; - cy.get(`[data-column-name="${header}"]`).scrollIntoView().should('be.visible'); - cy.get(`[data-incident-header="${header}"][data-incident-row-cell-idx="0"]`).then(($el) => { + const customColumnDefinitions = [ + { header: 'Quote', accessorPath: 'details.quote', expression: '' }, + ]; + manageCustomColumnDefinitions(customColumnDefinitions); + customColumnDefinitions.forEach((column) => { + cy.get(`[data-column-name="${column.header}"]`).scrollIntoView().should('be.visible'); + cy.get(`[data-incident-header="${column.header}"][data-incident-row-cell-idx="0"]`).then(($el) => { // eslint-disable-next-line no-unused-expressions expect($el.text()).to.exist; + // Quote exists in the alert body, so it should not be empty + expect($el.text()).to.not.equal('--'); + expect($el.text().length).to.be.greaterThan(20); }); }); }); it('Add valid custom alert column with JSON path containing spaces to incident table', () => { - const customAlertColumnDefinitions = ["Fav Flavour:details.['favorite ice cream flavor']"]; - manageCustomAlertColumnDefinitions(customAlertColumnDefinitions); - customAlertColumnDefinitions.forEach((columnName) => { - const header = columnName.split(':')[0]; - cy.get(`[data-column-name="${header}"]`).scrollIntoView().should('be.visible'); - cy.get(`[data-incident-header="${header}"][data-incident-row-cell-idx="0"]`).then(($el) => { + const customColumnDefinitions = [ + { header: 'Fav Flavour', accessorPath: "details.['favorite ice cream flavor']", expression: '' }, + ]; + manageCustomColumnDefinitions(customColumnDefinitions); + customColumnDefinitions.forEach((column) => { + cy.get(`[data-column-name="${column.header}"]`).scrollIntoView().should('be.visible'); + cy.get(`[data-incident-header="${column.header}"][data-incident-row-cell-idx="0"]`).then(($el) => { + // eslint-disable-next-line no-unused-expressions + expect($el.text()).to.exist; + // Fav Flavour doesn't exist in the alert body, so it should be empty + expect($el.text()).to.equal('--'); + }); + }); + }); + + it('Add valid custom computed column to incident table', () => { + const customColumnDefinitions = [ + { header: 'CI', accessorPath: 'first_trigger_log_entry.channel.details', expression: '(.*.example.com)' }, + ]; + manageCustomColumnDefinitions(customColumnDefinitions, 'computed'); + customColumnDefinitions.forEach((column) => { + cy.get(`[data-column-name="${column.header}"]`).scrollIntoView().should('be.visible'); + cy.get(`[data-incident-header="${column.header}"][data-incident-row-cell-idx="0"]`).then(($el) => { + // eslint-disable-next-line no-unused-expressions + expect($el.text()).to.exist; + // CI doesn't exist in the alert body, so it should be empty + expect($el.text()).to.equal('--'); + }); + }); + }); + + it('Add two valid custom computed column to incident table with different expressions', () => { + const customColumnDefinitions = [ + { header: 'CI', accessorPath: 'first_trigger_log_entry.channel.details', expression: '(.*.example.com)' }, + { header: 'Category', accessorPath: 'first_trigger_log_entry.channel.details', expression: 'Category(.*)' }, + ]; + manageCustomColumnDefinitions(customColumnDefinitions, 'computed'); + customColumnDefinitions.forEach((column) => { + cy.get(`[data-column-name="${column.header}"]`).scrollIntoView().should('be.visible'); + cy.get(`[data-incident-header="${column.header}"][data-incident-row-cell-idx="0"]`).then(($el) => { + // eslint-disable-next-line no-unused-expressions + expect($el.text()).to.exist; + // CI or Category don't exist in the alert body, so it should be empty + expect($el.text()).to.equal('--'); + }); + }); + }); + + it('Add valid quote custom computed column to incident table', () => { + const customColumnDefinitions = [ + { header: 'QuoteRegex', accessorPath: 'first_trigger_log_entry.channel.details', expression: '{"quote":"(.*)"}' }, + ]; + manageCustomColumnDefinitions(customColumnDefinitions, 'computed'); + customColumnDefinitions.forEach((column) => { + cy.get(`[data-column-name="${column.header}"]`).scrollIntoView().should('be.visible'); + cy.get(`[data-incident-header="${column.header}"][data-incident-row-cell-idx="0"]`).then(($el) => { // eslint-disable-next-line no-unused-expressions expect($el.text()).to.exist; + // Quote does exist in the alert body, so it should not be empty and also shouldn't contain the custom details JSON with quote key, just the quote value + expect($el.text()).to.not.equal('--'); + expect($el.text()).to.not.contain('"quote"'); + expect($el.text().length).to.be.greaterThan(20); }); }); }); diff --git a/cypress/support/util/common.js b/cypress/support/util/common.js index 61ab0e23..5309595a 100644 --- a/cypress/support/util/common.js +++ b/cypress/support/util/common.js @@ -255,7 +255,7 @@ export const manageIncidentTableColumns = (desiredState = 'add', columns = []) = checkActionAlertsModalContent('Incident table columns saved'); }; -export const manageCustomAlertColumnDefinitions = (customAlertColumnDefinitions) => { +export const manageCustomColumnDefinitions = (customColumnDefinitions, type = 'alert') => { cy.get('.settings-panel-dropdown').click(); cy.get('.dropdown-item').contains('Columns').click(); @@ -263,14 +263,23 @@ export const manageCustomAlertColumnDefinitions = (customAlertColumnDefinitions) cy.wrap($el).click(); }); - customAlertColumnDefinitions.forEach((customAlertColumnDefinition) => { - const [header, accessorPath] = customAlertColumnDefinition.split(':'); - cy.get('input[placeholder="Header"]').type(header); - cy.get('input[placeholder="JSON Path"]').type(accessorPath); + customColumnDefinitions.forEach((customColumnDefinition) => { + const { + header, accessorPath, expression, + } = customColumnDefinition; + cy.get('#column-type-select').select(type); + cy.get('input[placeholder="Header"]').clear().type(header); + cy.get('input[placeholder="JSON Path"]').clear().type(accessorPath); + if (type === 'computed') { + cy.get('input[placeholder="Regex"]').clear().type(expression, { parseSpecialCharSequences: false }); + } cy.get('button[aria-label="Add custom column"]').click(); // Need to escape special characters in accessorPath // https://docs.cypress.io/faq/questions/using-cypress-faq#How-do-I-use-special-characters-with-cyget - cy.get(`#column-${Cypress.$.escapeSelector(accessorPath)}-add-icon`).click(); + const columnId = Cypress.$.escapeSelector( + [header, accessorPath, expression.replace(/:/g, '\\:')].filter((value) => value !== '').join(':'), + ); + cy.get(`#column-${columnId}-add-icon`).click(); }); cy.get('#save-columns-button').click(); checkActionAlertsModalContent('Incident table columns saved'); diff --git a/src/components/ColumnsModal/ColumnsModalComponent.jsx b/src/components/ColumnsModal/ColumnsModalComponent.jsx index b5af8d5e..6a5a1515 100644 --- a/src/components/ColumnsModal/ColumnsModalComponent.jsx +++ b/src/components/ColumnsModal/ColumnsModalComponent.jsx @@ -34,6 +34,7 @@ import { Input, Text, useToast, + Select, } from '@chakra-ui/react'; import { AddIcon, @@ -41,7 +42,8 @@ import { import { defaultColumns, customAlertColumns, - columnsForSavedColumns, + customComputedColumns, + // columnsForSavedColumns, } from 'src/config/column-generator'; import { @@ -52,6 +54,7 @@ import { import { toggleColumnsModal as toggleColumnsModalConnected, setAlertCustomDetailColumns as setAlertCustomDetailColumnsConnected, + setComputedColumns as setComputedColumnsConnected, } from 'src/redux/settings/actions'; import ColumnsModalItem from './subcomponents/ColumnsModalItem'; @@ -59,7 +62,8 @@ import DraggableColumnsModalItem from './subcomponents/DraggableColumnsModalItem const TableColumnsModalComponent = () => { const displayColumnsModal = useSelector((state) => state.settings.displayColumnsModal); - const alertCustomDetailFields = useSelector((state) => state.settings.alertCustomDetailFields); + const alertCustomDetailFields = useSelector((state) => state.settings.alertCustomDetailFields) || []; + const computedFields = useSelector((state) => state.settings.computedFields) || []; const incidentTableColumns = useSelector((state) => state.incidentTable.incidentTableColumns); const { incidentTableState, @@ -70,6 +74,9 @@ const TableColumnsModalComponent = () => { const setAlertCustomDetailColumns = (newAlertCustomDetailFields) => { dispatch(setAlertCustomDetailColumnsConnected(newAlertCustomDetailFields)); }; + const setComputedColumns = (newComputedFields) => { + dispatch(setComputedColumnsConnected(newComputedFields)); + }; const saveIncidentTable = (updatedIncidentTableColumns) => { dispatch(saveIncidentTableConnected(updatedIncidentTableColumns)); }; @@ -83,65 +90,46 @@ const TableColumnsModalComponent = () => { const toast = useToast(); - const columnValue = (column) => { - if (!column) return ''; - let value; - if (column.columnType === 'alert') { - // Alert column based on aggregator - value = column.Header - + (column.accessorPath ? `:${column.accessorPath}` : '') - + (column.aggregator ? `:${column.aggregator}` : ''); - } else { - // Incident column - value = column.Header; - } - return value; - }; - const getAllAvailableColumns = () => { // eslint-disable-next-line max-len - const v = [...defaultColumns(), ...customAlertColumns(alertCustomDetailFields)].sort((a, b) => columnValue(a).localeCompare(columnValue(b))); + const v = [...defaultColumns(), ...customAlertColumns(alertCustomDetailFields), ...customComputedColumns(computedFields)].sort((a, b) => a.value.localeCompare(b.value)); return v; }; - const allAvailableColumns = useMemo(getAllAvailableColumns, [alertCustomDetailFields]); + const allAvailableColumns = useMemo(getAllAvailableColumns, [alertCustomDetailFields, computedFields]); - const getSelectedColumns = () => columnsForSavedColumns(incidentTableColumns).map((column) => { - // Recreate original value used from react-select in order to populate dual list - const value = columnValue(column); - return { - id: column.id, - Header: column.Header, - columnType: column.columnType, - accessor: column.accessor, - accessorPath: column.accessorPath, + const getSelectedColumns = () => incidentTableColumns.map((column) => ( + { + ...column, label: column.i18n ? column.i18n : column.Header, - value, - }; - }); + } + )); const [selectedColumns, setSelectedColumns] = useState(getSelectedColumns()); const getUnselectedColumns = () => { const unselected = allAvailableColumns.filter( - (c) => !selectedColumns.find((s) => s.value === columnValue(c)), + (c) => !selectedColumns.find((s) => s.value === c.value), ); return unselected; }; const unselectedColumns = useMemo(getUnselectedColumns, [allAvailableColumns, selectedColumns]); const unselectColumn = (column) => { - setSelectedColumns((prev) => prev.filter((c) => columnValue(c) !== columnValue(column))); + setSelectedColumns((prev) => prev.filter((c) => c.value !== column.value)); }; const selectColumn = (column) => { setSelectedColumns((prev) => [...prev, column]); }; - const addCustomAlertColumn = (value) => { - const [Header, accessorPath, aggregator] = value.split(':'); + const addCustomAlertColumn = (Header, accessorPath) => { + const value = `${Header}:${accessorPath}`; + if (!Header || !accessorPath) { + return; + } const newColumn = { + id: value, Header, accessorPath, - aggregator, value, label: value, columnType: 'alert', @@ -150,6 +138,25 @@ const TableColumnsModalComponent = () => { setAlertCustomDetailColumns(newAlertCustomDetailFields); }; + const addCustomComputedColumn = (Header, accessorPath, expression) => { + const value = `${Header}:${accessorPath}:${expression.replace(/:/g, '\\:')}`; + if (!Header || !accessorPath || !expression) { + return; + } + const newColumn = { + id: value, + Header, + accessorPath, + value, + expressionType: 'regex-single', + expression, + label: value, + columnType: 'computed', + }; + const newComputedFields = [...computedFields, newColumn]; + setComputedColumns(newComputedFields); + }; + const removeCustomAlertColumn = (column) => { unselectColumn(column); const newAlertCustomDetailFields = alertCustomDetailFields.filter( @@ -158,10 +165,22 @@ const TableColumnsModalComponent = () => { setAlertCustomDetailColumns(newAlertCustomDetailFields); }; + const removeCustomComputedColumn = (column) => { + unselectColumn(column); + const newComputedFields = computedFields.filter( + (c) => c.value !== column.value, + ); + setComputedColumns(newComputedFields); + }; + + const columnTypeInputRef = useRef(null); const headerInputRef = useRef(null); const accessorPathInputRef = useRef(null); + const regexInputRef = useRef(null); const addButtonRef = useRef(null); + const [columnType, setColumnType] = useState('alert'); + const [inputIsValid, setInputIsValid] = useState(false); const validateInput = () => { let valid = true; @@ -176,7 +195,7 @@ const TableColumnsModalComponent = () => { const findColumnInSelectedColumns = useCallback( (value) => { - const column = selectedColumns.find((c) => columnValue(c) === value); + const column = selectedColumns.find((c) => c.value === value); return { column, index: selectedColumns.indexOf(column), @@ -200,7 +219,7 @@ const TableColumnsModalComponent = () => { const [, drop] = useDrop(() => ({ accept: 'DraggableColumnsModalItem' })); return ( - + {t('Incident Table')} @@ -209,13 +228,13 @@ const TableColumnsModalComponent = () => { - {t('Selected')} + {t('Selected')} - + {selectedColumns.map((column) => ( unselectColumn(column)} itemType="selected" @@ -226,20 +245,20 @@ const TableColumnsModalComponent = () => { - + {t('Drag and drop to reorder')} - + - {t('Available')} + {t('Available')} - - + + {unselectedColumns.map((column) => ( selectColumn(column)} itemType="available" @@ -248,22 +267,60 @@ const TableColumnsModalComponent = () => { - + - {t('Custom')} + {t('Custom')} - + - {alertCustomDetailFields.map((column) => ( - removeCustomAlertColumn(column)} - itemType="custom" - /> - ))} + + + {t('alert')} + + + {alertCustomDetailFields.map((column) => ( + removeCustomAlertColumn(column)} + itemType="custom" + /> + ))} + + + + + {t('computed')} + + + {computedFields.map((column) => ( + removeCustomComputedColumn(column)} + itemType="custom" + columnType="computed" + /> + ))} + + - + + { ref={accessorPathInputRef} onChange={validateInput} m={1} - w="60%" + w="50%" placeholder={t('JSON Path')} size="sm" /> +