UI: Allow fallback date querying for client usage when no billing timestamp is provided (#10728) (#10755)

* some change

* add console logs to debug rendering error

* add fallback in case billingStartTime does not exist

* remove console logs

* allow querying dates if there is no billing start timestamp

* add comments and test coverage

* add changelog

* remove extra divs, small copy changes

* remove extra line

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
Vault Automation 2025-11-12 17:11:24 -05:00 committed by GitHub
parent 6e60eb3ff6
commit 4ab8b902f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 132 additions and 85 deletions

3
changelog/_10728.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui/activity (enterprise): Allow manual querying of client usage if there is a problem retrieving the license start time.
```

View File

@ -5,7 +5,8 @@
<div ...attributes>
<div class="is-flex-column align-items-end">
{{#if this.version.isEnterprise}}
{{! Enterprise should always have a @billingStartTime but as a fallback allow the user to query dates manually }}
{{#if (and @billingStartTime this.version.isEnterprise)}}
<Hds::Text::Display @tag="p" @size="100" class="has-bottom-margin-xs">
{{if this.flags.isHvdManaged "Change data period" "Change billing period"}}
</Hds::Text::Display>
@ -34,17 +35,15 @@
{{/each}}
{{/if}}
</Hds::Dropdown>
{{else}}
{{! Hide if the user has made an initial query because dates can be updated by clicking "Edit" beside the date range }}
{{#unless (and @startTimestamp @endTimestamp)}}
<Hds::Button
class="has-left-margin-xs"
@text="Set date range"
@icon="edit"
{{on "click" (fn @setEditModalVisible true)}}
data-test-date-range-edit
/>
{{/unless}}
{{else if (not (and @startTimestamp @endTimestamp))}}
{{! Hide if the user has already made an initial query because dates can be updated by clicking "Edit" beside the date range }}
<Hds::Button
class="has-left-margin-xs"
@text="Set date range"
@icon="edit"
{{on "click" (fn @setEditModalVisible true)}}
data-test-date-range-edit
/>
{{/if}}
</div>
@ -54,62 +53,61 @@
Edit date range
</M.Header>
<M.Body>
<Hds::Text::Body @tag="div">
{{#if this.version.isCommunity}}
<p class="has-bottom-margin-s">
Use custom date ranges to query historic client count data. Query results do not include the current month.
</p>
<ul class="has-bottom-margin-s">
<li>
<strong>Start</strong>
sets the month and year (inclusive) for the first month of client counting.
</li>
<li>
<strong>End</strong>
sets the month and year (inclusive) for the final month of client counting.
</li>
</ul>
<p class="has-bottom-margin-s">
We recommend setting
{{/if}}
<ul class="has-bottom-margin-s">
<li>
<strong>Start</strong>
to your Vault deploy date to get the most accurate new and total client count estimations.</p>
<div class="clients-date-range-display">
<div>
<Hds::Form::TextInput::Field
@type="month"
@value={{this.modalStart}}
max={{this.previousMonth}}
id="start-month"
name="modalStart"
{{on "change" this.updateDate}}
data-test-date-edit="start"
as |F|
>
<F.Label>Start</F.Label>
</Hds::Form::TextInput::Field>
</div>
<div>
<Hds::Form::TextInput::Field
@type="month"
@value={{this.modalEnd}}
max={{this.previousMonth}}
id="end-month"
name="modalEnd"
{{on "change" this.updateDate}}
data-test-date-edit="end"
as |F|
>
<F.Label>End</F.Label>
</Hds::Form::TextInput::Field>
</div>
sets the month and year (inclusive) for the first month of client counting.
</li>
<li>
<strong>End</strong>
sets the month and year (inclusive) for the final month of client counting.
</li>
</ul>
<p class="has-bottom-margin-s">
We recommend setting
<strong>Start</strong>
to your Vault deploy date to get the most accurate client counts.</p>
<div class="clients-date-range-display">
<div>
<Hds::Form::TextInput::Field
@type="month"
@value={{this.modalStart}}
max={{if this.version.isCommunity this.previousMonth (date-format this.currentMonth "yyyy-MM")}}
id="start-month"
name="modalStart"
{{on "change" this.updateDate}}
data-test-date-edit="start"
as |F|
>
<F.Label>Start</F.Label>
</Hds::Form::TextInput::Field>
</div>
{{#if this.validationError}}
<Hds::Form::Error
class="has-top-margin-xs"
data-test-date-range-validation
>{{this.validationError}}</Hds::Form::Error>
{{/if}}
</Hds::Text::Body>
<div>
<Hds::Form::TextInput::Field
@type="month"
@value={{this.modalEnd}}
max={{if this.version.isCommunity this.previousMonth (date-format this.currentMonth "yyyy-MM")}}
id="end-month"
name="modalEnd"
{{on "change" this.updateDate}}
data-test-date-edit="end"
as |F|
>
<F.Label>End</F.Label>
</Hds::Form::TextInput::Field>
</div>
</div>
{{#if this.validationError}}
<Hds::Form::Error
class="has-top-margin-xs"
data-test-date-range-validation
>{{this.validationError}}</Hds::Form::Error>
{{/if}}
</M.Body>
<M.Footer as |F|>
<Hds::Button @text="Save" {{on "click" this.handleSave}} data-test-submit />

View File

@ -86,7 +86,8 @@ export default class ClientsDateRangeComponent extends Component<Args> {
if (this.modalStart > this.modalEnd) {
return 'Start date must be before end date.';
}
if (this.modalStart > this.previousMonth || this.modalEnd > this.previousMonth) {
const isCurrentMonth = this.modalStart > this.previousMonth || this.modalEnd > this.previousMonth;
if (this.version.isCommunity && isCurrentMonth) {
return 'You cannot select the current month or beyond.';
}
return null;

View File

@ -23,22 +23,21 @@
/>
</PH.Subtitle>
{{/if}}
{{#if this.version.isEnterprise}}
<PH.Description class="has-text-weight-semibold flex">
<p>
{{if this.flags.isHvdManaged "For data period:" "For billing period:"}}
<PH.Description>
{{#if (and this.version.isEnterprise @billingStartTime)}}
{{! Enterprise should always have a @billingStartTime but as a fallback allow the user to query dates manually. }}
{{! A dropdown renders in Clients::DateRange to query specific dates if we have a @billingStartTime }}
<p class="has-text-weight-semibold">
{{if this.flags.isHvdManaged "For data period: " "For billing period: "}}
<span data-test-date-range="start">{{this.formattedStartDate}}</span>
-
<span data-test-date-range="end">{{this.formattedEndDate}}</span>
</p>
</PH.Description>
{{/if}}
<PH.Description>
{{#if this.showCommunity}}
{{else if (and @startTimestamp @endTimestamp)}}
{{! If these timestamps exist an initial query has already been made using the modal in Clients::DateRange }}
<div class="has-top-padding-m">
<Hds::Text::Display @tag="h3">Client counting period</Hds::Text::Display>
<Hds::Text::Body @tag="p" class="has-text-grey">Specify a date range to view within that timeframe. Click 'Edit' to
choose a different date range.</Hds::Text::Body>
<Hds::Text::Body @tag="p" @size="100" color="faint">Click 'Edit' to select a different date range.</Hds::Text::Body>
<Hds::Text::Body @tag="p">
<span data-test-date-range="start">{{this.formattedStartDate}}</span>
-

View File

@ -95,10 +95,6 @@ export default class ClientsPageHeaderComponent extends Component {
return `clients_export${ns}${csvDateRange}`;
}
get showCommunity() {
return this.version.isCommunity && !!this.formattedStartDate && !!this.formattedEndDate;
}
async getExportData() {
const adapter = this.store.adapterFor('clients/activity');
const { startTimestamp, endTimestamp } = this.args;

View File

@ -39,7 +39,10 @@ export default class ClientsCountsPageComponent extends Component<Args> {
}
get formattedBillingStartDate() {
return this.args.config.billingStartTimestamp.toISOString();
if (this.args.config?.billingStartTimestamp) {
return this.args.config.billingStartTimestamp.toISOString();
}
return null;
}
// passed into page-header for the export modal alert

View File

@ -28,7 +28,7 @@ export default class ClientsOverviewPageComponent extends Component<Args> {
get byMonthClients() {
// HVD clusters are billed differently and the monthly total is the important metric.
if (this.flags.isHvdManaged) {
return this.args.activity.byMonth;
return this.args.activity.byMonth || [];
}
// For self-managed clusters only the new_clients per month are relevant because clients accumulate over a billing period.
// (Since "total" per month is not cumulative it's not a useful metric)
@ -36,6 +36,7 @@ export default class ClientsOverviewPageComponent extends Component<Args> {
}
// Supplies data passed to dropdown filters (except months which is computed below )
@cached
get activityData() {
// If no month is selected the table displays all of the activity for the queried date range.
const selectedMonth = this.args.filterQueryParams.month;
@ -49,9 +50,12 @@ export default class ClientsOverviewPageComponent extends Component<Args> {
@cached
get months() {
return this.byMonthClients.map((m) => m.timestamp).reverse();
const timestamps = this.byMonthClients.map((m) => m.timestamp);
// display the most recent month at the top of the dropdown
return timestamps.reverse();
}
@cached
get tableData() {
if (this.activityData?.length) {
// Reset the `month` query param because it determines which dataset (see this.activityData)

View File

@ -18,6 +18,7 @@ module('Integration | Component | clients/date-range', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.version = this.owner.lookup('service:version');
Sinon.replace(timestamp, 'now', Sinon.fake.returns(new Date('2018-04-03T14:15:30')));
this.now = timestamp.now();
this.startTimestamp = '2018-01-01T14:15:30';
@ -36,6 +37,11 @@ module('Integration | Component | clients/date-range', function (hooks) {
};
});
test('it does not render if a start and end timestamp are already provided', async function (assert) {
await this.renderComponent();
assert.dom(DATE_RANGE.edit).doesNotExist('it does not render if timestamps are provided');
});
test('it formats modal inputs to ISO string timestamps', async function (assert) {
this.startTimestamp = undefined;
await this.renderComponent();
@ -80,7 +86,7 @@ module('Integration | Component | clients/date-range', function (hooks) {
});
test('it does not allow the current month to be selected as a start date or as an end date', async function (assert) {
this.owner.lookup('service:version').type = 'community';
this.version.type = 'community';
this.endTimestamp = undefined;
const currentMonth = format(timestamp.now(), 'yyyy-MM');
@ -91,7 +97,7 @@ module('Integration | Component | clients/date-range', function (hooks) {
assert.dom(DATE_RANGE.validation).hasText('You cannot select the current month or beyond.');
await click(GENERAL.submitButton);
assert.false(this.onChange.called);
assert.false(this.onChange.called, 'it does not call @onChange callback');
// This tests validation when the end date is the current month and start is valid.
// If start is current month and end is a valid prior selection, it will run into the validation error of start being after end date
@ -99,7 +105,22 @@ module('Integration | Component | clients/date-range', function (hooks) {
await fillIn(DATE_RANGE.editDate('start'), '2018-01');
await fillIn(DATE_RANGE.editDate('end'), currentMonth);
await click(GENERAL.submitButton);
assert.false(this.onChange.called);
assert.false(this.onChange.called, 'it does not call @onChange callback');
});
test('it allows the current month to be selected if enterprise and there is not a @billingStartTime', async function (assert) {
this.version.type = 'enterprise';
this.endTimestamp = undefined;
const currentMonth = format(timestamp.now(), 'yyyy-MM');
await this.renderComponent();
await click(DATE_RANGE.edit);
await fillIn(DATE_RANGE.editDate('start'), currentMonth);
await fillIn(DATE_RANGE.editDate('end'), currentMonth);
assert.dom(DATE_RANGE.validation).doesNotExist();
await click(GENERAL.submitButton);
assert.true(this.onChange.called, 'it calls @onChange callback');
});
module('enterprise', function (hooks) {
@ -126,6 +147,19 @@ module('Integration | Component | clients/date-range', function (hooks) {
});
});
test('it renders date range modal if there are no timestamps provided', async function (assert) {
this.billingStartTime = '';
this.startTimestamp = '';
this.endTimestamp = '';
await this.renderComponent();
assert
.dom(DATE_RANGE.edit)
.exists('it renders button to open date range modal')
.hasText('Set date range');
await click(DATE_RANGE.edit);
assert.dom(DATE_RANGE.editModal).exists();
});
test('it updates toggle text when a new date is selected', async function (assert) {
this.onChange = ({ start_time }) => this.set('startTimestamp', start_time);

View File

@ -25,6 +25,7 @@ module('Integration | Component | clients/page-header', function (hooks) {
this.downloadStub = Sinon.stub(this.owner.lookup('service:download'), 'download');
this.startTimestamp = '2022-06-01T23:00:11.050Z';
this.endTimestamp = '2022-12-01T23:00:11.050Z';
this.billingStartTime = this.startTimestamp;
this.upgradesDuringActivity = [];
this.noData = undefined;
this.server.post('/sys/capabilities-self', () =>
@ -34,7 +35,7 @@ module('Integration | Component | clients/page-header', function (hooks) {
this.renderComponent = async () => {
return render(hbs`
<Clients::PageHeader
@billingStartTime={{this.startTimestamp}}
@billingStartTime={{this.billingStartTime}}
@startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}}
@upgradesDuringActivity={{this.upgradesDuringActivity}}
@ -279,5 +280,13 @@ module('Integration | Component | clients/page-header', function (hooks) {
await this.renderComponent();
assert.dom(this.element).hasTextContaining('Client Usage For data period:');
});
test('it allows date editing if no billing start time is provided', async function (assert) {
this.billingStartTime = '';
await this.renderComponent();
assert.dom(CLIENT_COUNT.dateRange.edit).exists('it renders edit button to open modal').hasText('Edit');
assert.dom(CLIENT_COUNT.dateRange.dateDisplay('start')).hasText('June 2022');
assert.dom(CLIENT_COUNT.dateRange.dateDisplay('end')).hasText('December 2022');
});
});
});