From 61bd4c878719e1f3fe2690c5b580caef5ee44c48 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:00:31 +0100 Subject: [PATCH 1/3] [O2B-1492] Add status history filter to environments overview Introduces a status history filter to the environments overview page. Includes special handling for status histories ending with 'X'. Adds tests to verify correct filtering behaviour. Additionally colourises the status history legend to make easier to compare. --- .../environmentStatusHistoryComponent.js | 4 +- .../environmentsActiveColumns.js | 12 +++++ .../Overview/EnvironmentOverviewModel.js | 2 + .../environment/GetAllEnvironmentsUseCase.js | 47 ++++++++++++++----- test/public/envs/overview.test.js | 30 ++++++++++++ 5 files changed, 81 insertions(+), 14 deletions(-) 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 c570931869..8a0c8bef9f 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -24,6 +24,7 @@ import { environmentStatusHistoryLegendComponent } from '../../../components/env import { infoTooltip } from '../../../components/common/popover/infoTooltip.js'; import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; import { StatusAcronym } from '../../../domain/enums/statusAcronym.mjs'; +import { rawTextFilter } from '../../../components/Filters/common/filters/rawTextFilter.js'; /** * List of active columns for a generic Environments component @@ -95,5 +96,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 1d701b4a27..53f418e66a 100644 --- a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js +++ b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js @@ -14,6 +14,7 @@ import { buildUrl } from '/js/src/index.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; +import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { debounce } from '../../../utilities/debounce.js'; /** @@ -28,6 +29,7 @@ export class EnvironmentOverviewModel extends OverviewPageModel { super(); this._filteringModel = new FilteringModel({ + statusHistory: new RawTextFilterModel(), }); this._filteringModel.observe(() => this._applyFilters(true)); 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/public/envs/overview.test.js b/test/public/envs/overview.test.js index ae673ef4e0..cefcc8ff79 100644 --- a/test/public/envs/overview.test.js +++ b/test/public/envs/overview.test.js @@ -26,6 +26,9 @@ const { getPopoverSelector, goToPage, openFilteringPanel, + fillInput, + expectAttributeValue, + resetFilters, } = require('../defaults.js'); const dateAndTime = require('date-and-time'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -290,4 +293,31 @@ module.exports = () => { await openFilteringPanel(page); 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); + }); }; From f4f4264e01ee7e6900d43567881c94e48fe79158 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:29:47 +0100 Subject: [PATCH 2/3] [O2B-1492] Add test for filtering environments by partial status history Ensures the environments API supports partial sequence matching with regards to the status history filter. --- test/api/environments.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) 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'); From fddd637e68703e84f7b49296190d600ff1fa49a2 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:23:27 +0100 Subject: [PATCH 3/3] [O2B-1492] Fix formatting in overview.test.js --- test/public/envs/overview.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/public/envs/overview.test.js b/test/public/envs/overview.test.js index ba697b8f76..4fa6ca82a0 100644 --- a/test/public/envs/overview.test.js +++ b/test/public/envs/overview.test.js @@ -364,7 +364,7 @@ module.exports = () => { 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, '.id-filter input', 'placeholder', 'e.g. CmCvjNbg, TDI59So3d...'); await filterOnID('.id-filter input', 'CmCvjNbg', ['CmCvjNbg']);