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 ...attributes>
<div class="is-flex-column align-items-end"> <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"> <Hds::Text::Display @tag="p" @size="100" class="has-bottom-margin-xs">
{{if this.flags.isHvdManaged "Change data period" "Change billing period"}} {{if this.flags.isHvdManaged "Change data period" "Change billing period"}}
</Hds::Text::Display> </Hds::Text::Display>
@ -34,9 +35,8 @@
{{/each}} {{/each}}
{{/if}} {{/if}}
</Hds::Dropdown> </Hds::Dropdown>
{{else}} {{else if (not (and @startTimestamp @endTimestamp))}}
{{! Hide if the user has made an initial query because dates can be updated by clicking "Edit" beside the date range }} {{! Hide if the user has already made an initial query because dates can be updated by clicking "Edit" beside the date range }}
{{#unless (and @startTimestamp @endTimestamp)}}
<Hds::Button <Hds::Button
class="has-left-margin-xs" class="has-left-margin-xs"
@text="Set date range" @text="Set date range"
@ -44,7 +44,6 @@
{{on "click" (fn @setEditModalVisible true)}} {{on "click" (fn @setEditModalVisible true)}}
data-test-date-range-edit data-test-date-range-edit
/> />
{{/unless}}
{{/if}} {{/if}}
</div> </div>
@ -54,10 +53,11 @@
Edit date range Edit date range
</M.Header> </M.Header>
<M.Body> <M.Body>
<Hds::Text::Body @tag="div"> {{#if this.version.isCommunity}}
<p class="has-bottom-margin-s"> <p class="has-bottom-margin-s">
Use custom date ranges to query historic client count data. Query results do not include the current month. Use custom date ranges to query historic client count data. Query results do not include the current month.
</p> </p>
{{/if}}
<ul class="has-bottom-margin-s"> <ul class="has-bottom-margin-s">
<li> <li>
<strong>Start</strong> <strong>Start</strong>
@ -71,14 +71,13 @@
<p class="has-bottom-margin-s"> <p class="has-bottom-margin-s">
We recommend setting We recommend setting
<strong>Start</strong> <strong>Start</strong>
to your Vault deploy date to get the most accurate new and total client count estimations.</p> to your Vault deploy date to get the most accurate client counts.</p>
<div class="clients-date-range-display"> <div class="clients-date-range-display">
<div> <div>
<Hds::Form::TextInput::Field <Hds::Form::TextInput::Field
@type="month" @type="month"
@value={{this.modalStart}} @value={{this.modalStart}}
max={{this.previousMonth}} max={{if this.version.isCommunity this.previousMonth (date-format this.currentMonth "yyyy-MM")}}
id="start-month" id="start-month"
name="modalStart" name="modalStart"
{{on "change" this.updateDate}} {{on "change" this.updateDate}}
@ -92,7 +91,7 @@
<Hds::Form::TextInput::Field <Hds::Form::TextInput::Field
@type="month" @type="month"
@value={{this.modalEnd}} @value={{this.modalEnd}}
max={{this.previousMonth}} max={{if this.version.isCommunity this.previousMonth (date-format this.currentMonth "yyyy-MM")}}
id="end-month" id="end-month"
name="modalEnd" name="modalEnd"
{{on "change" this.updateDate}} {{on "change" this.updateDate}}
@ -109,7 +108,6 @@
data-test-date-range-validation data-test-date-range-validation
>{{this.validationError}}</Hds::Form::Error> >{{this.validationError}}</Hds::Form::Error>
{{/if}} {{/if}}
</Hds::Text::Body>
</M.Body> </M.Body>
<M.Footer as |F|> <M.Footer as |F|>
<Hds::Button @text="Save" {{on "click" this.handleSave}} data-test-submit /> <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) { if (this.modalStart > this.modalEnd) {
return 'Start date must be before end date.'; 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 'You cannot select the current month or beyond.';
} }
return null; return null;

View File

@ -23,22 +23,21 @@
/> />
</PH.Subtitle> </PH.Subtitle>
{{/if}} {{/if}}
{{#if this.version.isEnterprise}} <PH.Description>
<PH.Description class="has-text-weight-semibold flex"> {{#if (and this.version.isEnterprise @billingStartTime)}}
<p> {{! Enterprise should always have a @billingStartTime but as a fallback allow the user to query dates manually. }}
{{if this.flags.isHvdManaged "For data period:" "For billing period:"}} {{! 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="start">{{this.formattedStartDate}}</span>
- -
<span data-test-date-range="end">{{this.formattedEndDate}}</span> <span data-test-date-range="end">{{this.formattedEndDate}}</span>
</p> </p>
</PH.Description> {{else if (and @startTimestamp @endTimestamp)}}
{{/if}} {{! If these timestamps exist an initial query has already been made using the modal in Clients::DateRange }}
<PH.Description>
{{#if this.showCommunity}}
<div class="has-top-padding-m"> <div class="has-top-padding-m">
<Hds::Text::Display @tag="h3">Client counting period</Hds::Text::Display> <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 <Hds::Text::Body @tag="p" @size="100" color="faint">Click 'Edit' to select a different date range.</Hds::Text::Body>
choose a different date range.</Hds::Text::Body>
<Hds::Text::Body @tag="p"> <Hds::Text::Body @tag="p">
<span data-test-date-range="start">{{this.formattedStartDate}}</span> <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}`; return `clients_export${ns}${csvDateRange}`;
} }
get showCommunity() {
return this.version.isCommunity && !!this.formattedStartDate && !!this.formattedEndDate;
}
async getExportData() { async getExportData() {
const adapter = this.store.adapterFor('clients/activity'); const adapter = this.store.adapterFor('clients/activity');
const { startTimestamp, endTimestamp } = this.args; const { startTimestamp, endTimestamp } = this.args;

View File

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

View File

@ -28,7 +28,7 @@ export default class ClientsOverviewPageComponent extends Component<Args> {
get byMonthClients() { get byMonthClients() {
// HVD clusters are billed differently and the monthly total is the important metric. // HVD clusters are billed differently and the monthly total is the important metric.
if (this.flags.isHvdManaged) { 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. // 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) // (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 ) // Supplies data passed to dropdown filters (except months which is computed below )
@cached
get activityData() { get activityData() {
// If no month is selected the table displays all of the activity for the queried date range. // If no month is selected the table displays all of the activity for the queried date range.
const selectedMonth = this.args.filterQueryParams.month; const selectedMonth = this.args.filterQueryParams.month;
@ -49,9 +50,12 @@ export default class ClientsOverviewPageComponent extends Component<Args> {
@cached @cached
get months() { 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() { get tableData() {
if (this.activityData?.length) { if (this.activityData?.length) {
// Reset the `month` query param because it determines which dataset (see this.activityData) // 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); setupRenderingTest(hooks);
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.version = this.owner.lookup('service:version');
Sinon.replace(timestamp, 'now', Sinon.fake.returns(new Date('2018-04-03T14:15:30'))); Sinon.replace(timestamp, 'now', Sinon.fake.returns(new Date('2018-04-03T14:15:30')));
this.now = timestamp.now(); this.now = timestamp.now();
this.startTimestamp = '2018-01-01T14:15:30'; 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) { test('it formats modal inputs to ISO string timestamps', async function (assert) {
this.startTimestamp = undefined; this.startTimestamp = undefined;
await this.renderComponent(); 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) { 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; this.endTimestamp = undefined;
const currentMonth = format(timestamp.now(), 'yyyy-MM'); 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.'); assert.dom(DATE_RANGE.validation).hasText('You cannot select the current month or beyond.');
await click(GENERAL.submitButton); 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. // 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 // 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('start'), '2018-01');
await fillIn(DATE_RANGE.editDate('end'), currentMonth); await fillIn(DATE_RANGE.editDate('end'), currentMonth);
await click(GENERAL.submitButton); 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) { 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) { test('it updates toggle text when a new date is selected', async function (assert) {
this.onChange = ({ start_time }) => this.set('startTimestamp', start_time); 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.downloadStub = Sinon.stub(this.owner.lookup('service:download'), 'download');
this.startTimestamp = '2022-06-01T23:00:11.050Z'; this.startTimestamp = '2022-06-01T23:00:11.050Z';
this.endTimestamp = '2022-12-01T23:00:11.050Z'; this.endTimestamp = '2022-12-01T23:00:11.050Z';
this.billingStartTime = this.startTimestamp;
this.upgradesDuringActivity = []; this.upgradesDuringActivity = [];
this.noData = undefined; this.noData = undefined;
this.server.post('/sys/capabilities-self', () => this.server.post('/sys/capabilities-self', () =>
@ -34,7 +35,7 @@ module('Integration | Component | clients/page-header', function (hooks) {
this.renderComponent = async () => { this.renderComponent = async () => {
return render(hbs` return render(hbs`
<Clients::PageHeader <Clients::PageHeader
@billingStartTime={{this.startTimestamp}} @billingStartTime={{this.billingStartTime}}
@startTimestamp={{this.startTimestamp}} @startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}} @endTimestamp={{this.endTimestamp}}
@upgradesDuringActivity={{this.upgradesDuringActivity}} @upgradesDuringActivity={{this.upgradesDuringActivity}}
@ -279,5 +280,13 @@ module('Integration | Component | clients/page-header', function (hooks) {
await this.renderComponent(); await this.renderComponent();
assert.dom(this.element).hasTextContaining('Client Usage For data period:'); 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');
});
}); });
}); });