Vault Automation 2b469deeca
UI: Update client count overview table and add filtering (#8663) (#9057)
* rename query params to match keys from api response

* move type guard check to util

* update color scheme

* remove passing selected namespace to export activity data request

* remove namespace and mount filter toolbar

* update response, sort by client count number

* remove default page size for testing

* implement table and filters in overview tab

* remove old query params

* cleanup unused args

* revert page header changes

* update mirage, remove month from utils

* update client count utils

* one more color!

* reset table to page 1;

* workaround to force Hds::Pagination::Numbered to update when currentPage changes

* add empty state test for no attribution

* delete unused methods

* add test for new utils

* add changelog

* rename changelog

---------

Co-authored-by: claire bontempo <cbontempo@hashicorp.com>
2025-09-02 13:43:02 -07:00

232 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, fillIn, findAll, render, waitFor } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
const MOCK_DATA = [
{ island: 'Maldives', visit_length: 5, is_booked: false, trip_date: '2025-06-22T00:00:00.000Z' },
{ island: 'Bora Bora', visit_length: 7, is_booked: true, trip_date: '2025-03-15T00:00:00.000Z' },
{ island: 'Fiji', visit_length: 10, is_booked: true, trip_date: '2025-09-08T00:00:00.000Z' },
{ island: 'Santorini', visit_length: 4, is_booked: false, trip_date: '2026-04-10T00:00:00.000Z' },
{ island: 'Maui', visit_length: 8, is_booked: true, trip_date: '2026-01-18T00:00:00.000Z' },
{ island: 'Seychelles', visit_length: 6, is_booked: false, trip_date: '2025-12-03T00:00:00.000Z' },
];
module('Integration | Component | clients/table', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(async function () {
this.data = undefined;
this.columns = [
{ key: 'island', label: 'Islands', isSortable: true },
{ key: 'visit_length', label: 'Visit length', isSortable: true },
{ key: 'is_booked', label: 'Vacation booking status', isSortable: true },
{ key: 'trip_date', label: 'Date trip starts', isSortable: true },
];
this;
this.initiallySortBy = undefined;
this.setPageSize = undefined;
this.showPaginationSizeSelector = undefined;
// helper function to setup table with X number of simple objects for pagination tests
this.mockMoreData = (recordNumber) => {
const record = (i) => ({ id: i, name: `record-${i}` });
this.columns = [
{ key: 'id', label: 'ID', isSortable: true },
{ key: 'name', label: 'Name', isSortable: true },
];
this.data = Array.from({ length: recordNumber }, (_, i) => record(i));
};
this.renderComponent = async () => {
return render(hbs`
<Clients::Table
@columns={{this.columns}}
@data={{this.data}}
@initiallySortBy={{this.initiallySortBy}}
@setPageSize={{this.setPageSize}}
@showPaginationSizeSelector={{this.showPaginationSizeSelector}}
/>`);
};
});
test('it renders default empty state when no data exists', async function (assert) {
await this.renderComponent();
assert.dom(CLIENT_COUNT.card('table empty state')).hasText('No data to display');
});
test('it renders yielded empty state block when no data exists', async function (assert) {
await render(hbs`
<Clients::Table @columns={{this.columns}} @data={{this.data}}>
<:emptyState>Oh no, there's no data!</:emptyState>
</Clients::Table>`);
assert.dom(CLIENT_COUNT.card('table empty state')).hasText("Oh no, there's no data!");
});
test('it renders and paginates data', async function (assert) {
this.data = MOCK_DATA;
await this.renderComponent();
assert.dom(CLIENT_COUNT.card('table empty state')).doesNotExist();
assert.dom(GENERAL.paginationInfo).hasText(`15 of ${this.data.length}`);
await click(GENERAL.nextPage);
assert.dom(GENERAL.tableData(0, 'island')).hasText('Seychelles', 'it paginates the data');
});
test('it sorts table data', async function (assert) {
this.data = MOCK_DATA;
const assertSortOrder = (expectedValues, { column, page }) => {
expectedValues.forEach((value, idx) => {
assert
.dom(GENERAL.tableData(idx, column))
.hasText(value, `page ${page}, row ${idx} has ${column}: ${value}`);
});
};
await this.renderComponent();
const [firstColumn, secondColumn, thirdColumn, fourthColumn] = findAll(GENERAL.icon('swap-vertical'));
await click(firstColumn);
assertSortOrder(['Bora Bora', 'Fiji', 'Maldives', 'Maui', 'Santorini'], { column: 'island', page: 1 });
await click(GENERAL.nextPage);
assertSortOrder(['Seychelles'], { column: 'island', page: 2 });
await click(GENERAL.prevPage);
await click(secondColumn);
assertSortOrder(['4', '5', '6', '7', '8'], { column: 'visit_length', page: 1 });
await click(GENERAL.nextPage);
assertSortOrder(['10'], { column: 'visit_length', page: 2 });
await click(GENERAL.prevPage);
await click(thirdColumn);
assertSortOrder(['false', 'false', 'false', 'true', 'true'], { column: 'is_booked', page: 1 });
await click(GENERAL.nextPage);
assertSortOrder(['true'], { column: 'is_booked', page: 2 });
await click(GENERAL.prevPage);
await click(fourthColumn);
assertSortOrder(
[
'2025-03-15T00:00:00.000Z',
'2025-06-22T00:00:00.000Z',
'2025-09-08T00:00:00.000Z',
'2025-12-03T00:00:00.000Z',
'2026-01-18T00:00:00.000Z',
],
{
column: 'trip_date',
page: 1,
}
);
await click(GENERAL.nextPage);
assertSortOrder(['2026-04-10T00:00:00.000Z'], {
column: 'trip_date',
page: 2,
});
});
test('it pre-sorts table data if @initiallySortBy is set', async function (assert) {
this.data = MOCK_DATA;
this.initiallySortBy = { column: 'visit_length', direction: 'desc' };
await this.renderComponent();
assert.dom(GENERAL.tableColumnHeader(2, { isAdvanced: true })).hasAttribute('aria-sort', 'descending');
assert
.dom(`${GENERAL.tableColumnHeader(2, { isAdvanced: true })} ${GENERAL.icon('arrow-down')}`)
.exists();
const firstPage = ['Fiji', 'Maui', 'Bora Bora', 'Seychelles', 'Maldives'];
firstPage.forEach((value, idx) => {
assert.dom(GENERAL.tableData(idx, 'island')).hasText(value, `page 1, row ${idx} has ${value}`);
});
await click(GENERAL.nextPage);
const secondPage = ['Santorini'];
secondPage.forEach((value, idx) => {
assert.dom(GENERAL.tableData(idx, 'island')).hasText(value, `page 2, row ${idx} has ${value}`);
});
});
test('it sets page size if @setPageSize has a value', async function (assert) {
this.mockMoreData(15);
this.setPageSize = 8; // component default for testing is 3, so set to anything but 3
await this.renderComponent();
assert.dom(GENERAL.paginationInfo).hasText(`1${this.setPageSize} of ${this.data.length}`);
let idx = 7; // 8th item, table items are 0-indexed
assert
.dom(GENERAL.tableData(idx, 'name'))
.hasText(this.data[idx].name, 'last row is 8th item in dataset');
await click(GENERAL.nextPage);
idx = 8; // 9th item, table items are 0-indexed
assert
.dom(GENERAL.tableData(0, 'name'))
.hasText(this.data[idx].name, 'first row on page 2 is 9th item in dataset');
});
test('it renders size selector if @showPaginationSizeSelector is true', async function (assert) {
this.mockMoreData(10);
this.setPageSize = 5;
this.showPaginationSizeSelector = true;
await this.renderComponent();
assert.dom(GENERAL.tableRow()).exists({ count: 5 }, '5 rows render');
assert.dom(GENERAL.paginationInfo).hasText(`1${this.setPageSize} of ${this.data.length}`);
assert.dom(GENERAL.paginationSizeSelector).hasValue('5');
let idx = 4; // rows and ids are 0-indexed
assert.dom(GENERAL.tableData(idx, 'id')).hasText(`${idx}`, `last row is ${idx + 1}th item in dataset`);
await fillIn(GENERAL.paginationSizeSelector, '10');
idx = 9;
assert.dom(GENERAL.tableRow()).exists({ count: 10 }, '10 rows render');
assert.dom(GENERAL.paginationSizeSelector).hasValue('10', 'it updates the size selector to 10');
assert.dom(GENERAL.tableData(idx, 'id')).hasText(`${idx}`, `last row is ${idx + 1}th item in dataset`);
});
test('it renders "Deleted" badge for "mount_type" keys if the value is "deleted mount"', async function (assert) {
this.columns = [
{ key: 'mount_type', label: 'Mount type' },
{ key: 'mount_path', label: 'Mount path' },
];
this.data = [
{ mount_type: 'deleted mount', mount_path: 'auth/userpass/' },
{ mount_type: 'ns_token', mount_path: 'auth/token/' },
];
await this.renderComponent();
assert.dom('.hds-badge').exists({ count: 1 }, 'only one badge renders');
assert
.dom(`${GENERAL.tableData(0, 'mount_type')} .hds-badge`)
.exists('it renders a badge for the deleted mount')
.hasText('Deleted');
});
test('it resets pagination when data changes', async function (assert) {
// We need more than 5 rows, so here's more mock data!
const moreData = [
{ island: 'Tahiti', visit_length: 12, is_booked: true, trip_date: '2025-05-10T00:00:00.000Z' },
{ island: 'Barbados', visit_length: 6, is_booked: false, trip_date: '2025-08-25T00:00:00.000Z' },
{ island: 'Cyprus', visit_length: 9, is_booked: true, trip_date: '2026-03-12T00:00:00.000Z' },
{ island: 'Jamaica', visit_length: 7, is_booked: false, trip_date: '2025-11-05T00:00:00.000Z' },
{ island: 'Crete', visit_length: 11, is_booked: true, trip_date: '2026-06-18T00:00:00.000Z' },
{ island: 'Aruba', visit_length: 5, is_booked: false, trip_date: '2025-10-14T00:00:00.000Z' },
];
this.data = [...MOCK_DATA, ...moreData];
this.showPaginationSizeSelector = true;
await this.renderComponent();
await fillIn(GENERAL.paginationSizeSelector, '10'); // Default is 5, so change to something else
await click(GENERAL.nextPage);
assert.dom(GENERAL.paginationInfo).hasText(`1112 of ${this.data.length}`, 'it navigates to next page');
assert.dom(GENERAL.tableRow()).exists({ count: 2 }, '2 row renders');
// Changing the @data arg should trigger an update and reset pagination
// We have to use `this.set` to trigger did-update
this.set('data', [
{ island: 'Palawan', visit_length: 9, is_booked: true, trip_date: '2025-11-14T00:00:00.000Z' },
{ island: 'Mykonos', visit_length: 3, is_booked: false, trip_date: '2026-02-28T00:00:00.000Z' },
]);
// There's a workaround using next() from @ember/runloop because the Hds::Pagination::Numbered component
// doesn't re-render when @currentPage updates. When that's fixed at the source we should be able to remove waitFor
await waitFor(GENERAL.paginationInfo);
assert.dom(GENERAL.paginationInfo).hasText(`12 of ${this.data.length}`);
assert.dom(GENERAL.tableRow()).exists({ count: 2 }, '2 rows render');
assert.dom(GENERAL.paginationSizeSelector).hasValue('10', 'page selector is unchanged when data updates');
});
});