From d385b08d848d2f67109a97bd1bd35bff21985694 Mon Sep 17 00:00:00 2001 From: Elliot Yibaebi Date: Thu, 23 Jan 2025 18:49:49 +0100 Subject: [PATCH] add select all functionality to captured apis table --- .../api-scan/captured-apis/index.hbs | 31 ++++++- .../api-scan/captured-apis/index.scss | 5 +- .../api-scan/captured-apis/index.ts | 53 ++++++++++++ app/styles/_component-variables.scss | 2 +- .../acceptance/file-details/api-scan-test.js | 86 +++++++++++++++++++ translations/en.json | 1 + translations/ja.json | 1 + 7 files changed, 174 insertions(+), 5 deletions(-) diff --git a/app/components/file-details/api-scan/captured-apis/index.hbs b/app/components/file-details/api-scan/captured-apis/index.hbs index 48e31e2d4..b2245bfe3 100644 --- a/app/components/file-details/api-scan/captured-apis/index.hbs +++ b/app/components/file-details/api-scan/captured-apis/index.hbs @@ -1,4 +1,4 @@ -{{#if this.fetchCapturedApis.isRunning}} +{{#if (and this.fetchCapturedApis.isRunning this.showAPIListLoadingState)}}
@@ -25,13 +25,38 @@ >
{{t 'capturedApiListTitle'}} + + + + + {{t 'selectAll'}} + + + {{#if this.selectAllCapturedApis.isRunning}} + + {{/if}} + + {{#each pgc.currentPageResults as |ca|}} diff --git a/app/components/file-details/api-scan/captured-apis/index.scss b/app/components/file-details/api-scan/captured-apis/index.scss index 8e892c12f..908ec03fb 100644 --- a/app/components/file-details/api-scan/captured-apis/index.scss +++ b/app/components/file-details/api-scan/captured-apis/index.scss @@ -5,8 +5,11 @@ .captured-api-title { padding: 1em; + } + + .select-all-captured-api-cta { background-color: var( - --file-details-api-scan-captured-apis-title-background + --file-details-api-scan-select-all-captured-api-background ); } } diff --git a/app/components/file-details/api-scan/captured-apis/index.ts b/app/components/file-details/api-scan/captured-apis/index.ts index 127c7441d..ff15f5683 100644 --- a/app/components/file-details/api-scan/captured-apis/index.ts +++ b/app/components/file-details/api-scan/captured-apis/index.ts @@ -10,6 +10,7 @@ import type IntlService from 'ember-intl/services/intl'; import type { DS } from 'ember-data'; import ENV from 'irene/config/environment'; +import parseError from 'irene/utils/parse-error'; import type { PaginationProviderActionsArgs } from 'irene/components/ak-pagination-provider'; import type FileModel from 'irene/models/file'; import type CapturedApiModel from 'irene/models/capturedapi'; @@ -41,6 +42,8 @@ export default class FileDetailsApiScanCapturedApisComponent extends Component { const url = [ ENV.endpoints['files'], @@ -137,6 +154,8 @@ export default class FileDetailsApiScanCapturedApisComponent extends Component { + this.showAPIListLoadingState = false; + + try { + await this.ajax.put(this.modAllSelectedAPIsEndpoint, { + data: { is_active: is_active }, + namespace: ENV.namespace_v2, + }); + + await this.fetchCapturedApis.perform(this.limit, this.offset); + await this.getAllAPIsSelectedStatus.perform(); + this.setSelectedApiCount.perform(); + } catch (err) { + this.notify.error(parseError(err, this.intl.t('tPleaseTryAgain'))); + } finally { + this.showAPIListLoadingState = true; + } + }); + + getAllAPIsSelectedStatus = task(async () => { + try { + const allAPIsSelected = await this.ajax.request<{ is_active: boolean }>( + this.modAllSelectedAPIsEndpoint, + { + namespace: ENV.namespace_v2, + } + ); + + this.allAPIsSelected = allAPIsSelected.is_active; + } catch (err) { + this.notify.error(parseError(err, this.intl.t('tPleaseTryAgain'))); + } + }); + fetchCapturedApis = task(async (limit: number, offset: number) => { try { this.capturedApiResponse = (await this.store.query('capturedapi', { diff --git a/app/styles/_component-variables.scss b/app/styles/_component-variables.scss index 06f06832f..5e2275e13 100644 --- a/app/styles/_component-variables.scss +++ b/app/styles/_component-variables.scss @@ -901,7 +901,7 @@ body { --file-details-api-scan-captured-apis-border-radius: var(--border-radius); --file-details-api-scan-captured-apis-card-box-shadow: var(--box-shadow-3); --file-details-api-scan-captured-apis-card-background: var(--background-main); - --file-details-api-scan-captured-apis-title-background: var( + --file-details-api-scan-select-all-captured-api-background: var( --neutral-grey-100 ); --file-details-api-scan-captured-apis-container-background: var( diff --git a/tests/acceptance/file-details/api-scan-test.js b/tests/acceptance/file-details/api-scan-test.js index cb570dad7..22c4a80c4 100644 --- a/tests/acceptance/file-details/api-scan-test.js +++ b/tests/acceptance/file-details/api-scan-test.js @@ -261,6 +261,92 @@ module('Acceptance | file-details/api-scan', function (hooks) { .hasText(t('apiScan')); }); + test.each( + 'it selects and unselects all captured APIs', + [true, false], + async function (assert, is_active) { + // Return opposite toggle states for all API endpoints + this.server.db.capturedapis.update({ is_active }); + + this.server.get('/v2/files/:id/capturedapis', (schema) => { + const results = schema.capturedapis.all().models; + + return { count: results.length, previous: null, next: null, results }; + }); + + this.server.put('/v2/files/:id/toggle_captured_apis', (schema, req) => { + const { is_active } = JSON.parse(req.requestBody); + + const results = schema.capturedapis.all().models; + + results.forEach((api) => api.update({ is_active })); + + return { count: results.length, previous: null, next: null, results }; + }); + + this.server.get('/v2/files/:id/toggle_captured_apis', () => { + return { is_active }; + }); + + await visit(`/dashboard/file/${this.file.id}/api-scan`); + + assert + .dom('[data-test-fileDetails-apiScan-breadcrumbContainer]') + .exists(); + assert.dom('[data-test-fileDetailsSummary-root]').exists(); + + assert + .dom('[data-test-fileDetails-apiScan-tabs="api-scan-tab"]') + .hasText(t('apiScan')); + + assert + .dom('[data-test-fileDetails-apiScan-capturedApi-title]') + .hasText(t('capturedApiListTitle')); + + assert + .dom('[data-test-fileDetails-apiScan-selectAllCapturedApis-text]') + .hasText(t('selectAll')); + + let apiEndpoints = findAll( + '[data-test-fileDetails-apiScan-capturedApi-endpointContainer]' + ); + + assert.strictEqual(apiEndpoints.length, 10); + + // All APIs should reflect the correct states + apiEndpoints.forEach((endpoint) => { + const endpointSelector = + '[data-test-fileDetails-apiScan-capturedApi-endpointSelectCheckbox]'; + + if (is_active) { + assert.dom(endpointSelector, endpoint).isNotDisabled().isChecked(); + } else { + assert.dom(endpointSelector, endpoint).isNotDisabled().isNotChecked(); + } + }); + + await click( + '[data-test-fileDetails-apiScan-selectAllCapturedApis-checkbox]' + ); + + apiEndpoints = findAll( + '[data-test-fileDetails-apiScan-capturedApi-endpointContainer]' + ); + + // All APIs should reflect the correct states + apiEndpoints.forEach((endpoint) => { + const endpointSelector = + '[data-test-fileDetails-apiScan-capturedApi-endpointSelectCheckbox]'; + + if (is_active) { + assert.dom(endpointSelector, endpoint).isNotDisabled().isNotChecked(); + } else { + assert.dom(endpointSelector, endpoint).isNotDisabled().isChecked(); + } + }); + } + ); + test('test toggle api endpoint selection', async function (assert) { this.server.get('/v2/files/:id/capturedapis', (schema, req) => { const results = req.queryParams.is_active diff --git a/translations/en.json b/translations/en.json index 2d965a10f..b2f250b9d 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1386,6 +1386,7 @@ "security": "Security", "securityDashboard": "Security Dashboard", "securityDashboardDesc": "For security researchers to perform manual assessments (PT)", + "selectAll": "Select All", "selectAnyTeam": "Please select any team", "selectThePath": "Select the path", "selectDevice": "Select the device", diff --git a/translations/ja.json b/translations/ja.json index a64974cba..28140a315 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -1386,6 +1386,7 @@ "security": "セキュリティ", "securityDashboard": "Security Dashboard", "securityDashboardDesc": "For security researchers to perform manual assessments (PT)", + "selectAll": "Select All", "selectAnyTeam": "チームを選択してください", "selectThePath": "Select the path", "selectDevice": "デバイスを選択する",