Skip to content

Commit

Permalink
add select all functionality to captured apis table
Browse files Browse the repository at this point in the history
  • Loading branch information
Elliot Yibaebi committed Jan 27, 2025
1 parent 2e5b844 commit d385b08
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 5 deletions.
31 changes: 28 additions & 3 deletions app/components/file-details/api-scan/captured-apis/index.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{#if this.fetchCapturedApis.isRunning}}
{{#if (and this.fetchCapturedApis.isRunning this.showAPIListLoadingState)}}
<AkStack @justifyContent='center' class='mt-7'>
<div local-class='captured-api-status-card'>
<FileDetails::ApiScan::CapturedApis::Loading />
Expand All @@ -25,13 +25,38 @@
>
<div class='mb-1'>
<AkTypography
data-test-fileDetails-apiScan-capturedApi-title
@color='textSecondary'
local-class='captured-api-title'
@variant='h6'
data-test-fileDetails-apiScan-capturedApi-title
>
{{t 'capturedApiListTitle'}}
</AkTypography>

<AkStack
@alignItems='center'
@spacing='1'
local-class='select-all-captured-api-cta'
class='w-full px-2 py-1'
>
<AkCheckbox
@disabled={{this.selectAllCapturedApis.isRunning}}
@checked={{this.allAPIsSelected}}
@onChange={{this.handleSelectAllCapturedApis}}
data-test-fileDetails-apiScan-selectAllCapturedApis-checkbox
/>

<AkTypography
@fontWeight='bold'
data-test-fileDetails-apiScan-selectAllCapturedApis-text
>
{{t 'selectAll'}}
</AkTypography>

{{#if this.selectAllCapturedApis.isRunning}}
<AkLoader @size={{13}} class='ml-1' />
{{/if}}
</AkStack>

<AkStack @direction='column' @spacing='1.5'>
{{#each pgc.currentPageResults as |ca|}}
<AkDivider />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
}
Expand Down
53 changes: 53 additions & 0 deletions app/components/file-details/api-scan/captured-apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -41,6 +42,8 @@ export default class FileDetailsApiScanCapturedApisComponent extends Component<F
@service('notifications') declare notify: NotificationService;

@tracked selectedCount = 0;
@tracked allAPIsSelected = false;
@tracked showAPIListLoadingState = true;
@tracked capturedApiResponse: CapturedApiQueryResponse | null = null;
@tracked limit = 10;
@tracked offset = 0;
Expand All @@ -51,6 +54,7 @@ export default class FileDetailsApiScanCapturedApisComponent extends Component<F
) {
super(owner, args);

this.getAllAPIsSelectedStatus.perform();
this.setSelectedApiCount.perform();
this.fetchCapturedApis.perform(this.limit, this.offset);
}
Expand All @@ -67,6 +71,14 @@ export default class FileDetailsApiScanCapturedApisComponent extends Component<F
return this.totalCapturedApiCount === 0;
}

get modAllSelectedAPIsEndpoint() {
return [
ENV.endpoints['files'],
this.args.file.id,
'toggle_captured_apis',
].join('/');
}

setFooterComponentDetails() {
if (this.hasNoCapturedApi) {
return;
Expand Down Expand Up @@ -96,6 +108,11 @@ export default class FileDetailsApiScanCapturedApisComponent extends Component<F
this.fetchCapturedApis.perform(limit, this.offset);
}

@action
handleSelectAllCapturedApis(_: Event, checked: boolean) {
this.selectAllCapturedApis.perform(checked);
}

getSelectedApis = task(async () => {
const url = [
ENV.endpoints['files'],
Expand Down Expand Up @@ -137,6 +154,8 @@ export default class FileDetailsApiScanCapturedApisComponent extends Component<F
this.notify.success(this.intl.t('capturedApiSaveSuccessMsg'));

this.setSelectedApiCount.perform();

await this.getAllAPIsSelectedStatus.perform();
} catch (err) {
const error = err as AdapterError;
let errMsg = this.intl.t('tPleaseTryAgain');
Expand All @@ -151,6 +170,40 @@ export default class FileDetailsApiScanCapturedApisComponent extends Component<F
}
});

selectAllCapturedApis = task(async (is_active: boolean) => {
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', {
Expand Down
2 changes: 1 addition & 1 deletion app/styles/_component-variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
86 changes: 86 additions & 0 deletions tests/acceptance/file-details/api-scan-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "デバイスを選択する",
Expand Down

0 comments on commit d385b08

Please sign in to comment.