diff --git a/lib/public/components/environments/environmentStatusHistoryComponent.js b/lib/public/components/environments/environmentStatusHistoryComponent.js index 090e667bf1..022d595803 100644 --- a/lib/public/components/environments/environmentStatusHistoryComponent.js +++ b/lib/public/components/environments/environmentStatusHistoryComponent.js @@ -25,8 +25,8 @@ export const environmentStatusHistoryLegendComponent = () => h('h5', 'Status History Legend'), Object.keys(StatusAcronym).map((status) => h('.flex-row.justify-between', [ - h('', status), - h('', StatusAcronym[status]), + coloredEnvironmentStatusComponent(status), + h('', coloredEnvironmentStatusComponent(status, StatusAcronym[status])), ])), ]); diff --git a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js index 0641c16b64..49da11ae91 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -116,5 +116,16 @@ export const environmentsActiveColumns = { coloredEnvironmentStatusComponent(status, StatusAcronym[status]), ]).slice(1), balloon: true, + + /** + * Status history filter component + * + * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model + * @return {Component} the filter component + */ + filter: (environmentOverviewModel) => rawTextFilter( + environmentOverviewModel.filteringModel.get('statusHistory'), + { classes: ['w-100'], placeholder: 'e.g. D-R-X' }, + ), }, }; diff --git a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js index 94a5563a63..1d72fc2528 100644 --- a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js +++ b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js @@ -32,6 +32,7 @@ export class EnvironmentOverviewModel extends OverviewPageModel { super(); this._filteringModel = new FilteringModel({ + statusHistory: new RawTextFilterModel(), currentStatus: new SelectionFilterModel({ availableOptions: Object.keys(StatusAcronym).map((status) => ({ value: status, diff --git a/lib/usecases/environment/GetAllEnvironmentsUseCase.js b/lib/usecases/environment/GetAllEnvironmentsUseCase.js index 5751d8ee88..fd01f05813 100644 --- a/lib/usecases/environment/GetAllEnvironmentsUseCase.js +++ b/lib/usecases/environment/GetAllEnvironmentsUseCase.js @@ -126,8 +126,10 @@ class GetAllEnvironmentsUseCase { } if (statusHistory) { - // Split the string into separate characters - const historyItems = statusHistory.split(''); + // Check if status history ends with 'X' and remove it if present to handle the special case later + const containsX = statusHistory.endsWith('X'); + const cleanedStatusHistory = containsX ? statusHistory.slice(0, -1) : statusHistory; + const historyItems = cleanedStatusHistory.split(''); // Swap the acronyms with the status (=acronym -> status) const acronymToStatus = {}; @@ -145,17 +147,38 @@ class GetAllEnvironmentsUseCase { } } - // Filter the environments by status history using the subquery - filterQueryBuilder.literalWhere( - `${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} = :statusFilters`, - // Create a string of the status filters separated by a comma - { statusFilters: statusFilters.join(',') }, - ); + if (containsX) { + const statusFiltersWithDestroyed = [...statusFilters, 'DESTROYED'].join(','); + const statusFiltersWithDone = [...statusFilters, 'DONE'].join(','); + + /* + * Use OR condition to match subsequences ending with either DESTROYED or DONE + * Filter the environments by using LIKE for subsequence matching + */ + filterQueryBuilder.literalWhere( + `(${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} LIKE :statusFiltersWithDestroyed OR ` + + `${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} LIKE :statusFiltersWithDone)`, + { + statusFiltersWithDestroyed: `%${statusFiltersWithDestroyed}`, + statusFiltersWithDone: `%${statusFiltersWithDone}`, + }, + ); - filterQueryBuilder.includeAttribute({ - query: ENVIRONMENT_STATUS_HISTORY_SUBQUERY, - alias: 'statusHistory', - }); + filterQueryBuilder.includeAttribute({ + query: ENVIRONMENT_STATUS_HISTORY_SUBQUERY, + alias: 'statusHistory', + }); + } else { + filterQueryBuilder.literalWhere( + `${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} LIKE :statusFilters`, + { statusFilters: `%${statusFilters.join(',')}%` }, + ); + + filterQueryBuilder.includeAttribute({ + query: ENVIRONMENT_STATUS_HISTORY_SUBQUERY, + alias: 'statusHistory', + }); + } } if (runNumbersExpression) { diff --git a/test/api/environments.test.js b/test/api/environments.test.js index 97009a40c6..770df255a9 100644 --- a/test/api/environments.test.js +++ b/test/api/environments.test.js @@ -171,6 +171,15 @@ module.exports = () => { expect(withChar[1].id).to.be.equal(withoutChar[1].id); }); + it('should successfully filter environments on status history with a partial sequence', async () => { + const response = await request(server).get('/api/environments?filter[statusHistory]=D-E'); + + expect(response.status).to.equal(200); + const environments = response.body.data; + expect(environments.length).to.be.equal(1); + expect(environments[0].id).to.be.equal('KGIS12DS'); + }); + it('should successfully filter environments status history with limit', async () => { const response = await request(server).get('/api/environments?filter[statusHistory]=SE&page[limit]=1'); diff --git a/test/public/envs/overview.test.js b/test/public/envs/overview.test.js index b6e9183798..4fa6ca82a0 100644 --- a/test/public/envs/overview.test.js +++ b/test/public/envs/overview.test.js @@ -294,6 +294,33 @@ module.exports = () => { await page.waitForSelector(filterPanelSelector, { visible: true }); }); + it('should successfully filter environments by their status history', async () => { + /** + * This is the sequence to test filtering the environments on their status history. + * + * @param {string} selector the filter input selector + * @param {string} inputValue the value to type in the filter input + * @param {string[]} expectedIds the list of expected environment IDs after filtering + * @return {void} + */ + const filterOnStatusHistory = async (selector, inputValue, expectedIds) => { + await fillInput(page, selector, inputValue, ['change']); + await waitForTableLength(page, expectedIds.length); + expect(await page.$$eval('tbody tr', (rows) => rows.map((row) => row.id))).to.eql(expectedIds.map(id => `row${id}`)); + }; + + await expectAttributeValue(page, '.historyItems-filter input', 'placeholder', 'e.g. D-R-X'); + + await filterOnStatusHistory('.historyItems-filter input', 'C-R-D-X', ['TDI59So3d']); + await resetFilters(page); + + await filterOnStatusHistory('.historyItems-filter input', 'S-E', ['EIDO13i3D', '8E4aZTjY']); + await resetFilters(page); + + await filterOnStatusHistory('.historyItems-filter input', 'D-E', ['KGIS12DS']); + await resetFilters(page); + }); + it('should successfully filter environments by their current status', async () => { /** * Checks that all the rows of the given table have a valid current status