UI/Client counts view if no license (#13964)

* adds date picker if no license start date found

* handle permissions denied for license endpoint

* handle permissions errors if no license start date

* change empty state copy for OSS

* fix tests and empty state view

* update nav links

* remove ternary

Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>

* simplify hbs boolean

Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>

* organize history file

* organize current file

* rerun tests

* fix conditional to show attribution chart

* match main

Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
This commit is contained in:
claire bontempo
2022-02-10 12:51:50 -08:00
committed by GitHub
parent 282ab01e27
commit 171b4f46fd
13 changed files with 220 additions and 110 deletions

View File

@@ -4,32 +4,25 @@ import { formatRFC3339 } from 'date-fns';
export default Application.extend({ export default Application.extend({
formatTimeParams(query) { formatTimeParams(query) {
let { start_time, end_time } = query; let { start_time, end_time } = query;
// do not query without start_time. Otherwise returns last year data, which is not reflective of billing data. // check if it's an array, if it is, it's coming from an action like selecting a new startTime or new EndTime
if (start_time) { if (Array.isArray(start_time)) {
// check if it's an array, if it is, it's coming from an action like selecting a new startTime or new EndTime let startYear = Number(start_time[0]);
if (Array.isArray(start_time)) { let startMonth = Number(start_time[1]);
let startYear = Number(start_time[0]); start_time = formatRFC3339(new Date(startYear, startMonth));
let startMonth = Number(start_time[1]); }
start_time = formatRFC3339(new Date(startYear, startMonth)); if (end_time) {
if (Array.isArray(end_time)) {
let endYear = Number(end_time[0]);
let endMonth = Number(end_time[1]);
end_time = formatRFC3339(new Date(endYear, endMonth));
} }
if (end_time) {
if (Array.isArray(end_time)) {
let endYear = Number(end_time[0]);
let endMonth = Number(end_time[1]);
end_time = formatRFC3339(new Date(endYear, endMonth));
}
return { start_time, end_time }; return { start_time, end_time };
} else {
return { start_time };
}
} else { } else {
// did not have a start time, return null through to component. return { start_time };
return null;
} }
}, },
// ARG TODO current Month tab is hitting this endpoint. Need to amend so only hit on Monthly history (large payload)
// query comes in as either: {start_time: '2021-03-17T00:00:00Z'} or // query comes in as either: {start_time: '2021-03-17T00:00:00Z'} or
// {start_time: Array(2), end_time: Array(2)} // {start_time: Array(2), end_time: Array(2)}
// end_time: (2) ['2022', 0] // end_time: (2) ['2022', 0]
@@ -44,9 +37,6 @@ export default Application.extend({
response.id = response.request_id || 'no-data'; response.id = response.request_id || 'no-data';
return response; return response;
}); });
} else {
// did not have a start time, return null through to component.
return null;
} }
}, },
}); });

View File

@@ -7,18 +7,28 @@ export default class Current extends Component {
{ key: 'entity_clients', label: 'entity clients' }, { key: 'entity_clients', label: 'entity clients' },
{ key: 'non_entity_clients', label: 'non-entity clients' }, { key: 'non_entity_clients', label: 'non-entity clients' },
]; ];
@tracked firstUpgradeVersion = this.args.model.versionHistory[0].id || null; // return 1.9.0 or earliest upgrade post 1.9.0
@tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled || null; // returns RFC3339 timestamp
@tracked selectedNamespace = null; @tracked selectedNamespace = null;
@tracked namespaceArray = this.byNamespaceCurrent.map((namespace) => { @tracked namespaceArray = this.byNamespaceCurrent.map((namespace) => {
return { name: namespace['label'], id: namespace['label'] }; return { name: namespace['label'], id: namespace['label'] };
}); });
@tracked firstUpgradeVersion = this.args.model.versionHistory[0].id || null; // return 1.9.0 or earliest upgrade post 1.9.0
@tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled || null; // returns RFC3339 timestamp
// API client count data by namespace for current/partial month // Response client count data by namespace for current/partial month
get byNamespaceCurrent() { get byNamespaceCurrent() {
return this.args.model.monthly?.byNamespace || []; return this.args.model.monthly?.byNamespace || [];
} }
get isGatheringData() {
// return true if tracking IS enabled but no data collected yet
return this.args.model.config?.enabled === 'On' && this.byNamespaceCurrent.length === 0;
}
get hasAttributionData() {
return this.totalUsageCounts.clients !== 0 && this.totalClientsData.length !== 0;
}
get countsIncludeOlderData() { get countsIncludeOlderData() {
let firstUpgrade = this.args.model.versionHistory[0]; let firstUpgrade = this.args.model.versionHistory[0];
if (!firstUpgrade) { if (!firstUpgrade) {

View File

@@ -3,9 +3,10 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { isSameMonth, isAfter } from 'date-fns'; import { isSameMonth, isAfter } from 'date-fns';
export default class History extends Component { export default class History extends Component {
// TODO CMB alphabetize and delete unused vars (particularly @tracked) @service store;
@service version;
arrayOfMonths = [ arrayOfMonths = [
'January', 'January',
'February', 'February',
@@ -26,7 +27,7 @@ export default class History extends Component {
{ key: 'non_entity_clients', label: 'non-entity clients' }, { key: 'non_entity_clients', label: 'non-entity clients' },
]; ];
// needed for startTime modal picker // FOR START DATE EDIT & MODAL //
months = Array.from({ length: 12 }, (item, i) => { months = Array.from({ length: 12 }, (item, i) => {
return new Date(0, i).toLocaleString('en-US', { month: 'long' }); return new Date(0, i).toLocaleString('en-US', { month: 'long' });
}); });
@@ -34,33 +35,43 @@ export default class History extends Component {
return new Date().getFullYear() - i; return new Date().getFullYear() - i;
}); });
@service store;
@tracked queriedActivityResponse = null;
@tracked barChartSelection = false;
@tracked isEditStartMonthOpen = false; @tracked isEditStartMonthOpen = false;
@tracked responseRangeDiffMessage = null;
@tracked startTimeRequested = null;
@tracked startTimeFromResponse = this.args.model.startTimeFromLicense; // ex: ['2021', 3] is April 2021 (0 indexed)
@tracked endTimeFromResponse = this.args.model.endTimeFromResponse;
@tracked startMonth = null; @tracked startMonth = null;
@tracked startYear = null; @tracked startYear = null;
// FOR HISTORY COMPONENT //
// RESPONSE
@tracked endTimeFromResponse = this.args.model.endTimeFromResponse;
@tracked startTimeFromResponse = this.args.model.startTimeFromLicense; // ex: ['2021', 3] is April 2021 (0 indexed)
@tracked startTimeRequested = null;
@tracked queriedActivityResponse = null;
// VERSION/UPGRADE INFO
@tracked firstUpgradeVersion = this.args.model.versionHistory[0].id || null; // return 1.9.0 or earliest upgrade post 1.9.0
@tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled || null; // returns RFC3339 timestamp
// SEARCH SELECT
@tracked selectedNamespace = null; @tracked selectedNamespace = null;
@tracked noActivityDate = '';
@tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => { @tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => {
return { name: namespace['label'], id: namespace['label'] }; return { name: namespace['label'], id: namespace['label'] };
}); });
@tracked firstUpgradeVersion = this.args.model.versionHistory[0].id || null; // return 1.9.0 or earliest upgrade post 1.9.0
@tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled || null; // returns RFC3339 timestamp // TEMPLATE MESSAGING
@tracked noActivityDate = '';
@tracked responseRangeDiffMessage = null;
// on init API response uses license start_date, getter updates when user queries dates // on init API response uses license start_date, getter updates when user queries dates
get getActivityResponse() { get getActivityResponse() {
return this.queriedActivityResponse || this.args.model.activity; return this.queriedActivityResponse || this.args.model.activity;
} }
get hasAttributionData() {
return this.totalUsageCounts.clients !== 0 && this.totalClientsData.length !== 0;
}
get startTimeDisplay() { get startTimeDisplay() {
if (!this.startTimeFromResponse) { if (!this.startTimeFromResponse) {
// otherwise will return date of new Date(null)
return null; return null;
} }
let month = this.startTimeFromResponse[1]; let month = this.startTimeFromResponse[1];
@@ -70,7 +81,6 @@ export default class History extends Component {
get endTimeDisplay() { get endTimeDisplay() {
if (!this.endTimeFromResponse) { if (!this.endTimeFromResponse) {
// otherwise will return date of new Date(null)
return null; return null;
} }
let month = this.endTimeFromResponse[1]; let month = this.endTimeFromResponse[1];
@@ -120,7 +130,6 @@ export default class History extends Component {
return isAfter(versionDate, startTimeFromResponseAsDateObject) ? versionDate : false; return isAfter(versionDate, startTimeFromResponseAsDateObject) ? versionDate : false;
} }
// ACTIONS
@action @action
async handleClientActivityQuery(month, year, dateType) { async handleClientActivityQuery(month, year, dateType) {
if (dateType === 'cancel') { if (dateType === 'cancel') {
@@ -134,7 +143,7 @@ export default class History extends Component {
// clicked "Edit" Billing start month in Dashboard which opens a modal. // clicked "Edit" Billing start month in Dashboard which opens a modal.
if (dateType === 'startTime') { if (dateType === 'startTime') {
let monthIndex = this.arrayOfMonths.indexOf(month); let monthIndex = this.arrayOfMonths.indexOf(month);
this.startTimeRequested = [year.toString(), monthIndex]; // ['2021', 0] (e.g. January 2021) // TODO CHANGE TO ARRAY this.startTimeRequested = [year.toString(), monthIndex]; // ['2021', 0] (e.g. January 2021)
this.endTimeRequested = null; this.endTimeRequested = null;
} }
// clicked "Custom End Month" from the calendar-widget // clicked "Custom End Month" from the calendar-widget
@@ -173,7 +182,7 @@ export default class History extends Component {
} }
this.queriedActivityResponse = response; this.queriedActivityResponse = response;
} catch (e) { } catch (e) {
// ARG TODO handle error return e;
} }
} }
@@ -188,6 +197,7 @@ export default class History extends Component {
this.selectedNamespace = value; this.selectedNamespace = value;
} }
// FOR START DATE MODAL
@action @action
selectStartMonth(month) { selectStartMonth(month) {
this.startMonth = month; this.startMonth = month;
@@ -198,7 +208,7 @@ export default class History extends Component {
this.startYear = year; this.startYear = year;
} }
// HELPERS // HELPERS //
filterByNamespace(namespace) { filterByNamespace(namespace) {
return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace); return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace);
} }

View File

@@ -0,0 +1,37 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
/**
* @module DateDropdown
* DateDropdown components are used to display a dropdown of months and years to handle date selection
*
* @example
* ```js
* <DateDropdown @handleDateSelection={this.actionFromParent} @name={{"startTime"}}/>
* ```
* @param {function} handleDateSelection - is the action from the parent that the date picker triggers
* @param {string} [name] - optional argument passed from date dropdown to parent function
*/
export default class DateDropdown extends Component {
@tracked startMonth = null;
@tracked startYear = null;
months = Array.from({ length: 12 }, (item, i) => {
return new Date(0, i).toLocaleString('en-US', { month: 'long' });
});
years = Array.from({ length: 5 }, (item, i) => {
return new Date().getFullYear() - i;
});
@action
selectStartMonth(month) {
this.startMonth = month;
}
@action
selectStartYear(year) {
this.startYear = year;
}
}

View File

@@ -5,19 +5,24 @@ import { action } from '@ember/object';
export default class HistoryRoute extends Route { export default class HistoryRoute extends Route {
async getActivity(start_time) { async getActivity(start_time) {
try { try {
return this.store.queryRecord('clients/activity', { start_time }); // on init ONLY make network request if we have a start time from the license
// otherwise user needs to manually input
return start_time
? await this.store.queryRecord('clients/activity', { start_time })
: { endTime: null };
} catch (e) { } catch (e) {
// ARG TODO handle
return e; return e;
} }
} }
async getLicense() { async getLicenseStartTime() {
try { try {
return this.store.queryRecord('license', {}); let license = await this.store.queryRecord('license', {});
// if license.startTime is 'undefined' return 'null' for consistency
return license.startTime || null;
} catch (e) { } catch (e) {
// ARG TODO handle // if error due to permission denied, return null so user can input date manually
return e; return null;
} }
} }
@@ -48,18 +53,18 @@ export default class HistoryRoute extends Route {
async model() { async model() {
let config = await this.store.queryRecord('clients/config', {}).catch((e) => { let config = await this.store.queryRecord('clients/config', {}).catch((e) => {
console.debug(e);
// swallowing error so activity can show if no config permissions // swallowing error so activity can show if no config permissions
console.debug(e);
return {}; return {};
}); });
let license = await this.getLicense(); let licenseStart = await this.getLicenseStartTime();
let activity = await this.getActivity(license.startTime); let activity = await this.getActivity(licenseStart);
return RSVP.hash({ return RSVP.hash({
config, config,
activity, activity,
startTimeFromLicense: this.parseRFC3339(license.startTime), startTimeFromLicense: this.parseRFC3339(licenseStart),
endTimeFromResponse: activity ? this.parseRFC3339(activity.endTime) : null, endTimeFromResponse: this.parseRFC3339(activity?.endTime),
versionHistory: this.getVersionHistory(), versionHistory: this.getVersionHistory(),
}); });
} }

View File

@@ -13,46 +13,37 @@
</div> </div>
</div> </div>
{{#if (eq @totalUsageCounts.clients 0)}} <div class="chart-container-wide">
<div class="chart-empty-state"> <Clients::HorizontalBarChart
<EmptyState @dataset={{this.barChartTotalClients}}
@title="No data received" @chartLegend={{@chartLegend}}
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes." @totalUsageCounts={{@totalUsageCounts}}
/> />
</div> </div>
{{else}}
<div class="chart-container-wide">
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@chartLegend={{@chartLegend}}
@totalUsageCounts={{@totalUsageCounts}}
/>
</div>
<div class="chart-subTitle"> <div class="chart-subTitle">
<p class="chart-subtext">{{this.chartText.totalCopy}}</p> <p class="chart-subtext">{{this.chartText.totalCopy}}</p>
</div> </div>
<div class="data-details-top"> <div class="data-details-top">
<h3 class="data-details">Top {{this.attributionBreakdown}}</h3> <h3 class="data-details">Top {{this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.label}}</p> <p class="data-details">{{this.topClientCounts.label}}</p>
</div> </div>
<div class="data-details-bottom"> <div class="data-details-bottom">
<h3 class="data-details">Clients in {{this.attributionBreakdown}}</h3> <h3 class="data-details">Clients in {{this.attributionBreakdown}}</h3>
<p class="data-details">{{format-number this.topClientCounts.clients}}</p> <p class="data-details">{{format-number this.topClientCounts.clients}}</p>
</div> </div>
<div class="timestamp"> <div class="timestamp">
Updated Updated
{{date-format @timestamp "MMM dd yyyy, h:mm:ss aaa"}} {{date-format @timestamp "MMM dd yyyy, h:mm:ss aaa"}}
</div> </div>
<div class="legend-center"> <div class="legend-center">
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span> <span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span> <span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
</div> </div>
{{/if}}
</div> </div>
{{! MODAL FOR CSV DOWNLOAD }} {{! MODAL FOR CSV DOWNLOAD }}

View File

@@ -13,6 +13,11 @@
</LinkTo> </LinkTo>
{{/if}} {{/if}}
</EmptyState> </EmptyState>
{{else if this.isGatheringData}}
<EmptyState
@title="No data received"
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
/>
{{else}} {{else}}
<div class="is-subtitle-gray has-bottom-margin-m"> <div class="is-subtitle-gray has-bottom-margin-m">
FILTERS FILTERS
@@ -48,7 +53,7 @@
@title={{date-format this.responseTimestamp "MMMM"}} @title={{date-format this.responseTimestamp "MMMM"}}
@totalUsageCounts={{this.totalUsageCounts}} @totalUsageCounts={{this.totalUsageCounts}}
/> />
{{#if this.totalClientsData}} {{#if this.hasAttributionData}}
<Clients::Attribution <Clients::Attribution
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@totalClientsData={{this.totalClientsData}} @totalClientsData={{this.totalClientsData}}
@@ -59,8 +64,6 @@
@timestamp={{this.responseTimestamp}} @timestamp={{this.responseTimestamp}}
/> />
{{/if}} {{/if}}
{{else}}
<EmptyState @title={{concat "No partial history"}} @message="There is no data in the current month yet." />
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}

View File

@@ -8,10 +8,14 @@
Billing start month Billing start month
</h1> </h1>
<div data-test-start-date-editor class="is-flex-align-baseline"> <div data-test-start-date-editor class="is-flex-align-baseline">
<p class="is-size-6">{{this.startTimeDisplay}}</p> {{#if this.startTimeDisplay}}
<button type="button" class="button is-link" {{on "click" (fn (mut this.isEditStartMonthOpen) true)}}> <p class="is-size-6">{{this.startTimeDisplay}}</p>
Edit <button type="button" class="button is-link" {{on "click" (fn (mut this.isEditStartMonthOpen) true)}}>
</button> Edit
</button>
{{else}}
<DateDropdown @handleDateSelection={{this.handleClientActivityQuery}} @name={{"startTime"}} />
{{/if}}
</div> </div>
<p class="is-8 has-text-grey has-bottom-margin-xl"> <p class="is-8 has-text-grey has-bottom-margin-xl">
This date comes from your license, and defines when client counting starts. Without this starting point, the data shown This date comes from your license, and defines when client counting starts. Without this starting point, the data shown
@@ -106,7 +110,7 @@
{{else}} {{else}}
{{#if this.totalUsageCounts}} {{#if this.totalUsageCounts}}
<Clients::UsageStats @title="Total usage" @totalUsageCounts={{this.totalUsageCounts}} /> <Clients::UsageStats @title="Total usage" @totalUsageCounts={{this.totalUsageCounts}} />
{{#if this.totalClientsData}} {{#if this.hasAttributionData}}
<Clients::Attribution <Clients::Attribution
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@totalClientsData={{this.totalClientsData}} @totalClientsData={{this.totalClientsData}}
@@ -123,10 +127,17 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
{{else}} {{else}}
<EmptyState {{#if this.version.isEnterprise}}
@title="No billing start date found" <EmptyState
@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." @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."
/>
{{else}}
<EmptyState
@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."
/>
{{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}

View File

@@ -152,7 +152,7 @@
<ul class="menu-list"> <ul class="menu-list">
<li class="action"> <li class="action">
{{! template-lint-disable no-unknown-arguments-for-builtin-components }} {{! template-lint-disable no-unknown-arguments-for-builtin-components }}
<LinkTo @route="vault.cluster.clients" @query={{hash tab="current"}} @invokeAction={{@onLinkClick}}> <LinkTo @route="vault.cluster.clients.index" @invokeAction={{@onLinkClick}}>
<div class="level is-mobile"> <div class="level is-mobile">
<span class="level-left">Client count</span> <span class="level-left">Client count</span>
<Chevron class="has-text-grey-light level-right" /> <Chevron class="has-text-grey-light level-right" />

View File

@@ -0,0 +1,54 @@
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger
data-test-popup-menu-trigger="true"
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
>
{{or this.startMonth "Month"}}
<Chevron @direction="down" @isButton={{true}} />
</D.Trigger>
<D.Content class="popup-menu-content is-wide">
<nav class="box menu scroll">
<ul class="menu-list">
{{#each this.months as |month|}}
<button
type="button"
class="link"
{{on "click" (queue (fn this.selectStartMonth month) (action D.actions.close))}}
>
{{month}}
</button>
{{/each}}
</ul>
</nav>
</D.Content>
</BasicDropdown>
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger
data-test-popup-menu-trigger="true"
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
>
{{or this.startYear "Year"}}
<Chevron @direction="down" @isButton={{true}} />
</D.Trigger>
<D.Content class="popup-menu-content is-wide">
<nav class="box menu">
<ul class="menu-list">
{{#each this.years as |year|}}
<button type="button" class="link" {{on "click" (queue (fn this.selectStartYear year) (action D.actions.close))}}>
{{year}}
</button>
{{/each}}
</ul>
</nav>
</D.Content>
</BasicDropdown>
<button
type="button"
class="button is-primary"
disabled={{if (and this.startMonth this.startYear) false true}}
{{on "click" (fn @handleDateSelection this.startMonth this.startYear @name)}}
>
Save
</button>

View File

@@ -76,9 +76,8 @@
{{! template-lint-configure no-curly-component-invocation "warn" }} {{! template-lint-configure no-curly-component-invocation "warn" }}
{{! template-lint-configure no-link-to-positional-params "warn" }} {{! template-lint-configure no-link-to-positional-params "warn" }}
{{#link-to {{#link-to
"vault.cluster.clients" "vault.cluster.clients.history"
(query-params tab="history") current-when="vault.cluster.clients.history"
current-when="vault.cluster.clients"
data-test-navbar-item="metrics" data-test-navbar-item="metrics"
}} }}
Client count Client count

View File

@@ -1,3 +1,2 @@
<Clients::Dashboard @model={{@model}} /> <Clients::Dashboard @model={{@model}} />
<Clients::History @model={{@model}} @isLoading={{this.currentlyLoading}} /> <Clients::History @model={{@model}} @isLoading={{this.currentlyLoading}} />

View File

@@ -32,12 +32,13 @@ module('Integration | Component | client count current', function (hooks) {
<div id="modal-wormhole"></div> <div id="modal-wormhole"></div>
<Clients::Current @model={{this.model}} />`); <Clients::Current @model={{this.model}} />`);
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists'); assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
assert.dom('[data-test-empty-state-title]').hasText('No partial history'); assert.dom('[data-test-empty-state-title]').hasText('No data received');
}); });
test('it shows zeroed data when enabled but no counts', async function (assert) { test('it shows zeroed data when enabled but no counts', async function (assert) {
Object.assign(this.model.config, { queriesAvailable: true, enabled: 'On' }); Object.assign(this.model.config, { queriesAvailable: true, enabled: 'On' });
Object.assign(this.model.monthly, { Object.assign(this.model.monthly, {
byNamespace: [{ label: 'root', clients: 0, entity_clients: 0, non_entity_clients: 0 }],
total: { clients: 0, entity_clients: 0, non_entity_clients: 0 }, total: { clients: 0, entity_clients: 0, non_entity_clients: 0 },
}); });
await render(hbs` await render(hbs`