{
{
key: 'client_first_used_time',
label: 'Initial usage',
- tooltip: 'When the client ID was first used in the selected billing period.',
+ tooltip:
+ 'First usage date in the billing period. Vault only provides this data for clients initially used after upgrading to version 1.21.',
},
{ key: 'mount_path', label: 'Mount path' },
{ key: 'mount_type', label: 'Mount type' },
diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs
index 451031c2bb..49785d3535 100644
--- a/ui/app/components/clients/page/counts.hbs
+++ b/ui/app/components/clients/page/counts.hbs
@@ -16,7 +16,7 @@
-
+
This is the dashboard for your overall client count usages. Review Vault's
client counting documentation
diff --git a/ui/app/routes/vault/cluster/clients/counts.ts b/ui/app/routes/vault/cluster/clients/counts.ts
index 594397b359..6d99fd3b70 100644
--- a/ui/app/routes/vault/cluster/clients/counts.ts
+++ b/ui/app/routes/vault/cluster/clients/counts.ts
@@ -77,25 +77,29 @@ export default class ClientsCountsRoute extends Route {
}
async fetchAndFormatExportData(startTimestamp: string | undefined, endTimestamp: string | undefined) {
- const adapter = this.store.adapterFor('clients/activity');
- let exportData, exportError;
- try {
- const resp = await adapter.exportData({
- // the API only accepts json or csv
- format: 'json',
- start_time: startTimestamp,
- end_time: endTimestamp,
- });
- const jsonLines = await resp.text();
- const lines = jsonLines.trim().split('\n');
- exportData = lines.map((line: string) => JSON.parse(line));
- } catch (error) {
- // Ideally we would not handle errors manually but this is the pattern the other client.counts
- // route follow since the sys/internal/counters API doesn't always return helpful error messages.
- // When these routes are migrated away from ember data we should revisit the error handling.
- exportError = error as AdapterError;
+ // The "Client List" tab is only available on enterprise versions
+ if (this.version.isEnterprise) {
+ const adapter = this.store.adapterFor('clients/activity');
+ let exportData, exportError;
+ try {
+ const resp = await adapter.exportData({
+ // the API only accepts json or csv
+ format: 'json',
+ start_time: startTimestamp,
+ end_time: endTimestamp,
+ });
+ const jsonLines = await resp.text();
+ const lines = jsonLines.trim().split('\n');
+ exportData = lines.map((line: string) => JSON.parse(line));
+ } catch (error) {
+ // Ideally we would not handle errors manually but this is the pattern the other client.counts
+ // route follow since the sys/internal/counters API doesn't always return helpful error messages.
+ // When these routes are migrated away from ember data we should revisit the error handling.
+ exportError = error as AdapterError;
+ }
+ return { exportData, exportError };
}
- return { exportData, exportError };
+ return { exportData: null, exportError: null };
}
async model(params: ClientsCountsRouteParams) {
diff --git a/ui/app/templates/vault/cluster/clients/counts/client-list.hbs b/ui/app/templates/vault/cluster/clients/counts/client-list.hbs
index 31b0ee9f6a..4695d402d9 100644
--- a/ui/app/templates/vault/cluster/clients/counts/client-list.hbs
+++ b/ui/app/templates/vault/cluster/clients/counts/client-list.hbs
@@ -10,7 +10,7 @@
>
<:actions>
{{#if (eq this.model.exportError.httpStatus 403)}}
-
+
Viewing export data requires
sudo
permissions.
diff --git a/ui/tests/acceptance/clients/counts-test.js b/ui/tests/acceptance/clients/counts-test.js
index 5cfc72bbe0..ac846c5738 100644
--- a/ui/tests/acceptance/clients/counts-test.js
+++ b/ui/tests/acceptance/clients/counts-test.js
@@ -14,13 +14,15 @@ import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
import timestamp from 'core/utils/timestamp';
import { overrideResponse } from 'vault/tests/helpers/stubs';
+import { format } from 'date-fns';
module('Acceptance | clients | counts', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(async function () {
- sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW));
+ this.timestampStub = sinon.stub(timestamp, 'now');
+ this.timestampStub.returns(STATIC_NOW);
clientsHandler(this.server);
this.store = this.owner.lookup('service:store');
return login();
@@ -38,6 +40,17 @@ module('Acceptance | clients | counts', function (hooks) {
.hasText('Only historical data may be queried. No data is available for the current month.');
});
+ test('it does not make a request to the export api on community versions', async function (assert) {
+ assert.expect(1);
+ this.owner.lookup('service:version').type = 'community';
+ server.get('/sys/internal/counters/activity/export', () => {
+ // passing "false" because a request should NOT be made, so if this assertion is hit we want it to fail
+ assert.true(false, 'it does not make request to export API on community versions ');
+ });
+ await visit('/vault/clients/counts/overview');
+ assert.dom(GENERAL.tab('client list')).doesNotExist();
+ });
+
test('it should redirect to counts overview route for transitions to parent', async function (assert) {
await visit('/vault/clients');
assert.strictEqual(currentURL(), '/vault/clients/counts/overview', 'Redirects to counts overview route');
@@ -137,11 +150,21 @@ module('Acceptance | clients | counts', function (hooks) {
// Change date to add query params
await click(CLIENT_COUNT.dateRange.edit);
await click(CLIENT_COUNT.dateRange.dropdownOption(1));
+ assert
+ .dom(CLIENT_COUNT.activityTimestamp)
+ .hasTextContaining(`Dashboard last updated: ${format(STATIC_NOW, 'MMM d yyyy')}`);
// Save URL with query params before clicking refresh
const url = currentURL();
+ // re-stub with a completely different year/month/day before clicking refresh
+ // to mock the timestamp updating when page reloads
+ const fakeUpdatedNow = new Date('2025-07-02T23:25:13Z');
+ this.timestampStub.returns(fakeUpdatedNow);
await click(GENERAL.button('Refresh page'));
assert.true(this.refreshSpy.calledOnce, 'router.refresh() is called once');
assert.strictEqual(currentURL(), url, 'url is the same after clicking refresh');
+ assert
+ .dom(CLIENT_COUNT.activityTimestamp)
+ .hasTextContaining(`Dashboard last updated: ${format(fakeUpdatedNow, 'MMM d yyyy')}`);
});
test('enterprise: it refreshes the client-list route and preserves query params', async function (assert) {
@@ -154,11 +177,21 @@ module('Acceptance | clients | counts', function (hooks) {
// Change date to add query params
await click(CLIENT_COUNT.dateRange.edit);
await click(CLIENT_COUNT.dateRange.dropdownOption(1));
+ assert
+ .dom(CLIENT_COUNT.activityTimestamp)
+ .hasTextContaining(`Dashboard last updated: ${format(STATIC_NOW, 'MMM d yyyy')}`);
// Save URL with query params before clicking refresh
const url = currentURL();
+ // re-stub with a completely different year/month/day before clicking refresh
+ // to mock the timestamp updating when page reloads
+ const fakeUpdatedNow = new Date('2025-07-02T23:25:13Z');
+ this.timestampStub.returns(fakeUpdatedNow);
await click(GENERAL.button('Refresh page'));
assert.true(this.refreshSpy.calledOnce, 'router.refresh() is called once');
assert.strictEqual(currentURL(), url, 'url is the same after clicking refresh');
+ assert
+ .dom(CLIENT_COUNT.activityTimestamp)
+ .hasTextContaining(`Dashboard last updated: ${format(fakeUpdatedNow, 'MMM d yyyy')}`);
});
});
});
diff --git a/ui/tests/helpers/clients/client-count-selectors.ts b/ui/tests/helpers/clients/client-count-selectors.ts
index 2a160e61d6..7901d325e3 100644
--- a/ui/tests/helpers/clients/client-count-selectors.ts
+++ b/ui/tests/helpers/clients/client-count-selectors.ts
@@ -5,6 +5,7 @@
// TODO: separate nested into distinct exported consts
export const CLIENT_COUNT = {
+ activityTimestamp: '[data-test-activity-timestamp]',
card: (name: string) => `[data-test-card="${name}"]`,
counts: {
description: '[data-test-counts-description]',
diff --git a/ui/tests/integration/components/clients/filter-toolbar-test.js b/ui/tests/integration/components/clients/filter-toolbar-test.js
index fe559f96cf..647e658a34 100644
--- a/ui/tests/integration/components/clients/filter-toolbar-test.js
+++ b/ui/tests/integration/components/clients/filter-toolbar-test.js
@@ -11,28 +11,48 @@ import { GENERAL } from 'vault/tests/helpers/general-selectors';
import sinon from 'sinon';
import { FILTERS } from 'vault/tests/helpers/clients/client-count-selectors';
import { ClientFilters } from 'core/utils/client-count-utils';
+import { parseAPITimestamp } from 'core/utils/date-formatters';
module('Integration | Component | clients/filter-toolbar', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
- this.dataset = [
- { namespace_path: '', mount_type: 'userpass/', mount_path: 'auth/auto/eng/core/auth/core-gh-auth/' },
- { namespace_path: '', mount_type: 'userpass/', mount_path: 'auth/auto/eng/core/auth/core-gh-auth/' },
- { namespace_path: '', mount_type: 'userpass/', mount_path: 'auth/userpass-root/' },
- { namespace_path: 'admin/', mount_type: 'token/', mount_path: 'auth/token/' },
- { namespace_path: 'ns1/', mount_type: 'token/', mount_path: 'auth/token/' },
- { namespace_path: 'ns1/', mount_type: 'ns_token/', mount_path: 'auth/token/' },
- ];
+ this.generateData = ({ withTimestamps = false }) => {
+ const timestamps = [
+ '2025-04-27T07:36:21Z',
+ '2025-04-01T00:00:00Z',
+ '2025-03-21T05:36:21Z',
+ '2025-03-21T07:26:21Z',
+ '2025-02-06T03:36:21Z',
+ '2025-01-29T01:36:21Z',
+ ];
+ const data = [
+ { namespace_path: '', mount_type: 'userpass/', mount_path: 'auth/auto/eng/core/auth/core-gh-auth/' },
+ { namespace_path: '', mount_type: 'userpass/', mount_path: 'auth/auto/eng/core/auth/core-gh-auth/' },
+ { namespace_path: '', mount_type: 'userpass/', mount_path: 'auth/userpass-root/' },
+ { namespace_path: 'admin/', mount_type: 'token/', mount_path: 'auth/token/' },
+ { namespace_path: 'ns1/', mount_type: 'token/', mount_path: 'auth/token/' },
+ { namespace_path: 'ns1/', mount_type: 'ns_token/', mount_path: 'auth/token/' },
+ ];
+ // Only activity export data from Vault versions 1.21 or later will have a `client_first_used_time`
+ return withTimestamps
+ ? data.map((d, idx) => ({ ...d, client_first_used_time: timestamps[idx] }))
+ : data;
+ };
+
this.onFilter = sinon.spy();
this.filterQueryParams = { namespace_path: '', mount_path: '', mount_type: '', month: '' };
-
+ this.dataset = undefined;
+ this.dropdownMonths = undefined;
+ this.isExportData = undefined;
this.renderComponent = async () => {
await render(hbs`
`);
};
@@ -41,6 +61,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
namespace_path: 'admin/',
mount_path: 'auth/userpass-root/',
mount_type: 'token/',
+ month: '2025-04-01T00:00:00Z',
};
};
@@ -54,19 +75,23 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
// select mount type
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
await click(FILTERS.dropdownItem('token/'));
+ // select month
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ await click(FILTERS.dropdownItem('2025-04-01T00:00:00Z'));
};
});
- test('it renders dropdowns', async function (assert) {
+ test('it renders dropdowns when there is no data', async function (assert) {
await this.renderComponent();
-
assert.dom(FILTERS.dropdownToggle(ClientFilters.NAMESPACE)).hasText('Namespace');
assert.dom(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH)).hasText('Mount path');
assert.dom(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE)).hasText('Mount type');
+ assert.dom(FILTERS.dropdownToggle(ClientFilters.MONTH)).hasText('Month');
assert.dom(FILTERS.tagContainer).hasText('Filters applied: None');
});
test('it renders dropdown items and does not include duplicates', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: true });
await this.renderComponent();
const expectedNamespaces = ['root', 'admin/', 'ns1/'];
const expectedMountPaths = [
@@ -75,6 +100,13 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
'auth/token/',
];
const expectedMountTypes = ['userpass/', 'token/', 'ns_token/'];
+ // The component normalizes timestamps to the first of the month
+ const expectedMonths = [
+ '2025-04-01T00:00:00Z',
+ '2025-03-01T00:00:00Z',
+ '2025-02-01T00:00:00Z',
+ '2025-01-01T00:00:00Z',
+ ];
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
assert.dom('li button').exists({ count: 3 }, 'list renders 3 namespaces');
@@ -98,9 +130,33 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
const m = expectedMountTypes[idx];
assert.dom(item).hasText(m, `it renders mount_type: ${m}`);
});
+
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ assert.dom('li button').exists({ count: 4 }, 'list renders 4 months');
+ findAll('li button').forEach((item, idx) => {
+ const m = expectedMonths[idx];
+ const display = parseAPITimestamp(m, 'MMMM yyyy');
+ assert.dom(item).hasText(display, `it renders month: ${m}`);
+ });
+ });
+
+ test('it renders months passed in as an arg instead of from dataset', async function (assert) {
+ // Include timestamps in the dataset AND pass in months to ensure @dropdownMonths overrides the timestamps in dataset
+ this.dataset = this.generateData({ withTimestamps: true });
+ this.dropdownMonths = ['2025-10-01T07:36:21Z', '2025-09-01T02:38:21Z', '2025-08-01T03:56:21Z'];
+ await this.renderComponent();
+
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ assert.dom('li button').exists({ count: 3 }, 'list renders 3 months');
+ findAll('li button').forEach((item, idx) => {
+ const m = this.dropdownMonths[idx];
+ const display = parseAPITimestamp(m, 'MMMM yyyy');
+ assert.dom(item).hasText(display, `it renders month: ${m}`);
+ });
});
test('it searches dropdown items', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: true });
await this.renderComponent();
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
@@ -121,21 +177,37 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
await waitUntil(() => dropdownItems.length === 2);
assert.dom('ul').hasText('token/ ns_token/', 'it renders matching mount types');
- // confirm that search input is cleared and dropdown renders all items again when re-opened
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ await typeIn(FILTERS.dropdownSearch(ClientFilters.MONTH), 'y');
+ dropdownItems = findAll('li button');
+ await waitUntil(() => dropdownItems.length === 2);
+ assert.dom('ul').hasText('February 2025 January 2025', 'it renders matching months');
+ // Months can be searched by the ISO timestamp or the display value
+ await fillIn(FILTERS.dropdownSearch(ClientFilters.MONTH), '4');
+ dropdownItems = findAll('li button');
+ await waitUntil(() => dropdownItems.length === 1);
+ assert.dom('ul').hasText('April 2025', 'it renders matching months');
+
+ // Re-open each dropdown to confirm search input and dropdown reset after close
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
- assert.dom('ul').hasText('root admin/ ns1/', 'it resets filter and renders all namespace path');
+ assert.dom('ul').hasText('root admin/ ns1/', 'namespace dropdown resets on close');
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
assert
.dom('ul')
.hasText(
'auth/auto/eng/core/auth/core-gh-auth/ auth/userpass-root/ auth/token/',
- 'it resets filter and renders all mount paths'
+ 'mount path dropdown resets on close'
);
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
- assert.dom('ul').hasText('userpass/ token/ ns_token/', 'it resets filter and renders all mount types');
+ assert.dom('ul').hasText('userpass/ token/ ns_token/', 'mount types dropdown resets on close');
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ assert
+ .dom('ul')
+ .hasText('April 2025 March 2025 February 2025 January 2025', 'months dropdown resets on close');
});
test('it searches and renders no matches found message', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: true });
await this.renderComponent();
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
@@ -155,10 +227,16 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
dropdownItems = findAll('li button');
await waitUntil(() => dropdownItems.length === 0);
assert.dom('ul').hasText('No matching mount types');
+
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ await fillIn(FILTERS.dropdownSearch(ClientFilters.MONTH), 'no matches');
+ dropdownItems = findAll('li button');
+ await waitUntil(() => dropdownItems.length === 0);
+ assert.dom('ul').hasText('No matching months');
});
test('it renders no items to filter if dropdown is empty', async function (assert) {
- this.dataset = [{ namespace_path: null, mount_type: null, mount_path: null }];
+ this.dataset = [{ namespace_path: null, mount_type: null, mount_path: null, months: null }];
await this.renderComponent();
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
assert.dom('ul').hasText('No namespaces to filter');
@@ -166,6 +244,36 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
assert.dom('ul').hasText('No mount paths to filter');
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
assert.dom('ul').hasText('No mount types to filter');
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ assert.dom('ul').hasText('No months to filter');
+ });
+
+ test('it renders version message when no month data exists and @isExportData is true', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: false });
+ this.isExportData = true;
+ await this.renderComponent();
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ assert
+ .dom('ul')
+ .hasText(
+ 'Filtering by month is only available for clients initially used after upgrading to version 1.21.'
+ );
+ });
+
+ test('it renders no months to filter message when data has no client_first_used_time', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: false });
+ await this.renderComponent();
+ assert.dom(FILTERS.dropdownToggle(ClientFilters.MONTH)).hasText('Month');
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ assert.dom('ul').hasText('No months to filter');
+ });
+
+ test('it renders no months to filter message when @dropdownMonths is empty', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: true });
+ this.dropdownMonths = [];
+ await this.renderComponent();
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ assert.dom('ul').hasText('No months to filter');
});
test('it renders no items to filter if dataset does not contain expected keys', async function (assert) {
@@ -177,9 +285,12 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
assert.dom('ul').hasText('No mount paths to filter');
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
assert.dom('ul').hasText('No mount types to filter');
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ assert.dom('ul').hasText('No months to filter');
});
test('it selects dropdown items and renders a filter tag', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: true });
await this.renderComponent();
// select namespace
@@ -187,34 +298,44 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
await click(FILTERS.dropdownItem('admin/'));
assert.dom(FILTERS.tag(ClientFilters.NAMESPACE, 'admin/')).exists();
assert.dom(FILTERS.tag()).exists({ count: 1 }, '1 filter tag renders');
+ // dropdown should close after an item is selected, reopen to assert the correct item is selected
+ await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
+ assert.dom(FILTERS.dropdownItem('admin/')).hasAttribute('aria-selected', 'true');
+ assert.dom(`${FILTERS.dropdownItem('admin/')} ${GENERAL.icon('check')}`).exists();
// select mount path
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
await click(FILTERS.dropdownItem('auth/userpass-root/'));
assert.dom(FILTERS.tag(ClientFilters.MOUNT_PATH, 'auth/userpass-root/')).exists();
assert.dom(FILTERS.tag()).exists({ count: 2 }, '2 filter tags render');
+ // dropdown should close after an item is selected, reopen to assert the correct item is selected
+ await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
+ assert.dom(FILTERS.dropdownItem('auth/userpass-root/')).hasAttribute('aria-selected', 'true');
+ assert.dom(`${FILTERS.dropdownItem('auth/userpass-root/')} ${GENERAL.icon('check')}`).exists();
// select mount type
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
await click(FILTERS.dropdownItem('token/'));
assert.dom(FILTERS.tag(ClientFilters.MOUNT_TYPE, 'token/')).exists();
assert.dom(FILTERS.tag()).exists({ count: 3 }, '3 filter tags render');
-
- // dropdown closes when an item is selected, reopen each one to assert the correct item is selected
- await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
- assert.dom(FILTERS.dropdownItem('admin/')).hasAttribute('aria-selected', 'true');
- assert.dom(`${FILTERS.dropdownItem('admin/')} ${GENERAL.icon('check')}`).exists();
-
- await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
- assert.dom(FILTERS.dropdownItem('auth/userpass-root/')).hasAttribute('aria-selected', 'true');
- assert.dom(`${FILTERS.dropdownItem('auth/userpass-root/')} ${GENERAL.icon('check')}`).exists();
-
+ // dropdown should close after an item is selected, reopen to assert the correct item is selected
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
assert.dom(FILTERS.dropdownItem('token/')).hasAttribute('aria-selected', 'true');
assert.dom(`${FILTERS.dropdownItem('token/')} ${GENERAL.icon('check')}`).exists();
+
+ // select month
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ await click(FILTERS.dropdownItem('2025-02-01T00:00:00Z'));
+ assert.dom(FILTERS.tag(ClientFilters.MONTH, '2025-02-01T00:00:00Z')).exists();
+ assert.dom(FILTERS.tag()).exists({ count: 4 }, '4 filter tags render');
+ // dropdown should close after an item is selected, reopen to assert the correct item is selected
+ await click(FILTERS.dropdownToggle(ClientFilters.MONTH));
+ assert.dom(FILTERS.dropdownItem('2025-02-01T00:00:00Z')).hasAttribute('aria-selected', 'true');
+ assert.dom(`${FILTERS.dropdownItem('2025-02-01T00:00:00Z')} ${GENERAL.icon('check')}`).exists();
});
test('it fires callback when a filter is selected', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: true });
await this.renderComponent();
// select namespace
@@ -241,32 +362,50 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
});
test('it renders filter tags when initialized with @filterQueryParams', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: true });
this.presetFilters();
await this.renderComponent();
- assert.dom(FILTERS.tag()).exists({ count: 3 }, '3 filter tags render');
+ assert.dom(FILTERS.tag()).exists({ count: 4 }, '4 filter tags render');
assert.dom(FILTERS.tag(ClientFilters.NAMESPACE, 'admin/')).exists();
assert.dom(FILTERS.tag(ClientFilters.MOUNT_PATH, 'auth/userpass-root/')).exists();
assert.dom(FILTERS.tag(ClientFilters.MOUNT_TYPE, 'token/')).exists();
+ assert.dom(FILTERS.tag(ClientFilters.MONTH, '2025-04-01T00:00:00Z')).exists();
});
test('it updates filters tags when initialized with @filterQueryParams', async function (assert) {
- this.filterQueryParams = { namespace_path: 'ns1/', mount_path: 'auth/token/', mount_type: 'ns_token/' };
+ this.dataset = this.generateData({ withTimestamps: true });
+ this.filterQueryParams = {
+ namespace_path: 'ns1/',
+ mount_path: 'auth/token/',
+ mount_type: 'ns_token/',
+ month: '2025-03-01T00:00:00Z',
+ };
await this.renderComponent();
// Check initial filters
- assert.dom(FILTERS.tagContainer).hasText('Filters applied: ns1/ auth/token/ ns_token/');
+ assert
+ .dom(FILTERS.tagContainer)
+ .hasText('Filters applied: ns1/ auth/token/ ns_token/ March 2025 Clear filters');
// Change filters and confirm callback has updated values
await this.selectFilters();
const [afterUpdate] = this.onFilter.lastCall.args;
assert.propEqual(
afterUpdate,
- { namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/', month: '' },
+ {
+ namespace_path: 'admin/',
+ mount_path: 'auth/userpass-root/',
+ mount_type: 'token/',
+ month: '2025-04-01T00:00:00Z',
+ },
'callback fires with updated selection'
);
- assert.dom(FILTERS.tagContainer).hasText('Filters applied: admin/ auth/userpass-root/ token/');
+ assert
+ .dom(FILTERS.tagContainer)
+ .hasText('Filters applied: admin/ auth/userpass-root/ token/ April 2025 Clear filters');
});
test('it clears all filters', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: true });
this.presetFilters();
await this.renderComponent();
await click(GENERAL.button('Clear filters'));
@@ -280,22 +419,29 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
});
test('it clears individual filters', async function (assert) {
+ this.dataset = this.generateData({ withTimestamps: true });
this.presetFilters();
await this.renderComponent();
await click(FILTERS.clearTag('admin/'));
const afterClear = this.onFilter.lastCall.args[0];
assert.propEqual(
afterClear,
- { namespace_path: '', mount_path: 'auth/userpass-root/', mount_type: 'token/', month: '' },
+ {
+ namespace_path: '',
+ mount_path: 'auth/userpass-root/',
+ mount_type: 'token/',
+ month: '2025-04-01T00:00:00Z',
+ },
'onFilter callback fires with empty namespace_path'
);
});
test('it renders an alert when initialized with @filterQueryParams that are not present in the dropdown', async function (assert) {
- this.filterQueryParams = { namespace_path: 'admin/', mount_path: '', mount_type: 'banana' };
+ this.dataset = this.generateData({ withTimestamps: true });
+ this.filterQueryParams = { namespace_path: 'admin/', mount_path: '', mount_type: 'banana', month: '' };
await this.renderComponent();
assert.dom(FILTERS.tag()).exists({ count: 2 }, '2 filter tags render');
- assert.dom(FILTERS.tagContainer).hasText('Filters applied: admin/ banana');
+ assert.dom(FILTERS.tagContainer).hasText('Filters applied: admin/ banana Clear filters');
assert.dom(GENERAL.inlineAlert).hasText(`Mount type "banana" not found in the current data.`);
});
});
diff --git a/ui/tests/integration/components/clients/page-header-test.js b/ui/tests/integration/components/clients/page-header-test.js
index eccce1544d..e658652ae9 100644
--- a/ui/tests/integration/components/clients/page-header-test.js
+++ b/ui/tests/integration/components/clients/page-header-test.js
@@ -163,16 +163,16 @@ module('Integration | Component | clients/page-header', function (hooks) {
});
test('it refreshes route after clicking "Refresh page" button', async function (assert) {
- const routeName = 'vault.cluster.clients.counts.overview';
+ const routeName = 'vault.cluster.clients.counts';
const router = this.owner.lookup('service:router');
- Sinon.stub(router, 'currentRoute').value({ name: routeName });
+ Sinon.stub(router, 'currentRoute').value({ parent: { name: routeName } });
const refreshStub = Sinon.stub(router, 'refresh');
this.activityTimestamp = timestamp.now().toISOString();
await this.renderComponent();
await click(GENERAL.button('Refresh page'));
const [transitionRoute] = refreshStub.lastCall.args;
assert.true(refreshStub.calledOnce, 'clicking "Refresh page" calls refresh()');
- assert.strictEqual(transitionRoute, routeName, 'it calls refresh() with route name');
+ assert.strictEqual(transitionRoute, routeName, 'it calls refresh() with parent route name');
});
module('download naming', function () {