UI: Update date-picker UX in Client Counts (#27796)

* Create date-range component with tests

* update selectors and callback behavior

* update cc tests & selectors

* cleanup

* RIP calendar-widget and date-dropdown -- you were good components

* reset on close

* Add changelog

* use parseApiTimestamp

* fix test

* cleanup

* make date-range typescript, update behavior

* add words

* minor styling

* fix test
This commit is contained in:
Chelsea Shaw 2024-07-17 11:48:44 -05:00 committed by GitHub
parent d444a32f8c
commit 050a90b2ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 454 additions and 838 deletions

3
changelog/27796.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:improvement
ui: simplify the date range editing experience in the client counts dashboard.
```

View file

@ -1,103 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { ARRAY_OF_MONTHS, parseAPITimestamp } from 'core/utils/date-formatters';
import { addYears, isSameYear, subYears } from 'date-fns';
import timestamp from 'core/utils/timestamp';
/**
* @module CalendarWidget
* CalendarWidget component is used in the client counts dashboard to select a month/year to query the /activity endpoint.
* The component returns an object with selected date info, example: { dateType: 'endDate', monthIdx: 0, monthName: 'January', year: 2022 }
*
* @example
* ```js
* <CalendarWidget @startTimestamp={{this.startTime}} @endTimestamp={{this.endTime}} @selectMonth={{this.handleSelection}} />
*
* @param {string} startTimestamp - ISO timestamp string of the calendar widget's start time, displays in dropdown trigger
* @param {string} endTimestamp - ISO timestamp string for the calendar widget's end time, displays in dropdown trigger
* @param {function} selectMonth - callback function from parent - fires when selecting a month or clicking "Current billing period"
* />
* ```
*/
export default class CalendarWidget extends Component {
currentDate = timestamp.now();
@tracked calendarDisplayDate = this.currentDate; // init to current date, updates when user clicks on calendar chevrons
@tracked showCalendar = false;
// both date getters return a date object
get startDate() {
return parseAPITimestamp(this.args.startTimestamp);
}
get endDate() {
return parseAPITimestamp(this.args.endTimestamp);
}
get displayYear() {
return this.calendarDisplayDate.getFullYear();
}
get disableFutureYear() {
return isSameYear(this.calendarDisplayDate, this.currentDate);
}
get disablePastYear() {
// calendar widget should only go as far back as the passed in start time
return isSameYear(this.calendarDisplayDate, this.startDate);
}
get widgetMonths() {
const startYear = this.startDate.getFullYear();
const startMonthIdx = this.startDate.getMonth();
return ARRAY_OF_MONTHS.map((month, index) => {
let readonly = false;
// if widget is showing same year as @startTimestamp year, disable if month is before start month
if (startYear === this.displayYear && index < startMonthIdx) {
readonly = true;
}
// if widget showing current year, disable if month is later than current month
if (this.displayYear === this.currentDate.getFullYear() && index > this.currentDate.getMonth()) {
readonly = true;
}
return {
index,
year: this.displayYear,
name: month,
readonly,
};
});
}
@action
addYear() {
this.calendarDisplayDate = addYears(this.calendarDisplayDate, 1);
}
@action
subYear() {
this.calendarDisplayDate = subYears(this.calendarDisplayDate, 1);
}
@action
toggleShowCalendar() {
this.showCalendar = !this.showCalendar;
this.calendarDisplayDate = this.endDate;
}
@action
handleDateShortcut(dropdown, { target }) {
this.args.selectMonth({ dateType: target.name }); // send clicked shortcut to parent callback
this.showCalendar = false;
dropdown.close();
}
@action
selectMonth(month, dropdown) {
const { index, year, name } = month;
this.toggleShowCalendar();
this.args.selectMonth({ monthIdx: index, monthName: name, year, dateType: 'endDate' });
dropdown.close();
}
}

View file

@ -0,0 +1,102 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<div ...attributes>
<Hds::Text::Display @tag="p" class="has-bottom-margin-xs">
Date range
</Hds::Text::Display>
<div class="is-flex-align-baseline">
{{#if (and @startTime @endTime)}}
<p class="is-size-6" data-test-date-range="start">{{this.formattedDate @startTime}}</p>
<p class="has-left-margin-xs"> — </p>
<p class="is-size-6 has-left-margin-xs" data-test-date-range="end">{{this.formattedDate @endTime}}</p>
<Hds::Button
class="has-left-margin-xs"
@text="Edit"
@color="tertiary"
@icon="edit"
@iconPosition="trailing"
data-test-date-range-edit
{{on "click" (fn (mut this.showEditModal) true)}}
/>
{{else}}
<Hds::Button
@text="Set date range"
@icon="edit"
{{on "click" (fn (mut this.showEditModal) true)}}
data-test-set-date-range
/>
{{/if}}
</div>
{{#if this.showEditModal}}
<Hds::Modal data-test-date-range-edit-modal @onClose={{this.onClose}} as |M|>
<M.Header>
Edit date range
</M.Header>
<M.Body>
<p class="has-bottom-margin-s">
The start date will be used as the client counting start time and all clients in that month will be considered new.
{{#if this.version.isEnterprise}}
We recommend setting this date as your license or billing start date to get the most accurate new and total
client count estimations. These dates are only for querying data in storage. Editing the date range does not
change any license or billing configurations.
{{/if}}
</p>
<div class="clients-date-range-display">
<div>
<Hds::Form::Label for="start-month">Start</Hds::Form::Label>
<input
class="hds-form-text-input"
type="month"
value={{this.startDate}}
max={{this.currentMonth}}
id="start-month"
name="start"
{{on "change" this.updateDate}}
data-test-date-edit="start"
/>
</div>
<div>
<Hds::Form::Label for="end-month">End</Hds::Form::Label>
<input
class="hds-form-text-input"
type="month"
value={{this.endDate}}
max={{this.currentMonth}}
id="end-month"
name="end"
{{on "change" this.updateDate}}
data-test-date-edit="end"
/>
</div>
<Hds::Button
@text="Reset"
@color="tertiary"
@icon="reload"
{{on "click" this.resetDates}}
data-test-date-edit="reset"
/>
</div>
{{#if this.validationError}}
<Hds::Form::Error
class="has-top-margin-xs"
data-test-date-range-validation
>{{this.validationError}}</Hds::Form::Error>
{{/if}}
{{#if this.useDefaultDates}}
<Hds::Alert @type="compact" @color="highlight" class="has-top-margin-xs" data-test-range-default-alert as |A|>
<A.Description>Dashboard will use the default date range from the API.</A.Description>
</Hds::Alert>
{{/if}}
</M.Body>
<M.Footer as |F|>
<Hds::Button data-test-save @text="Save" {{on "click" this.handleSave}} />
<Hds::Button data-test-cancel @text="Cancel" @color="secondary" {{on "click" F.close}} />
</M.Footer>
</Hds::Modal>
{{/if}}
</div>

View file

@ -0,0 +1,125 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { formatDateObject } from 'core/utils/client-count-utils';
import { parseAPITimestamp } from 'core/utils/date-formatters';
import timestamp from 'core/utils/timestamp';
import { format } from 'date-fns';
import type VersionService from 'vault/services/version';
import type { HTMLElementEvent } from 'forms';
interface OnChangeParams {
start_time: number | undefined;
end_time: number | undefined;
}
interface Args {
onChange: (callback: OnChangeParams) => void;
startTime: string;
endTime: string;
}
/**
* @module ClientsDateRange
* ClientsDateRange components are used to display the current date range and provide a modal interface for editing the date range.
*
* @example
*
* <Clients::DateRange @startTime="2018-01-01T14:15:30Z" @endTime="2019-01-31T14:15:30Z" @onChange={{this.handleDateChange}} />
*
* @param {function} onChange - callback when a new range is saved.
* @param {string} [startTime] - ISO string timestamp of the current start date
* @param {string} [endTime] - ISO string timestamp of the current end date
*/
export default class ClientsDateRangeComponent extends Component<Args> {
@service declare readonly version: VersionService;
@tracked showEditModal = false;
@tracked startDate = ''; // format yyyy-MM
@tracked endDate = ''; // format yyyy-MM
currentMonth = format(timestamp.now(), 'yyyy-MM');
constructor(owner: unknown, args: Args) {
super(owner, args);
this.setTrackedFromArgs();
}
setTrackedFromArgs() {
if (this.args.startTime) {
this.startDate = parseAPITimestamp(this.args.startTime, 'yyyy-MM') as string;
}
if (this.args.endTime) {
this.endDate = parseAPITimestamp(this.args.endTime, 'yyyy-MM') as string;
}
}
formattedDate = (isoTimestamp: string) => {
return parseAPITimestamp(isoTimestamp, 'MMMM yyyy');
};
get useDefaultDates() {
return !this.startDate && !this.endDate;
}
get validationError() {
if (this.useDefaultDates) {
// this means we want to reset, which is fine
return null;
}
if (!this.startDate || !this.endDate) {
return 'You must supply both start and end dates.';
}
if (this.startDate > this.endDate) {
return 'Start date must be before end date.';
}
return null;
}
@action onClose() {
// since the component never gets torn down, we have to manually re-set this on close
this.setTrackedFromArgs();
this.showEditModal = false;
}
@action resetDates() {
this.startDate = '';
this.endDate = '';
}
@action updateDate(evt: HTMLElementEvent<HTMLInputElement>) {
const { name, value } = evt.target;
if (name === 'end') {
this.endDate = value;
} else {
this.startDate = value;
}
}
@action handleSave() {
if (this.validationError) return;
const params: OnChangeParams = {
start_time: undefined,
end_time: undefined,
};
if (this.startDate) {
const [year, month] = this.startDate.split('-');
if (year && month) {
params.start_time = formatDateObject({ monthIdx: parseInt(month) - 1, year: parseInt(year) }, false);
}
}
if (this.endDate) {
const [year, month] = this.endDate.split('-');
if (year && month) {
params.end_time = formatDateObject({ monthIdx: parseInt(month) - 1, year: parseInt(year) }, true);
}
}
this.args.onChange(params);
this.onClose();
}
}

View file

@ -18,35 +18,12 @@
Date queries are sent in UTC.
</p>
<Hds::Text::Display @tag="p" class="has-bottom-margin-xs" data-test-counts-start-label>
{{this.versionText.label}}
</Hds::Text::Display>
<div class="is-flex-align-baseline">
{{#if this.formattedStartDate}}
<p class="is-size-6" data-test-counts-start-month>{{this.formattedStartDate}}</p>
<Hds::Button
class="has-left-margin-xs"
@text="Edit"
@color="tertiary"
@icon="edit"
@iconPosition="trailing"
data-test-counts-start-edit
{{on "click" (fn (mut this.showBillingStartModal) true)}}
/>
{{else}}
<DateDropdown
@handleSubmit={{this.onDateChange}}
@dateType="startDate"
@submitText="Save"
data-test-counts-start-dropdown
/>
{{/if}}
</div>
<Hds::Text::Body @tag="p" @color="faint" @size="300" class="has-bottom-margin-l" data-test-counts-description>
{{this.versionText.description}}
</Hds::Text::Body>
<Clients::DateRange
@startTime={{this.startTimestampISO}}
@endTime={{this.endTimestampISO}}
@onChange={{this.onDateChange}}
class="has-bottom-margin-l"
/>
{{#if (eq @activity.id "no-data")}}
<Clients::NoData @config={{@config}} @dateRangeMessage={{this.dateRangeMessage}} />
@ -76,11 +53,6 @@
</Hds::Text::Body>
<Toolbar aria-label="toolbar for filtering client count data" class="has-bottom-margin-m" data-test-clients-filter-bar>
<ToolbarFilters>
<CalendarWidget
@startTimestamp={{this.startTimestampISO}}
@endTimestamp={{this.endTimestampISO}}
@selectMonth={{this.onDateChange}}
/>
{{#if (or @namespace this.namespaces)}}
<SearchSelect
@id="namespace-search-select"
@ -172,22 +144,4 @@
</EmptyState>
{{/if}}
{{/if}}
</div>
{{#if this.showBillingStartModal}}
<Hds::Modal id="clients-edit-date-modal" @onClose={{fn (mut this.showBillingStartModal) false}} as |M|>
<M.Header>
Edit start month
</M.Header>
<M.Body>
<p class="has-bottom-margin-s">
{{this.versionText.description}}
</p>
<p><strong>{{this.versionText.label}}</strong></p>
<DateDropdown class="has-top-padding-s" @handleSubmit={{this.onDateChange}} @dateType="startDate" @submitText="Save" />
</M.Body>
<M.Footer as |F|>
<Hds::Button data-test-date-dropdown-cancel @text="Cancel" @color="secondary" {{on "click" F.close}} />
</M.Footer>
</Hds::Modal>
{{/if}}
</div>

View file

@ -6,10 +6,9 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { fromUnixTime, getUnixTime, isSameMonth, isAfter } from 'date-fns';
import { fromUnixTime, isSameMonth, isAfter } from 'date-fns';
import { parseAPITimestamp } from 'core/utils/date-formatters';
import { filterVersionHistory, formatDateObject } from 'core/utils/client-count-utils';
import timestamp from 'core/utils/timestamp';
import { filterVersionHistory } from 'core/utils/client-count-utils';
import type AdapterError from '@ember-data/adapter';
import type FlagsService from 'vault/services/flags';
@ -94,17 +93,11 @@ export default class ClientsCountsPageComponent extends Component<Args> {
get versionText() {
return this.version.isEnterprise
? {
label: 'Billing start month',
description:
'This date comes from your license, and defines when client counting starts. Without this starting point, the data shown is not reliable.',
title: 'No billing start date found',
message:
'In order to get the most from this data, please enter your billing period start month. This will ensure that the resulting data is accurate.',
}
: {
label: 'Client counting start date',
description:
'This date is when client counting starts. Without this starting point, the data shown is not reliable.',
title: 'No start date found',
message:
'In order to get the most from this data, please enter a start month above. Vault will calculate new clients starting from that month.',
@ -174,25 +167,8 @@ export default class ClientsCountsPageComponent extends Component<Args> {
}
@action
onDateChange(dateObject: { dateType: string; monthIdx: number; year: number }) {
const { dateType, monthIdx, year } = dateObject;
const { config } = this.args;
const currentTimestamp = getUnixTime(timestamp.now());
// converts the selectedDate to unix timestamp for activity query
const selectedDate = formatDateObject({ monthIdx, year }, dateType === 'endDate');
if (dateType !== 'cancel') {
const start_time = {
reset: getUnixTime(config?.billingStartTimestamp) || null, // clicked 'Current billing period' in calendar widget -> resets to billing start date
currentMonth: currentTimestamp, // clicked 'Current month' from calendar widget -> defaults to currentTimestamp
startDate: selectedDate, // from "Edit billing start" modal
}[dateType];
// endDate type is selection from calendar widget
const end_time = dateType === 'endDate' ? selectedDate : currentTimestamp; // defaults to currentTimestamp
const params = start_time !== undefined ? { start_time, end_time } : { end_time };
this.args.onFilterChange(params);
}
onDateChange(params: { start_time: number | undefined; end_time: number | undefined }) {
this.args.onFilterChange(params);
}
@action

View file

@ -1,78 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
import timestamp from 'core/utils/timestamp';
/**
* @module DateDropdown
* DateDropdown components are used to display a dropdown of months and years to handle date selection. Future dates are disabled (current month and year are selectable).
* The component returns an object with selected date info, example: { dateType: 'start', monthIdx: 0, monthName: 'January', year: 2022 }
*
* @example
* ```js
* <DateDropdown @handleSubmit={{this.actionFromParent}} @name="startTime" @submitText="Save" />
* ```
* @param {function} handleSubmit - callback function from parent that the date picker triggers on submit
* @param {string} [dateType] - optional argument to give the selected month/year a type
* @param {string} [submitText] - optional argument to change submit button text
* @param {function} [validateDate] - parent function to validate date selection, receives date object and returns an error message that's passed to the inline alert
*/
export default class DateDropdown extends Component {
currentDate = timestamp.now();
currentYear = this.currentDate.getFullYear(); // integer of year
currentMonthIdx = this.currentDate.getMonth(); // integer of month, 0 indexed
dropdownMonths = ARRAY_OF_MONTHS.map((m, i) => ({ name: m, index: i }));
dropdownYears = Array.from({ length: 5 }, (item, i) => this.currentYear - i);
@tracked maxMonthIdx = 11; // disables months with index greater than this number, initially all months are selectable
@tracked disabledYear = null; // year as integer if current year should be disabled
@tracked selectedMonth = null;
@tracked selectedYear = null;
@tracked invalidDate = null;
@action
selectMonth(month, dropdown) {
this.selectedMonth = month;
// disable current year if selected month is later than current month
this.disabledYear = month.index > this.currentMonthIdx ? this.currentYear : null;
dropdown.close();
}
@action
selectYear(year, dropdown) {
this.selectedYear = year;
// disable months after current month if selected year is current year
this.maxMonthIdx = year === this.currentYear ? this.currentMonthIdx : 11;
dropdown.close();
}
@action
handleSubmit() {
if (this.args.validateDate) {
this.invalidDate = null;
this.invalidDate = this.args.validateDate(new Date(this.selectedYear, this.selectedMonth.index));
if (this.invalidDate) return;
}
const { index, name } = this.selectedMonth;
this.args.handleSubmit({
monthIdx: index,
monthName: name,
year: this.selectedYear,
dateType: this.args.dateType,
});
this.resetDropdown();
}
resetDropdown() {
this.maxMonthIdx = 11;
this.disabledYear = null;
this.selectedMonth = null;
this.selectedYear = null;
this.invalidDate = null;
}
}

View file

@ -13,8 +13,6 @@ import type StoreService from 'vault/services/store';
import type VersionService from 'vault/services/version';
import type { ModelFrom } from 'vault/vault/route';
import type ClientsRoute from '../clients';
import type ClientsActivityModel from 'vault/models/clients/activity';
import type ClientsConfigModel from 'vault/models/clients/config';
import type ClientsCountsController from 'vault/controllers/vault/cluster/clients/counts';
import { setStartTimeQuery } from 'core/utils/client-count-utils';

View file

@ -0,0 +1,14 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
.clients-date-range-display {
display: flex;
align-items: bottom;
> * {
margin-right: $spacing-8;
vertical-align: bottom;
align-self: end;
}
}

View file

@ -59,6 +59,7 @@
@import './components/box-label';
@import './components/calendar-widget';
@import './components/chart-container';
@import './components/clients-date-range';
@import './components/cluster-banners';
@import './components/codemirror';
@import './components/console-ui-panel';

View file

@ -1,38 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<Hds::SegmentedGroup ...attributes as |S|>
<S.Dropdown @listPosition="bottom-left" @height="200px" as |dd|>
<dd.ToggleButton data-test-toggle-month @text={{or this.selectedMonth.name "Month"}} @color="secondary" />
{{#each this.dropdownMonths as |month|}}
<dd.Interactive
data-test-dropdown-month={{month.name}}
disabled={{if (gt month.index this.maxMonthIdx) true false}}
{{on "click" (fn this.selectMonth month dd)}}
@text={{month.name}}
/>
{{/each}}
</S.Dropdown>
<S.Dropdown data-test-year-list @listPosition="bottom-left" @height="200px" as |dd|>
<dd.ToggleButton data-test-toggle-year @text={{or this.selectedYear "Year"}} @color="secondary" />
{{#each this.dropdownYears as |year|}}
<dd.Interactive
data-test-dropdown-year={{year}}
disabled={{if (eq year this.disabledYear) true false}}
{{on "click" (fn this.selectYear year dd)}}
@text={{year}}
/>
{{/each}}
</S.Dropdown>
<S.Button
data-test-date-dropdown-submit
disabled={{if (and this.selectedMonth this.selectedYear) false true}}
{{on "click" this.handleSubmit}}
@text={{or @submitText "Submit"}}
/>
</Hds::SegmentedGroup>
{{#if this.invalidDate}}
<AlertInline @type="danger" @message={{this.invalidDate}} class="has-top-padding-s" />
{{/if}}

View file

@ -8,7 +8,7 @@ import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import clientsHandler, { STATIC_NOW } from 'vault/mirage/handlers/clients';
import sinon from 'sinon';
import { visit, click, currentURL } from '@ember/test-helpers';
import { visit, click, currentURL, fillIn } from '@ember/test-helpers';
import authPage from 'vault/tests/pages/auth';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
@ -42,14 +42,20 @@ module('Acceptance | clients | counts', function (hooks) {
test('it should persist filter query params between child routes', async function (assert) {
await visit('/vault/clients/counts/overview');
await click(CLIENT_COUNT.rangeDropdown);
await click(CLIENT_COUNT.currentBillingPeriod);
const timeQueryRegex = /end_time=\d+&start_time=\d+/g;
assert.ok(currentURL().match(timeQueryRegex).length, 'Start and end times added as query params');
await click(CLIENT_COUNT.dateRange.edit);
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2020-03');
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2022-02');
await click(GENERAL.saveButton);
assert.strictEqual(
currentURL(),
'/vault/clients/counts/overview?end_time=1706659200&start_time=1643673600',
'Start and end times added as query params'
);
await click(GENERAL.tab('token'));
assert.ok(
currentURL().match(timeQueryRegex).length,
assert.strictEqual(
currentURL(),
'/vault/clients/counts/token?end_time=1706659200&start_time=1643673600',
'Start and end times persist through child route change'
);

View file

@ -9,9 +9,8 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
import clientsHandler, { STATIC_NOW, LICENSE_START, UPGRADE_DATE } from 'vault/mirage/handlers/clients';
import syncHandler from 'vault/mirage/handlers/sync';
import sinon from 'sinon';
import { visit, click, findAll, settled } from '@ember/test-helpers';
import { visit, click, findAll, settled, fillIn } from '@ember/test-helpers';
import authPage from 'vault/tests/pages/auth';
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CHARTS, CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
import { create } from 'ember-cli-page-object';
@ -19,6 +18,7 @@ import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { formatNumber } from 'core/helpers/format-number';
import timestamp from 'core/utils/timestamp';
import ss from 'vault/tests/pages/components/search-select';
import { format } from 'date-fns';
const searchSelect = create(ss);
@ -36,11 +36,11 @@ module('Acceptance | clients | overview', function (hooks) {
test('it should render charts', async function (assert) {
assert
.dom(CLIENT_COUNT.counts.startMonth)
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
.hasText('July 2023', 'billing start month is correctly parsed from license');
assert
.dom(CLIENT_COUNT.rangeDropdown)
.hasText('Jul 2023 - Jan 2024', 'Date range shows dates correctly parsed activity response');
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
.hasText('January 2024', 'billing start month is correctly parsed from license');
assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area');
assert
.dom(CHARTS.container('Vault client counts'))
@ -53,12 +53,13 @@ module('Acceptance | clients | overview', function (hooks) {
test('it should update charts when querying date ranges', async function (assert) {
// query for single, historical month with no new counts (July 2023)
await click(CLIENT_COUNT.rangeDropdown);
await click(CLIENT_COUNT.calendarWidget.customEndMonth);
await click(CLIENT_COUNT.calendarWidget.previousYear);
const licenseStartMonth = format(LICENSE_START, 'yyyy-MM');
const upgradeMonth = format(UPGRADE_DATE, 'yyyy-MM');
await click(CLIENT_COUNT.dateRange.edit);
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), licenseStartMonth);
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), licenseStartMonth);
const month = ARRAY_OF_MONTHS[LICENSE_START.getMonth()];
await click(CLIENT_COUNT.calendarWidget.calendarMonth(month));
await click(GENERAL.saveButton);
assert
.dom(CLIENT_COUNT.usageStats('Vault client counts'))
.doesNotExist('running total single month stat boxes do not show');
@ -75,16 +76,15 @@ module('Acceptance | clients | overview', function (hooks) {
assert.dom(CHARTS.container('total-clients')).exists('total client attribution chart shows');
// reset to billing period
await click(CLIENT_COUNT.rangeDropdown);
await click(CLIENT_COUNT.currentBillingPeriod);
await click(CLIENT_COUNT.dateRange.edit);
await click(CLIENT_COUNT.dateRange.reset);
await click(GENERAL.saveButton);
// change to start on month/year of upgrade to 1.10
await click(CLIENT_COUNT.dateRange.edit);
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), upgradeMonth);
await click(GENERAL.saveButton);
// change billing start to month/year of upgrade to 1.10
await click(CLIENT_COUNT.counts.startEdit);
await click(CLIENT_COUNT.monthDropdown);
await click(CLIENT_COUNT.dateDropdown.selectMonth(ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()]));
await click(CLIENT_COUNT.yearDropdown);
await click(CLIENT_COUNT.dateDropdown.selectYear(UPGRADE_DATE.getFullYear()));
await click(CLIENT_COUNT.dateDropdown.submit);
assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area');
assert
.dom(CHARTS.container('Vault client counts'))
@ -95,11 +95,11 @@ module('Acceptance | clients | overview', function (hooks) {
assert.strictEqual(findAll(CHARTS.plotPoint).length, 5, 'line chart plots 5 points to match query');
// query for single, historical month (upgrade month)
await click(CLIENT_COUNT.rangeDropdown);
await click(CLIENT_COUNT.calendarWidget.customEndMonth);
assert.dom(CLIENT_COUNT.calendarWidget.displayYear).hasText('2024');
await click(CLIENT_COUNT.calendarWidget.previousYear);
await click(CLIENT_COUNT.calendarWidget.calendarMonth('September'));
await click(CLIENT_COUNT.dateRange.edit);
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), upgradeMonth);
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), upgradeMonth);
await click(GENERAL.saveButton);
assert
.dom(CLIENT_COUNT.usageStats('Vault client counts'))
.exists('running total single month usage stats show');
@ -111,9 +111,10 @@ module('Acceptance | clients | overview', function (hooks) {
assert.dom(CHARTS.container('total-clients')).exists('total client attribution chart shows');
// query historical date range (from September 2023 to December 2023)
await click(CLIENT_COUNT.rangeDropdown);
await click(CLIENT_COUNT.calendarWidget.customEndMonth);
await click(CLIENT_COUNT.calendarWidget.calendarMonth('December'));
await click(CLIENT_COUNT.dateRange.edit);
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2023-09');
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), '2023-12');
await click(GENERAL.saveButton);
assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area');
assert
@ -126,15 +127,15 @@ module('Acceptance | clients | overview', function (hooks) {
.hasText('12/23', 'x-axis labels end with queried end month');
// reset to billing period
await click(CLIENT_COUNT.rangeDropdown);
await click(CLIENT_COUNT.currentBillingPeriod);
await click(CLIENT_COUNT.dateRange.edit);
await click(CLIENT_COUNT.dateRange.reset);
await click(GENERAL.saveButton);
// query month older than count start date
await click(CLIENT_COUNT.counts.startEdit);
await click(CLIENT_COUNT.monthDropdown);
await click(CLIENT_COUNT.dateDropdown.selectMonth(ARRAY_OF_MONTHS[LICENSE_START.getMonth()]));
await click(CLIENT_COUNT.yearDropdown);
await click(CLIENT_COUNT.dateDropdown.selectYear(LICENSE_START.getFullYear() - 3));
await click(CLIENT_COUNT.dateDropdown.submit);
await click(CLIENT_COUNT.dateRange.edit);
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2020-07');
await click(GENERAL.saveButton);
assert
.dom(CLIENT_COUNT.counts.startDiscrepancy)
.hasTextContaining(

View file

@ -3,18 +3,8 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { click, findAll } from '@ember/test-helpers';
import { CLIENT_COUNT, CHARTS } from './client-count-selectors';
export async function dateDropdownSelect(month, year) {
const { dateDropdown, counts } = CLIENT_COUNT;
await click(counts.startEdit);
await click(dateDropdown.toggleMonth);
await click(dateDropdown.selectMonth(month));
await click(dateDropdown.toggleYear);
await click(dateDropdown.selectYear(year));
await click(dateDropdown.submit);
}
import { findAll } from '@ember/test-helpers';
import { CHARTS } from './client-count-selectors';
export function assertBarChart(assert, chartName, byMonthData, isStacked = false) {
// assertion count is byMonthData.length, plus 2

View file

@ -6,44 +6,28 @@
// TODO: separate nested into distinct exported consts
export const CLIENT_COUNT = {
counts: {
startLabel: '[data-test-counts-start-label]',
description: '[data-test-counts-description]',
startMonth: '[data-test-counts-start-month]',
startEdit: '[data-test-counts-start-edit]',
startDropdown: '[data-test-counts-start-dropdown]',
configDisabled: '[data-test-counts-disabled]',
namespaces: '[data-test-counts-namespaces]',
mountPaths: '[data-test-counts-auth-mounts]',
startDiscrepancy: '[data-test-counts-start-discrepancy]',
},
dateRange: {
dateDisplay: (name: string) => (name ? `[data-test-date-range="${name}"]` : '[data-test-date-range]'),
set: '[data-test-set-date-range]',
edit: '[data-test-date-range-edit]',
editModal: '[data-test-date-range-edit-modal]',
editDate: (name: string) => `[data-test-date-edit="${name}"]`,
reset: '[data-test-date-edit="reset"]',
defaultRangeAlert: '[data-test-range-default-alert]',
validation: '[data-test-date-range-validation]',
},
statText: (label: string) => `[data-test-stat-text="${label}"]`,
statTextValue: (label: string) =>
label ? `[data-test-stat-text="${label}"] .stat-value` : '[data-test-stat-text]',
usageStats: (title: string) => `[data-test-usage-stats="${title}"]`,
dateDisplay: '[data-test-date-display]',
attributionBlock: '[data-test-clients-attribution]',
filterBar: '[data-test-clients-filter-bar]',
rangeDropdown: '[data-test-calendar-widget-trigger]',
monthDropdown: '[data-test-toggle-month]',
yearDropdown: '[data-test-toggle-year]',
currentBillingPeriod: '[data-test-current-billing-period]',
dateDropdown: {
toggleMonth: '[data-test-toggle-month]',
toggleYear: '[data-test-toggle-year]',
selectMonth: (month: string) => `[data-test-dropdown-month="${month}"]`,
selectYear: (year: string) => `[data-test-dropdown-year="${year}"]`,
submit: '[data-test-date-dropdown-submit]',
},
calendarWidget: {
trigger: '[data-test-calendar-widget-trigger]',
currentMonth: '[data-test-current-month]',
currentBillingPeriod: '[data-test-current-billing-period]',
customEndMonth: '[data-test-show-calendar]',
previousYear: '[data-test-previous-year]',
nextYear: '[data-test-next-year]',
displayYear: '[data-test-display-year]',
calendarMonth: (month: string) => `[data-test-calendar-month="${month}"]`,
},
selectedAuthMount: 'div#mounts-search-select [data-test-selected-option] div',
selectedNs: 'div#namespace-search-select [data-test-selected-option] div',
upgradeWarning: '[data-test-clients-upgrade-warning]',

View file

@ -1,240 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import sinon from 'sinon';
import hbs from 'htmlbars-inline-precompile';
import calendarDropdown from 'vault/tests/pages/components/calendar-widget';
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
import { subMonths, subYears } from 'date-fns';
import timestamp from 'core/utils/timestamp';
module('Integration | Component | calendar-widget', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
sinon.replace(timestamp, 'now', sinon.fake.returns(new Date('2018-04-03T14:15:30')));
const CURRENT_DATE = timestamp.now();
this.set('currentDate', CURRENT_DATE);
this.set('calendarStartDate', subMonths(CURRENT_DATE, 12));
this.set('calendarEndDate', CURRENT_DATE);
this.set('startTimestamp', subMonths(CURRENT_DATE, 12).toISOString());
this.set('endTimestamp', CURRENT_DATE.toISOString());
this.set('handleClientActivityQuery', sinon.spy());
});
test('it renders and disables correct months when start date is 12 months ago', async function (assert) {
assert.expect(14);
await render(hbs`
<CalendarWidget
@startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}}
@selectMonth={{this.handleClientActivityQuery}}
/>
`);
assert
.dom('[data-test-calendar-widget-trigger]')
.hasText(`Apr 2017 - Apr 2018`, 'renders and formats start and end dates');
await calendarDropdown.openCalendar();
assert.ok(calendarDropdown.showsCalendar, 'renders the calendar component');
// assert months in current year are disabled/enabled correctly
const enabledMonths = ['January', 'February', 'March', 'April'];
ARRAY_OF_MONTHS.forEach(function (month) {
if (enabledMonths.includes(month)) {
assert.dom(`[data-test-calendar-month="${month}"]`).isNotDisabled(`${month} is enabled`);
} else {
assert.dom(`[data-test-calendar-month="${month}"]`).isDisabled(`${month} is disabled`);
}
});
});
test('it renders and disables months before start timestamp', async function (assert) {
await render(hbs`
<CalendarWidget
@startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}}
@selectMonth={{this.handleClientActivityQuery}}
/>
`);
await calendarDropdown.openCalendar();
assert.dom('[data-test-next-year]').isDisabled('Future year is disabled');
await calendarDropdown.clickPreviousYear();
assert
.dom('[data-test-display-year]')
.hasText(`${subYears(this.currentDate, 1).getFullYear()}`, 'shows the previous year');
assert.dom('[data-test-previous-year]').isDisabled('disables previous year');
// assert months in previous year are disabled/enabled correctly
const disabledMonths = ['January', 'February', 'March'];
ARRAY_OF_MONTHS.forEach(function (month) {
if (disabledMonths.includes(month)) {
assert.dom(`[data-test-calendar-month="${month}"]`).isDisabled(`${month} is disabled`);
} else {
assert.dom(`[data-test-calendar-month="${month}"]`).isNotDisabled(`${month} is enabled`);
}
});
});
test('it calls parent callback with correct arg when clicking "Current billing period"', async function (assert) {
await render(hbs`
<CalendarWidget
@startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}}
@selectMonth={{this.handleClientActivityQuery}}
/>
`);
await calendarDropdown.menuToggle();
await calendarDropdown.clickCurrentBillingPeriod();
assert.propEqual(
this.handleClientActivityQuery.args[0][0],
{ dateType: 'reset' },
'it calls parent function with reset dateType'
);
});
test('it calls parent callback with correct arg when clicking "Current month"', async function (assert) {
await render(hbs`
<CalendarWidget
@startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}}
@selectMonth={{this.handleClientActivityQuery}}
/>
`);
await calendarDropdown.menuToggle();
await calendarDropdown.clickCurrentMonth();
assert.propEqual(
this.handleClientActivityQuery.args[0][0],
{ dateType: 'currentMonth' },
'it calls parent function with currentMoth dateType'
);
});
test('it calls parent callback with correct arg when selecting a month', async function (assert) {
await render(hbs`
<CalendarWidget
@startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}}
@selectMonth={{this.handleClientActivityQuery}}
/>
`);
await calendarDropdown.openCalendar();
await click(`[data-test-calendar-month="April"`);
assert.propEqual(
this.handleClientActivityQuery.lastCall.lastArg,
{
dateType: 'endDate',
monthIdx: 3,
monthName: 'April',
year: 2018,
},
'it calls parent function with end date (current) month/year'
);
await calendarDropdown.openCalendar();
await calendarDropdown.clickPreviousYear();
await click(`[data-test-calendar-month="May"]`);
assert.propEqual(
this.handleClientActivityQuery.lastCall.lastArg,
{
dateType: 'endDate',
monthIdx: 4,
monthName: 'May',
year: 2017,
},
'it calls parent function with selected start date month/year'
);
});
test('it disables correct months when start date 6 months ago', async function (assert) {
this.set('calendarStartDate', subMonths(this.currentDate, 6)); // Nov 3, 2017
this.set('startTimestamp', subMonths(this.currentDate, 6).toISOString());
await render(hbs`
<CalendarWidget
@startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}}
@selectMonth={{this.handleClientActivityQuery}}
/>
`);
await calendarDropdown.openCalendar();
assert.dom('[data-test-next-year]').isDisabled('Future year is disabled');
// Check start year disables correct months
await calendarDropdown.clickPreviousYear();
assert.dom('[data-test-previous-year]').isDisabled('previous year is disabled');
const prevYearEnabled = ['October', 'November', 'December'];
ARRAY_OF_MONTHS.forEach(function (month) {
if (prevYearEnabled.includes(month)) {
assert.dom(`[data-test-calendar-month="${month}"]`).isNotDisabled(`${month} is enabled`);
} else {
assert.dom(`[data-test-calendar-month="${month}"]`).isDisabled(`${month} is read only`);
}
});
// Check end year disables correct months
await click('[data-test-next-year]');
const currYearEnabled = ['January', 'February', 'March', 'April'];
ARRAY_OF_MONTHS.forEach(function (month) {
if (currYearEnabled.includes(month)) {
assert.dom(`[data-test-calendar-month="${month}"]`).isNotDisabled(`${month} is enabled`);
} else {
assert.dom(`[data-test-calendar-month="${month}"]`).isDisabled(`${month} is disabled`);
}
});
});
test('it disables correct months when start date 36 months ago', async function (assert) {
this.set('calendarStartDate', subMonths(this.currentDate, 36)); // April 3 2015
this.set('startTimestamp', subMonths(this.currentDate, 36).toISOString());
await render(hbs`
<CalendarWidget
@startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}}
@selectMonth={{this.handleClientActivityQuery}}
/>
`);
await calendarDropdown.openCalendar();
assert.dom('[data-test-next-year]').isDisabled('Future year is disabled');
for (const year of [2017, 2016, 2015]) {
await calendarDropdown.clickPreviousYear();
assert.dom('[data-test-display-year]').hasText(year.toString());
}
assert.dom('[data-test-previous-year]').isDisabled('previous year is disabled');
assert.dom('[data-test-next-year]').isEnabled('next year is enabled');
const disabledMonths = ['January', 'February', 'March'];
ARRAY_OF_MONTHS.forEach(function (month) {
if (disabledMonths.includes(month)) {
assert.dom(`[data-test-calendar-month="${month}"]`).isDisabled(`${month} is disabled`);
} else {
assert.dom(`[data-test-calendar-month="${month}"]`).isNotDisabled(`${month} is enabled`);
}
});
await click('[data-test-next-year]');
ARRAY_OF_MONTHS.forEach(function (month) {
assert.dom(`[data-test-calendar-month="${month}"]`).isNotDisabled(`${month} is enabled for 2016`);
});
await click('[data-test-next-year]');
ARRAY_OF_MONTHS.forEach(function (month) {
assert.dom(`[data-test-calendar-month="${month}"]`).isNotDisabled(`${month} is enabled for 2017`);
});
await click('[data-test-next-year]');
const enabledMonths = ['January', 'February', 'March', 'April'];
ARRAY_OF_MONTHS.forEach(function (month) {
if (enabledMonths.includes(month)) {
assert.dom(`[data-test-calendar-month="${month}"]`).isNotDisabled(`${month} is enabled`);
} else {
assert.dom(`[data-test-calendar-month="${month}"]`).isDisabled(`${month} is disabled`);
}
});
});
});

View file

@ -0,0 +1,106 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { click, fillIn, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import Sinon from 'sinon';
import timestamp from 'core/utils/timestamp';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
const DATE_RANGE = CLIENT_COUNT.dateRange;
module('Integration | Component | clients/date-range', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
Sinon.replace(timestamp, 'now', Sinon.fake.returns(new Date('2018-04-03T14:15:30')));
this.now = timestamp.now();
this.startTime = '2018-01-01T14:15:30';
this.endTime = '2019-01-31T14:15:30';
this.onChange = Sinon.spy();
this.renderComponent = async () => {
await render(
hbs`<Clients::DateRange @startTime={{this.startTime}} @endTime={{this.endTime}} @onChange={{this.onChange}} />`
);
};
});
test('it renders prompt to set dates if no start time', async function (assert) {
this.startTime = undefined;
await this.renderComponent();
assert.dom(DATE_RANGE.set).exists();
await click(DATE_RANGE.set);
assert.dom(DATE_RANGE.editModal).exists();
assert.dom(DATE_RANGE.editDate('start')).hasValue('');
await fillIn(DATE_RANGE.editDate('start'), '2018-01');
await fillIn(DATE_RANGE.editDate('end'), '2019-01');
await click(GENERAL.saveButton);
assert.deepEqual(this.onChange.args[0], [
{
end_time: 1548892800,
start_time: 1514764800,
},
]);
assert.dom(DATE_RANGE.editModal).doesNotExist('closes modal');
});
test('it renders the date range passed and can reset it', async function (assert) {
await this.renderComponent();
assert.dom(DATE_RANGE.dateDisplay('start')).hasText('January 2018');
assert.dom(DATE_RANGE.dateDisplay('end')).hasText('January 2019');
await click(DATE_RANGE.edit);
assert.dom(DATE_RANGE.editModal).exists();
assert.dom(DATE_RANGE.editDate('start')).hasValue('2018-01');
assert.dom(DATE_RANGE.editDate('end')).hasValue('2019-01');
assert.dom(DATE_RANGE.defaultRangeAlert).doesNotExist();
await click(DATE_RANGE.editDate('reset'));
assert.dom(DATE_RANGE.editDate('start')).hasValue('');
assert.dom(DATE_RANGE.editDate('end')).hasValue('');
assert.dom(DATE_RANGE.defaultRangeAlert).exists();
await click(GENERAL.saveButton);
assert.deepEqual(this.onChange.args[0], [{ start_time: undefined, end_time: undefined }]);
});
test('it does not trigger onChange if date range invalid', async function (assert) {
await this.renderComponent();
await click(DATE_RANGE.edit);
await click(DATE_RANGE.editDate('reset'));
await fillIn(DATE_RANGE.editDate('end'), '2017-05');
assert.dom(DATE_RANGE.validation).hasText('You must supply both start and end dates.');
await click(GENERAL.saveButton);
assert.false(this.onChange.called);
await fillIn(DATE_RANGE.editDate('start'), '2018-01');
assert.dom(DATE_RANGE.validation).hasText('Start date must be before end date.');
await click(GENERAL.saveButton);
assert.false(this.onChange.called);
await click(GENERAL.cancelButton);
assert.false(this.onChange.called);
assert.dom(DATE_RANGE.editModal).doesNotExist();
});
test('it resets the tracked values on close', async function (assert) {
await this.renderComponent();
await click(DATE_RANGE.edit);
await click(DATE_RANGE.editDate('reset'));
assert.dom(DATE_RANGE.editDate('start')).hasValue('');
assert.dom(DATE_RANGE.editDate('end')).hasValue('');
await click(GENERAL.cancelButton);
await click(DATE_RANGE.edit);
assert.dom(DATE_RANGE.editDate('start')).hasValue('2018-01');
assert.dom(DATE_RANGE.editDate('end')).hasValue('2019-01');
});
});

View file

@ -6,13 +6,12 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { render, click, settled, findAll } from '@ember/test-helpers';
import { render, click, findAll, fillIn } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients';
import { getUnixTime } from 'date-fns';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
import { dateDropdownSelect } from 'vault/tests/helpers/clients/client-count-helpers';
import { selectChoose } from 'ember-power-select/test-support';
import timestamp from 'core/utils/timestamp';
import sinon from 'sinon';
@ -57,38 +56,11 @@ module('Integration | Component | clients | Page::Counts', function (hooks) {
`);
});
test('it should render start date label and description based on version', async function (assert) {
const versionService = this.owner.lookup('service:version');
await this.renderComponent();
assert.dom(CLIENT_COUNT.counts.startLabel).hasText('Client counting start date', 'Label renders for OSS');
assert
.dom(CLIENT_COUNT.counts.description)
.hasText(
'This date is when client counting starts. Without this starting point, the data shown is not reliable.',
'Description renders for OSS'
);
versionService.set('type', 'enterprise');
await settled();
assert.dom(CLIENT_COUNT.counts.startLabel).hasText('Billing start month', 'Label renders for Enterprise');
assert
.dom(CLIENT_COUNT.counts.description)
.hasText(
'This date comes from your license, and defines when client counting starts. Without this starting point, the data shown is not reliable.',
'Description renders for Enterprise'
);
});
test('it should populate start and end month displays', async function (assert) {
await this.renderComponent();
assert.dom(CLIENT_COUNT.counts.startMonth).hasText('July 2023', 'Start month renders');
assert
.dom(CLIENT_COUNT.calendarWidget.trigger)
.hasText('Jul 2023 - Jan 2024', 'Start and end months render in filter bar');
assert.dom(CLIENT_COUNT.dateRange.dateDisplay('start')).hasText('July 2023', 'Start month renders');
assert.dom(CLIENT_COUNT.dateRange.dateDisplay('end')).hasText('January 2024', 'End month renders');
});
test('it should render no data empty state', async function (assert) {
@ -123,31 +95,41 @@ module('Integration | Component | clients | Page::Counts', function (hooks) {
});
test('it should send correct values on start and end date change', async function (assert) {
assert.expect(4);
assert.expect(3);
const jan23start = getUnixTime(new Date('2023-01-01T00:00:00Z'));
const dec23end = getUnixTime(new Date('2023-12-31T00:00:00Z'));
const jan24end = getUnixTime(new Date('2024-01-31T00:00:00Z'));
let expected = { start_time: getUnixTime(new Date('2023-01-01T00:00:00Z')), end_time: END_TIME };
const expected = { start_time: START_TIME, end_time: END_TIME };
this.onFilterChange = (params) => {
assert.deepEqual(params, expected, 'Correct values sent on filter change');
this.startTimestamp = params.start_time || START_TIME;
this.endTimestamp = params.end_time || END_TIME;
this.set('startTimestamp', params.start_time || START_TIME);
this.set('endTimestamp', params.end_time || END_TIME);
};
// page starts with default billing dates, which are july 23 - jan 24
await this.renderComponent();
await dateDropdownSelect('January', '2023');
expected.start_time = END_TIME;
await click(CLIENT_COUNT.calendarWidget.trigger);
await click(CLIENT_COUNT.calendarWidget.currentMonth);
// First, change only the start date
expected.start_time = jan23start;
// the end date which is first set to STATIC_NOW gets recalculated
// to the end of given month/year on date range change
expected.end_time = jan24end;
await click(CLIENT_COUNT.dateRange.edit);
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2023-01');
await click(GENERAL.saveButton);
expected.start_time = getUnixTime(this.config.billingStartTimestamp);
await click(CLIENT_COUNT.calendarWidget.trigger);
await click(CLIENT_COUNT.calendarWidget.currentBillingPeriod);
// Then change only the end date
expected.end_time = dec23end;
await click(CLIENT_COUNT.dateRange.edit);
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), '2023-12');
await click(GENERAL.saveButton);
expected = { end_time: getUnixTime(new Date('2023-12-31T00:00:00Z')) };
await click(CLIENT_COUNT.calendarWidget.trigger);
await click(CLIENT_COUNT.calendarWidget.customEndMonth);
await click(CLIENT_COUNT.calendarWidget.previousYear);
await click(CLIENT_COUNT.calendarWidget.calendarMonth('December'));
// Then reset to billing which should reset the params
expected.start_time = undefined;
expected.end_time = undefined;
await click(CLIENT_COUNT.dateRange.edit);
await click(CLIENT_COUNT.dateRange.reset);
await click(GENERAL.saveButton);
});
test('it should render namespace and auth mount filters', async function (assert) {
@ -254,9 +236,7 @@ module('Integration | Component | clients | Page::Counts', function (hooks) {
await this.renderComponent();
assert.dom(GENERAL.emptyStateTitle).hasText('No start date found', 'Empty state renders');
assert
.dom(CLIENT_COUNT.counts.startDropdown)
.exists('Date dropdown renders when start time is not provided');
assert.dom(CLIENT_COUNT.dateRange.set).exists();
});
test('it should render catch all empty state', async function (assert) {

View file

@ -1,165 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import sinon from 'sinon';
import { setupRenderingTest } from 'ember-qunit';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
import timestamp from 'core/utils/timestamp';
const SELECTORS = {
monthDropdown: '[data-test-toggle-month]',
specificMonth: (m) => `[data-test-dropdown-month="${m}"]`,
yearDropdown: '[data-test-toggle-year]',
specificYear: (y) => `[data-test-dropdown-year="${y}"]`,
submitButton: '[data-test-date-dropdown-submit]',
monthOptions: '[data-test-dropdown-month]',
};
module('Integration | Component | date-dropdown', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
sinon.replace(timestamp, 'now', sinon.fake.returns(new Date('2018-04-03T14:15:30')));
});
test('it renders dropdown', async function (assert) {
await render(hbs`
<div class="has-padding-l">
<DateDropdown/>
</div>
`);
assert.dom(SELECTORS.submitButton).hasText('Submit', 'button renders default text');
});
test('it renders dropdown and selects month and year', async function (assert) {
assert.expect(26);
const parentAction = (args) => {
assert.propEqual(
args,
{
dateType: 'start',
monthIdx: 1,
monthName: 'February',
year: 2016,
},
'sends correct args to parent'
);
};
this.set('parentAction', parentAction);
await render(hbs`
<div class="has-padding-l">
<DateDropdown
@handleSubmit={{this.parentAction}}
@dateType="start"
/>
</div>
`);
assert.dom(SELECTORS.submitButton).isDisabled('button is disabled when no month or year selected');
await click(SELECTORS.monthDropdown);
assert.dom(SELECTORS.monthOptions).exists({ count: 12 }, 'dropdown has 12 months');
ARRAY_OF_MONTHS.forEach((month) => {
assert.dom(SELECTORS.specificMonth(month)).hasText(`${month}`, `dropdown includes ${month}`);
});
await click(SELECTORS.specificMonth('February'));
assert.dom(SELECTORS.monthDropdown).hasText('February', 'dropdown shows selected month');
assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting month');
await click(SELECTORS.yearDropdown);
assert.dom('[data-test-dropdown-year]').exists({ count: 5 }, 'dropdown has 5 years');
for (const year of [2018, 2017, 2016, 2015, 2014]) {
assert.dom(SELECTORS.specificYear(year)).exists();
}
await click('[data-test-dropdown-year="2016"]');
assert.dom(SELECTORS.yearDropdown).hasText(`2016`, `dropdown shows selected year`);
assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting year');
assert.dom(SELECTORS.submitButton).isNotDisabled('button enabled when month and year selected');
await click(SELECTORS.submitButton);
});
test('selecting month first: current year enabled when current month selected', async function (assert) {
assert.expect(5);
await render(hbs`
<div class="has-padding-l">
<DateDropdown/>
</div>
`);
// select current month
await click(SELECTORS.monthDropdown);
await click(SELECTORS.specificMonth('January'));
await click(SELECTORS.yearDropdown);
// all years should be selectable
for (const year of [2018, 2017, 2016, 2015, 2014]) {
assert.dom(SELECTORS.specificYear(year)).isNotDisabled(`year ${year} is selectable`);
}
});
test('selecting month first: it disables current year when future months selected', async function (assert) {
assert.expect(5);
await render(hbs`
<div class="has-padding-l">
<DateDropdown/>
</div>
`);
// select future month
await click(SELECTORS.monthDropdown);
await click(SELECTORS.specificMonth('June'));
await click(SELECTORS.yearDropdown);
assert.dom(SELECTORS.specificYear(2018)).isDisabled(`current year is disabled`);
// previous years should be selectable
for (const year of [2017, 2016, 2015, 2014]) {
assert.dom(SELECTORS.specificYear(year)).isNotDisabled(`year ${year} is selectable`);
}
});
test('selecting year first: it disables future months when current year selected', async function (assert) {
assert.expect(12);
await render(hbs`
<div class="has-padding-l">
<DateDropdown/>
</div>
`);
await click(SELECTORS.yearDropdown);
await click(SELECTORS.specificYear(2018));
await click(SELECTORS.monthDropdown);
const expectedSelectable = ['January', 'February', 'March', 'April'];
ARRAY_OF_MONTHS.forEach((month) => {
if (expectedSelectable.includes(month)) {
assert.dom(SELECTORS.specificMonth(month)).isNotDisabled(`${month} is selectable for current year`);
} else {
assert.dom(SELECTORS.specificMonth(month)).isDisabled(`${month} is disabled for current year`);
}
});
});
test('selecting year first: it enables all months when past year is selected', async function (assert) {
assert.expect(12);
await render(hbs`
<div class="has-padding-l">
<DateDropdown/>
</div>
`);
await click(SELECTORS.yearDropdown);
await click(SELECTORS.specificYear(2017));
await click(SELECTORS.monthDropdown);
ARRAY_OF_MONTHS.forEach((month) => {
assert.dom(SELECTORS.specificMonth(month)).isNotDisabled(`${month} is selectable for previous year`);
});
});
});