UI/Update Routing and Handle response from API (#13885)
* updates data with response returned after dates queried * alphabetize todo * clarify comments * change dashboard.js to history.js * separate clients route, add history and config * add loading to config template * Add failsafes for no data * remove commented code * update all LinkTos with new routes, remove params * return response if no data * fix tests * cleanup * fixes template with namespace filter * fixes tests with namespace filter merged * fix namespace array mapping * add version history to test object Co-authored-by: hashishaw <cshaw@hashicorp.com>
This commit is contained in:
parent
12b0e2a56b
commit
5f5bd1126e
|
@ -46,12 +46,12 @@ export default class Attribution extends Component {
|
|||
// truncate data before sending to chart component
|
||||
// move truncating to serializer when we add separate request to fetch and export ALL namespace data
|
||||
get barChartTotalClients() {
|
||||
return this.args.totalClientsData.slice(0, 10);
|
||||
return this.args.totalClientsData?.slice(0, 10);
|
||||
}
|
||||
|
||||
get topClientCounts() {
|
||||
// get top namespace or auth method
|
||||
return this.args.totalClientsData[0];
|
||||
return this.args.totalClientsData ? this.args.totalClientsData[0] : null;
|
||||
}
|
||||
|
||||
get attributionBreakdown() {
|
||||
|
@ -65,8 +65,8 @@ export default class Attribution extends Component {
|
|||
return {
|
||||
description:
|
||||
'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.',
|
||||
newCopy: `The new clients in the namespace for this ${dateText}.
|
||||
This aids in understanding which namespaces create and use new clients
|
||||
newCopy: `The new clients in the namespace for this ${dateText}.
|
||||
This aids in understanding which namespaces create and use new clients
|
||||
${dateText === 'date range' ? ' over time.' : '.'}`,
|
||||
totalCopy: `The total clients in the namespace for this ${dateText}. This number is useful for identifying overall usage volume.`,
|
||||
};
|
||||
|
@ -74,7 +74,7 @@ export default class Attribution extends Component {
|
|||
return {
|
||||
description:
|
||||
'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.',
|
||||
newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients
|
||||
newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients
|
||||
${dateText === 'date range' ? ' over time.' : '.'}`,
|
||||
totalCopy: `The total clients used by the auth method for this ${dateText}. This number is useful for identifying overall usage volume. `,
|
||||
};
|
||||
|
|
|
@ -57,7 +57,7 @@ export default class ConfigComponent extends Component {
|
|||
this.error = err.message;
|
||||
return;
|
||||
}
|
||||
this.router.transitionTo('vault.cluster.clients.index');
|
||||
this.router.transitionTo('vault.cluster.clients.config');
|
||||
}).drop())
|
||||
save;
|
||||
|
||||
|
|
|
@ -7,12 +7,17 @@ export default class Current extends Component {
|
|||
{ key: 'entity_clients', label: 'entity clients' },
|
||||
{ key: 'non_entity_clients', label: 'non-entity clients' },
|
||||
];
|
||||
@tracked namespaceArray = this.args.model.monthly?.byNamespace.map((namespace) => {
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked namespaceArray = this.byNamespaceCurrent.map((namespace) => {
|
||||
return { name: namespace['label'], id: namespace['label'] };
|
||||
});
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked firstUpgradeVersion = this.args.model.versionHistory[0].id; // return 1.9.0 or earliest upgrade post 1.9.0
|
||||
@tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled; // returns RFC3339 timestamp
|
||||
@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
|
||||
get byNamespaceCurrent() {
|
||||
return this.args.model.monthly?.byNamespace || [];
|
||||
}
|
||||
|
||||
get countsIncludeOlderData() {
|
||||
let firstUpgrade = this.args.model.versionHistory[0];
|
||||
|
@ -24,15 +29,6 @@ export default class Current extends Component {
|
|||
return isAfter(versionDate, startOfMonth(new Date())) ? versionDate : false;
|
||||
}
|
||||
|
||||
get licenseStartDate() {
|
||||
return this.args.licenseStartDate || null;
|
||||
}
|
||||
|
||||
// API client count data by namespace for current/partial month
|
||||
get byNamespaceCurrent() {
|
||||
return this.args.model.monthly?.byNamespace || null;
|
||||
}
|
||||
|
||||
// top level TOTAL client counts for current/partial month
|
||||
get totalUsageCounts() {
|
||||
return this.selectedNamespace
|
||||
|
|
|
@ -1,209 +0,0 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { isSameMonth, isAfter } from 'date-fns';
|
||||
|
||||
export default class Dashboard extends Component {
|
||||
arrayOfMonths = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
maxNamespaces = 10;
|
||||
chartLegend = [
|
||||
{ key: 'entity_clients', label: 'entity clients' },
|
||||
{ key: 'non_entity_clients', label: 'non-entity clients' },
|
||||
];
|
||||
|
||||
// needed for startTime modal picker
|
||||
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;
|
||||
});
|
||||
|
||||
@service store;
|
||||
|
||||
@tracked barChartSelection = 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 startYear = null;
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked noActivityDate = '';
|
||||
@tracked namespaceArray = this.args.model.activity?.byNamespace.map((namespace) => {
|
||||
return { name: namespace['label'], id: namespace['label'] };
|
||||
});
|
||||
@tracked firstUpgradeVersion = this.args.model.versionHistory[0].id; // return 1.9.0 or earliest upgrade post 1.9.0
|
||||
@tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled; // returns RFC3339 timestamp
|
||||
|
||||
get startTimeDisplay() {
|
||||
if (!this.startTimeFromResponse) {
|
||||
// otherwise will return date of new Date(null)
|
||||
return null;
|
||||
}
|
||||
let month = this.startTimeFromResponse[1];
|
||||
let year = this.startTimeFromResponse[0];
|
||||
return `${this.arrayOfMonths[month]} ${year}`;
|
||||
}
|
||||
|
||||
get endTimeDisplay() {
|
||||
if (!this.endTimeFromResponse) {
|
||||
// otherwise will return date of new Date(null)
|
||||
return null;
|
||||
}
|
||||
let month = this.endTimeFromResponse[1];
|
||||
let year = this.endTimeFromResponse[0];
|
||||
return `${this.arrayOfMonths[month]} ${year}`;
|
||||
}
|
||||
|
||||
get isDateRange() {
|
||||
return !isSameMonth(
|
||||
new Date(this.args.model.activity.startTime),
|
||||
new Date(this.args.model.activity.endTime)
|
||||
);
|
||||
}
|
||||
|
||||
// API client count data by namespace for date range
|
||||
get byNamespaceActivity() {
|
||||
return this.args.model.activity?.byNamespace || null;
|
||||
}
|
||||
|
||||
// top level TOTAL client counts for given date range
|
||||
get totalUsageCounts() {
|
||||
return this.selectedNamespace
|
||||
? this.filterByNamespace(this.selectedNamespace)
|
||||
: this.args.model.activity?.total;
|
||||
}
|
||||
|
||||
// total client data for horizontal bar chart in attribution component
|
||||
get totalClientsData() {
|
||||
if (this.selectedNamespace) {
|
||||
let filteredNamespace = this.filterByNamespace(this.selectedNamespace);
|
||||
return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null;
|
||||
} else {
|
||||
return this.byNamespaceActivity;
|
||||
}
|
||||
}
|
||||
|
||||
get responseTimestamp() {
|
||||
return this.args.model.activity?.responseTimestamp;
|
||||
}
|
||||
|
||||
get countsIncludeOlderData() {
|
||||
let firstUpgrade = this.args.model.versionHistory[0];
|
||||
if (!firstUpgrade) {
|
||||
return false;
|
||||
}
|
||||
let versionDate = new Date(firstUpgrade.timestampInstalled);
|
||||
// compare against this startTimeFromResponse to show message or not.
|
||||
return isAfter(versionDate, new Date(this.startTimeFromResponse)) ? versionDate : false;
|
||||
}
|
||||
// HELPERS
|
||||
areArraysTheSame(a1, a2) {
|
||||
return (
|
||||
a1 === a2 ||
|
||||
(a1 !== null &&
|
||||
a2 !== null &&
|
||||
a1.length === a2.length &&
|
||||
a1
|
||||
.map(function (val, idx) {
|
||||
return val === a2[idx];
|
||||
})
|
||||
.reduce(function (prev, cur) {
|
||||
return prev && cur;
|
||||
}, true))
|
||||
);
|
||||
}
|
||||
|
||||
// ACTIONS
|
||||
@action
|
||||
async handleClientActivityQuery(month, year, dateType) {
|
||||
if (dateType === 'cancel') {
|
||||
return;
|
||||
}
|
||||
// clicked "Current Billing period" in the calendar widget
|
||||
if (dateType === 'reset') {
|
||||
this.startTimeRequested = this.args.model.startTimeFromLicense;
|
||||
this.endTimeRequested = null;
|
||||
}
|
||||
// clicked "Edit" Billing start month in Dashboard which opens a modal.
|
||||
if (dateType === 'startTime') {
|
||||
let monthIndex = this.arrayOfMonths.indexOf(month);
|
||||
this.startTimeRequested = [year.toString(), monthIndex]; // ['2021', 0] (e.g. January 2021) // TODO CHANGE TO ARRAY
|
||||
this.endTimeRequested = null;
|
||||
}
|
||||
// clicked "Custom End Month" from the calendar-widget
|
||||
if (dateType === 'endTime') {
|
||||
// use the currently selected startTime for your startTimeRequested.
|
||||
this.startTimeRequested = this.startTimeFromResponse;
|
||||
this.endTimeRequested = [year.toString(), month]; // endTime comes in as a number/index whereas startTime comes in as a month name. Hence the difference between monthIndex and month.
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await this.store.queryRecord('clients/activity', {
|
||||
start_time: this.startTimeRequested,
|
||||
end_time: this.endTimeRequested,
|
||||
});
|
||||
if (response.id === 'no-data') {
|
||||
// empty response is the only time we want to update the displayed date with the requested time
|
||||
this.startTimeFromResponse = this.startTimeRequested;
|
||||
this.noActivityDate = this.startTimeDisplay;
|
||||
} else {
|
||||
// note: this.startTimeDisplay (getter) is updated by this.startTimeFromResponse
|
||||
this.startTimeFromResponse = response.formattedStartTime;
|
||||
this.endTimeFromResponse = response.formattedEndTime;
|
||||
}
|
||||
// compare if the response and what you requested are the same. If they are not throw a warning.
|
||||
// this only gets triggered if the data was returned, which does not happen if the user selects a startTime after for which we have data. That's an adapter error and is captured differently.
|
||||
if (!this.areArraysTheSame(this.startTimeFromResponse, this.startTimeRequested)) {
|
||||
this.responseRangeDiffMessage = `You requested data from ${month} ${year}. We only have data from ${this.startTimeDisplay}, and that is what is being shown here.`;
|
||||
} else {
|
||||
this.responseRangeDiffMessage = null;
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
// ARG TODO handle error
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleCurrentBillingPeriod() {
|
||||
this.handleClientActivityQuery(0, 0, 'reset');
|
||||
}
|
||||
|
||||
@action
|
||||
selectNamespace([value]) {
|
||||
// value comes in as [namespace0]
|
||||
this.selectedNamespace = value;
|
||||
}
|
||||
|
||||
@action
|
||||
selectStartMonth(month) {
|
||||
this.startMonth = month;
|
||||
}
|
||||
|
||||
@action
|
||||
selectStartYear(year) {
|
||||
this.startYear = year;
|
||||
}
|
||||
|
||||
// HELPERS
|
||||
filterByNamespace(namespace) {
|
||||
return this.byNamespaceActivity.find((ns) => ns.label === namespace);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export default class HistoryComponent extends Component {
|
||||
max_namespaces = 10;
|
||||
|
||||
@tracked selectedNamespace = null;
|
||||
|
||||
@tracked barChartSelection = false;
|
||||
|
||||
// Determine if we have client count data based on the current tab
|
||||
get hasClientData() {
|
||||
if (this.args.tab === 'current') {
|
||||
// Show the current numbers as long as config is on
|
||||
return this.args.model.config?.enabled !== 'Off';
|
||||
}
|
||||
return this.args.model.activity && this.args.model.activity.total;
|
||||
}
|
||||
|
||||
// Show namespace graph only if we have more than 1
|
||||
get showGraphs() {
|
||||
return (
|
||||
this.args.model.activity &&
|
||||
this.args.model.activity.byNamespace &&
|
||||
this.args.model.activity.byNamespace.length > 1
|
||||
);
|
||||
}
|
||||
|
||||
// Construct the namespace model for the search select component
|
||||
get searchDataset() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
let dataList = this.args.model.activity.byNamespace;
|
||||
return dataList.map((d) => {
|
||||
return {
|
||||
name: d['namespace_id'],
|
||||
id: d['namespace_path'] === '' ? 'root' : d['namespace_path'],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Construct the namespace model for the bar chart component
|
||||
get barChartDataset() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
let dataset = this.args.model.activity.byNamespace.slice(0, this.max_namespaces);
|
||||
return dataset.map((d) => {
|
||||
return {
|
||||
label: d['namespace_path'] === '' ? 'root' : d['namespace_path'],
|
||||
non_entity_tokens: d['counts']['non_entity_tokens'],
|
||||
distinct_entities: d['counts']['distinct_entities'],
|
||||
total: d['counts']['clients'],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Create namespaces data for csv format
|
||||
get getCsvData() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
let results = '',
|
||||
namespaces = this.args.model.activity.byNamespace,
|
||||
fields = ['Namespace path', 'Active clients', 'Unique entities', 'Non-entity tokens'];
|
||||
|
||||
results = fields.join(',') + '\n';
|
||||
|
||||
namespaces.forEach(function (item) {
|
||||
let path = item.namespace_path !== '' ? item.namespace_path : 'root',
|
||||
total = item.counts.clients,
|
||||
unique = item.counts.distinct_entities,
|
||||
non_entity = item.counts.non_entity_tokens;
|
||||
|
||||
results += path + ',' + total + ',' + unique + ',' + non_entity + '\n';
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
// Return csv filename with start and end dates
|
||||
get getCsvFileName() {
|
||||
let defaultFileName = `clients-by-namespace`,
|
||||
startDate =
|
||||
this.args.model.queryStart || `${format(new Date(this.args.model.activity.startTime), 'MM-yyyy')}`,
|
||||
endDate =
|
||||
this.args.model.queryEnd || `${format(new Date(this.args.model.activity.endTime), 'MM-yyyy')}`;
|
||||
if (startDate && endDate) {
|
||||
defaultFileName += `-${startDate}-${endDate}`;
|
||||
}
|
||||
return defaultFileName;
|
||||
}
|
||||
|
||||
// Get the namespace by matching the path from the namespace list
|
||||
getNamespace(path) {
|
||||
return this.args.model.activity.byNamespace.find((ns) => {
|
||||
if (path === 'root') {
|
||||
return ns.namespace_path === '';
|
||||
}
|
||||
return ns.namespace_path === path;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
selectNamespace(value) {
|
||||
// In case of search select component, value returned is an array
|
||||
if (Array.isArray(value)) {
|
||||
this.selectedNamespace = this.getNamespace(value[0]);
|
||||
this.barChartSelection = false;
|
||||
} else if (typeof value === 'object') {
|
||||
// While D3 bar selection returns an object
|
||||
this.selectedNamespace = this.getNamespace(value.label);
|
||||
this.barChartSelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
resetData() {
|
||||
this.barChartSelection = false;
|
||||
this.selectedNamespace = null;
|
||||
}
|
||||
}
|
|
@ -1,124 +1,212 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { format } from 'date-fns';
|
||||
import { isSameMonth, isAfter } from 'date-fns';
|
||||
|
||||
export default class HistoryComponent extends Component {
|
||||
max_namespaces = 10;
|
||||
export default class History extends Component {
|
||||
// TODO CMB alphabetize and delete unused vars (particularly @tracked)
|
||||
arrayOfMonths = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
@tracked selectedNamespace = null;
|
||||
chartLegend = [
|
||||
{ key: 'entity_clients', label: 'entity clients' },
|
||||
{ key: 'non_entity_clients', label: 'non-entity clients' },
|
||||
];
|
||||
|
||||
// needed for startTime modal picker
|
||||
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;
|
||||
});
|
||||
|
||||
@service store;
|
||||
|
||||
@tracked queriedActivityResponse = null;
|
||||
@tracked barChartSelection = 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 startYear = null;
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked noActivityDate = '';
|
||||
@tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => {
|
||||
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
|
||||
|
||||
// Determine if we have client count data based on the current tab
|
||||
get hasClientData() {
|
||||
if (this.args.tab === 'current') {
|
||||
// Show the current numbers as long as config is on
|
||||
return this.args.model.config?.enabled !== 'Off';
|
||||
}
|
||||
return this.args.model.activity && this.args.model.activity.total;
|
||||
// on init API response uses license start_date, getter updates when user queries dates
|
||||
get getActivityResponse() {
|
||||
return this.queriedActivityResponse || this.args.model.activity;
|
||||
}
|
||||
|
||||
// Show namespace graph only if we have more than 1
|
||||
get showGraphs() {
|
||||
return (
|
||||
this.args.model.activity &&
|
||||
this.args.model.activity.byNamespace &&
|
||||
this.args.model.activity.byNamespace.length > 1
|
||||
get startTimeDisplay() {
|
||||
if (!this.startTimeFromResponse) {
|
||||
// otherwise will return date of new Date(null)
|
||||
return null;
|
||||
}
|
||||
let month = this.startTimeFromResponse[1];
|
||||
let year = this.startTimeFromResponse[0];
|
||||
return `${this.arrayOfMonths[month]} ${year}`;
|
||||
}
|
||||
|
||||
get endTimeDisplay() {
|
||||
if (!this.endTimeFromResponse) {
|
||||
// otherwise will return date of new Date(null)
|
||||
return null;
|
||||
}
|
||||
let month = this.endTimeFromResponse[1];
|
||||
let year = this.endTimeFromResponse[0];
|
||||
return `${this.arrayOfMonths[month]} ${year}`;
|
||||
}
|
||||
|
||||
get isDateRange() {
|
||||
return !isSameMonth(
|
||||
new Date(this.getActivityResponse.startTime),
|
||||
new Date(this.getActivityResponse.endTime)
|
||||
);
|
||||
}
|
||||
|
||||
// Construct the namespace model for the search select component
|
||||
get searchDataset() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
let dataList = this.args.model.activity.byNamespace;
|
||||
return dataList.map((d) => {
|
||||
return {
|
||||
name: d['namespace_id'],
|
||||
id: d['namespace_path'] === '' ? 'root' : d['namespace_path'],
|
||||
};
|
||||
});
|
||||
// top level TOTAL client counts for given date range
|
||||
get totalUsageCounts() {
|
||||
return this.selectedNamespace
|
||||
? this.filterByNamespace(this.selectedNamespace)
|
||||
: this.getActivityResponse.total;
|
||||
}
|
||||
|
||||
// Construct the namespace model for the bar chart component
|
||||
get barChartDataset() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
// total client data for horizontal bar chart in attribution component
|
||||
get totalClientsData() {
|
||||
if (this.selectedNamespace) {
|
||||
let filteredNamespace = this.filterByNamespace(this.selectedNamespace);
|
||||
return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null;
|
||||
} else {
|
||||
return this.getActivityResponse?.byNamespace;
|
||||
}
|
||||
let dataset = this.args.model.activity.byNamespace.slice(0, this.max_namespaces);
|
||||
return dataset.map((d) => {
|
||||
return {
|
||||
label: d['namespace_path'] === '' ? 'root' : d['namespace_path'],
|
||||
non_entity_tokens: d['counts']['non_entity_tokens'],
|
||||
distinct_entities: d['counts']['distinct_entities'],
|
||||
total: d['counts']['clients'],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Create namespaces data for csv format
|
||||
get getCsvData() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
let results = '',
|
||||
namespaces = this.args.model.activity.byNamespace,
|
||||
fields = ['Namespace path', 'Active clients', 'Unique entities', 'Non-entity tokens'];
|
||||
|
||||
results = fields.join(',') + '\n';
|
||||
|
||||
namespaces.forEach(function (item) {
|
||||
let path = item.namespace_path !== '' ? item.namespace_path : 'root',
|
||||
total = item.counts.clients,
|
||||
unique = item.counts.distinct_entities,
|
||||
non_entity = item.counts.non_entity_tokens;
|
||||
|
||||
results += path + ',' + total + ',' + unique + ',' + non_entity + '\n';
|
||||
});
|
||||
return results;
|
||||
get responseTimestamp() {
|
||||
return this.getActivityResponse.responseTimestamp;
|
||||
}
|
||||
|
||||
// Return csv filename with start and end dates
|
||||
get getCsvFileName() {
|
||||
let defaultFileName = `clients-by-namespace`,
|
||||
startDate =
|
||||
this.args.model.queryStart || `${format(new Date(this.args.model.activity.startTime), 'MM-yyyy')}`,
|
||||
endDate =
|
||||
this.args.model.queryEnd || `${format(new Date(this.args.model.activity.endTime), 'MM-yyyy')}`;
|
||||
if (startDate && endDate) {
|
||||
defaultFileName += `-${startDate}-${endDate}`;
|
||||
get countsIncludeOlderData() {
|
||||
let firstUpgrade = this.args.model.versionHistory[0];
|
||||
if (!firstUpgrade) {
|
||||
return false;
|
||||
}
|
||||
return defaultFileName;
|
||||
let versionDate = new Date(firstUpgrade.timestampInstalled);
|
||||
// compare against this startTimeFromResponse to show message or not.
|
||||
return isAfter(versionDate, new Date(this.startTimeFromResponse)) ? versionDate : false;
|
||||
}
|
||||
|
||||
// Get the namespace by matching the path from the namespace list
|
||||
getNamespace(path) {
|
||||
return this.args.model.activity.byNamespace.find((ns) => {
|
||||
if (path === 'root') {
|
||||
return ns.namespace_path === '';
|
||||
// HELPERS
|
||||
areArraysTheSame(a1, a2) {
|
||||
return (
|
||||
a1 === a2 ||
|
||||
(a1 !== null &&
|
||||
a2 !== null &&
|
||||
a1.length === a2.length &&
|
||||
a1
|
||||
.map(function (val, idx) {
|
||||
return val === a2[idx];
|
||||
})
|
||||
.reduce(function (prev, cur) {
|
||||
return prev && cur;
|
||||
}, true))
|
||||
);
|
||||
}
|
||||
|
||||
// ACTIONS
|
||||
@action
|
||||
async handleClientActivityQuery(month, year, dateType) {
|
||||
if (dateType === 'cancel') {
|
||||
return;
|
||||
}
|
||||
// clicked "Current Billing period" in the calendar widget
|
||||
if (dateType === 'reset') {
|
||||
this.startTimeRequested = this.args.model.startTimeFromLicense;
|
||||
this.endTimeRequested = null;
|
||||
}
|
||||
// clicked "Edit" Billing start month in Dashboard which opens a modal.
|
||||
if (dateType === 'startTime') {
|
||||
let monthIndex = this.arrayOfMonths.indexOf(month);
|
||||
this.startTimeRequested = [year.toString(), monthIndex]; // ['2021', 0] (e.g. January 2021) // TODO CHANGE TO ARRAY
|
||||
this.endTimeRequested = null;
|
||||
}
|
||||
// clicked "Custom End Month" from the calendar-widget
|
||||
if (dateType === 'endTime') {
|
||||
// use the currently selected startTime for your startTimeRequested.
|
||||
this.startTimeRequested = this.startTimeFromResponse;
|
||||
this.endTimeRequested = [year.toString(), month]; // endTime comes in as a number/index whereas startTime comes in as a month name. Hence the difference between monthIndex and month.
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await this.store.queryRecord('clients/activity', {
|
||||
start_time: this.startTimeRequested,
|
||||
end_time: this.endTimeRequested,
|
||||
});
|
||||
if (response.id === 'no-data') {
|
||||
// empty response is the only time we want to update the displayed date with the requested time
|
||||
this.startTimeFromResponse = this.startTimeRequested;
|
||||
this.noActivityDate = this.startTimeDisplay;
|
||||
} else {
|
||||
// note: this.startTimeDisplay (getter) is updated by this.startTimeFromResponse
|
||||
this.startTimeFromResponse = response.formattedStartTime;
|
||||
this.endTimeFromResponse = response.formattedEndTime;
|
||||
}
|
||||
return ns.namespace_path === path;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
selectNamespace(value) {
|
||||
// In case of search select component, value returned is an array
|
||||
if (Array.isArray(value)) {
|
||||
this.selectedNamespace = this.getNamespace(value[0]);
|
||||
this.barChartSelection = false;
|
||||
} else if (typeof value === 'object') {
|
||||
// While D3 bar selection returns an object
|
||||
this.selectedNamespace = this.getNamespace(value.label);
|
||||
this.barChartSelection = true;
|
||||
// compare if the response and what you requested are the same. If they are not throw a warning.
|
||||
// this only gets triggered if the data was returned, which does not happen if the user selects a startTime after for which we have data. That's an adapter error and is captured differently.
|
||||
if (!this.areArraysTheSame(this.startTimeFromResponse, this.startTimeRequested)) {
|
||||
this.responseRangeDiffMessage = `You requested data from ${month} ${year}. We only have data from ${this.startTimeDisplay}, and that is what is being shown here.`;
|
||||
} else {
|
||||
this.responseRangeDiffMessage = null;
|
||||
}
|
||||
this.queriedActivityResponse = response;
|
||||
} catch (e) {
|
||||
// ARG TODO handle error
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
resetData() {
|
||||
this.barChartSelection = false;
|
||||
this.selectedNamespace = null;
|
||||
handleCurrentBillingPeriod() {
|
||||
this.handleClientActivityQuery(0, 0, 'reset');
|
||||
}
|
||||
|
||||
@action
|
||||
selectNamespace([value]) {
|
||||
// value comes in as [namespace0]
|
||||
this.selectedNamespace = value;
|
||||
}
|
||||
|
||||
@action
|
||||
selectStartMonth(month) {
|
||||
this.startMonth = month;
|
||||
}
|
||||
|
||||
@action
|
||||
selectStartYear(year) {
|
||||
this.startYear = year;
|
||||
}
|
||||
|
||||
// HELPERS
|
||||
filterByNamespace(namespace) {
|
||||
return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ConfigController extends Controller {}
|
|
@ -0,0 +1,3 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default class HistoryController extends Controller {}
|
|
@ -1,8 +1,3 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ClientsController extends Controller {
|
||||
queryParams = ['tab', 'start', 'end']; // ARG TODO remove
|
||||
tab = null;
|
||||
start = null;
|
||||
end = null;
|
||||
}
|
||||
export default class ClientsController extends Controller {}
|
||||
|
|
|
@ -18,7 +18,8 @@ Router.map(function () {
|
|||
this.mount('open-api-explorer', { path: '/api-explorer' });
|
||||
this.route('license');
|
||||
this.route('clients', function () {
|
||||
this.route('index', { path: '/' });
|
||||
this.route('history');
|
||||
this.route('config');
|
||||
this.route('edit');
|
||||
});
|
||||
this.route('storage', { path: '/storage/raft' });
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class ConfigRoute extends Route {
|
||||
model() {
|
||||
return this.store.queryRecord('clients/config', {});
|
||||
}
|
||||
@action
|
||||
async loading(transition) {
|
||||
// eslint-disable-next-line ember/no-controller-access-in-routes
|
||||
let controller = this.controllerFor('vault.cluster.clients.config');
|
||||
if (controller) {
|
||||
controller.currentlyLoading = true;
|
||||
transition.promise.finally(function () {
|
||||
controller.currentlyLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import RSVP from 'rsvp';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class HistoryRoute extends Route {
|
||||
async getActivity(start_time) {
|
||||
try {
|
||||
return this.store.queryRecord('clients/activity', { start_time });
|
||||
} catch (e) {
|
||||
// ARG TODO handle
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
async getLicense() {
|
||||
try {
|
||||
return this.store.queryRecord('license', {});
|
||||
} catch (e) {
|
||||
// ARG TODO handle
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
async getVersionHistory() {
|
||||
try {
|
||||
let arrayOfModels = [];
|
||||
let response = await this.store.findAll('clients/version-history'); // returns a class with nested models
|
||||
response.forEach((model) => {
|
||||
arrayOfModels.push({
|
||||
id: model.id,
|
||||
perviousVersion: model.previousVersion,
|
||||
timestampInstalled: model.timestampInstalled,
|
||||
});
|
||||
});
|
||||
return arrayOfModels;
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
parseRFC3339(timestamp) {
|
||||
// convert '2021-03-21T00:00:00Z' --> ['2021', 2] (e.g. 2021 March, month is zero indexed)
|
||||
return timestamp
|
||||
? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1]
|
||||
: null;
|
||||
}
|
||||
|
||||
async model() {
|
||||
let config = await this.store.queryRecord('clients/config', {}).catch((e) => {
|
||||
console.debug(e);
|
||||
// swallowing error so activity can show if no config permissions
|
||||
return {};
|
||||
});
|
||||
let license = await this.getLicense();
|
||||
let activity = await this.getActivity(license.startTime);
|
||||
|
||||
return RSVP.hash({
|
||||
config,
|
||||
activity,
|
||||
startTimeFromLicense: this.parseRFC3339(license.startTime),
|
||||
endTimeFromResponse: activity ? this.parseRFC3339(activity.endTime) : null,
|
||||
versionHistory: this.getVersionHistory(),
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async loading(transition) {
|
||||
// eslint-disable-next-line ember/no-controller-access-in-routes
|
||||
let controller = this.controllerFor('vault.cluster.clients.history');
|
||||
if (controller) {
|
||||
controller.currentlyLoading = true;
|
||||
transition.promise.finally(function () {
|
||||
controller.currentlyLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +1,8 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
queryParams: {
|
||||
tab: {
|
||||
refreshModel: true,
|
||||
},
|
||||
start: {
|
||||
refreshModel: true,
|
||||
},
|
||||
end: {
|
||||
refreshModel: true,
|
||||
},
|
||||
},
|
||||
|
||||
async getActivity(start_time) {
|
||||
try {
|
||||
return this.store.queryRecord('clients/activity', { start_time });
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
import RSVP from 'rsvp';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class ClientsRoute extends Route {
|
||||
async getVersionHistory() {
|
||||
try {
|
||||
let arrayOfModels = [];
|
||||
|
@ -36,66 +16,34 @@ export default Route.extend(ClusterRoute, {
|
|||
});
|
||||
return arrayOfModels;
|
||||
} catch (e) {
|
||||
return null;
|
||||
console.debug(e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async getLicense() {
|
||||
try {
|
||||
return await this.store.queryRecord('license', {});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
|
||||
async getMonthly() {
|
||||
try {
|
||||
return await this.store.queryRecord('clients/monthly', {});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
|
||||
rfc33395ToMonthYear(timestamp) {
|
||||
// return ['2021', 2] (e.g. 2021 March, make 0-indexed)
|
||||
return timestamp
|
||||
? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1]
|
||||
: null;
|
||||
},
|
||||
}
|
||||
|
||||
async model() {
|
||||
let config = 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
|
||||
return {};
|
||||
});
|
||||
|
||||
let license = await this.getLicense(); // get default start_time
|
||||
let activity = await this.getActivity(license.startTime); // returns client counts using license start_time.
|
||||
let monthly = await this.getMonthly(); // returns the partial month endpoint
|
||||
let versionHistory = await this.getVersionHistory();
|
||||
let endTimeFromResponse = activity ? this.rfc33395ToMonthYear(activity.endTime) : null;
|
||||
let startTimeFromLicense = this.rfc33395ToMonthYear(license.startTime);
|
||||
|
||||
return hash({
|
||||
// ARG TODO will remove "hash" once remove "activity," which currently relies on it.
|
||||
activity,
|
||||
monthly,
|
||||
return RSVP.hash({
|
||||
config,
|
||||
endTimeFromResponse,
|
||||
startTimeFromLicense,
|
||||
versionHistory,
|
||||
monthly: await this.store.queryRecord('clients/monthly', {}),
|
||||
versionHistory: this.getVersionHistory(),
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
actions: {
|
||||
loading(transition) {
|
||||
// eslint-disable-next-line ember/no-controller-access-in-routes
|
||||
let controller = this.controllerFor('vault.cluster.clients.index');
|
||||
controller.set('currentlyLoading', true);
|
||||
@action
|
||||
async loading(transition) {
|
||||
// eslint-disable-next-line ember/no-controller-access-in-routes
|
||||
let controller = this.controllerFor('vault.cluster.clients.index');
|
||||
if (controller) {
|
||||
controller.currentlyLoading = true;
|
||||
transition.promise.finally(function () {
|
||||
controller.set('currentlyLoading', false);
|
||||
controller.currentlyLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ export default class ActivitySerializer extends ApplicationSerializer {
|
|||
return object;
|
||||
}
|
||||
|
||||
rfc33395ToMonthYear(timestamp) {
|
||||
// return ['2021', 2] (e.g. 2021 March, make 0-indexed)
|
||||
parseRFC3339(timestamp) {
|
||||
// convert '2021-03-21T00:00:00Z' --> ['2021', 2] (e.g. 2021 March, month is zero indexed)
|
||||
return timestamp
|
||||
? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1]
|
||||
: null;
|
||||
|
@ -65,8 +65,8 @@ export default class ActivitySerializer extends ApplicationSerializer {
|
|||
response_timestamp,
|
||||
by_namespace: this.flattenDataset(payload.data.by_namespace),
|
||||
total: this.homogenizeClientNaming(payload.data.total),
|
||||
formatted_end_time: this.rfc33395ToMonthYear(payload.data.end_time),
|
||||
formatted_start_time: this.rfc33395ToMonthYear(payload.data.start_time),
|
||||
formatted_end_time: this.parseRFC3339(payload.data.end_time),
|
||||
formatted_start_time: this.parseRFC3339(payload.data.start_time),
|
||||
};
|
||||
delete payload.data.by_namespace;
|
||||
delete payload.data.total;
|
||||
|
@ -81,7 +81,6 @@ payload.data.by_namespace = [
|
|||
{
|
||||
namespace_id: '5SWT8',
|
||||
namespace_path: 'namespacelonglonglong4/',
|
||||
_comment1: 'client counts are nested within own object',
|
||||
counts: {
|
||||
entity_clients: 171,
|
||||
non_entity_clients: 20,
|
||||
|
@ -103,7 +102,6 @@ payload.data.by_namespace = [
|
|||
transformedPayload.by_namespace = [
|
||||
{
|
||||
label: 'namespacelonglonglong4/',
|
||||
_comment2: 'remove nested object',
|
||||
entity_clients: 171,
|
||||
non_entity_clients: 20,
|
||||
clients: 191,
|
||||
|
|
|
@ -49,6 +49,9 @@ export default class MonthlySerializer extends ApplicationSerializer {
|
|||
}
|
||||
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
if (payload.id === 'no-data') {
|
||||
return super.normalizeResponse(store, primaryModelClass, payload, id, requestType);
|
||||
}
|
||||
let response_timestamp = formatISO(new Date());
|
||||
let transformedPayload = {
|
||||
...payload,
|
||||
|
|
|
@ -44,3 +44,29 @@
|
|||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tab-link {
|
||||
color: $grey;
|
||||
font-weight: $font-weight-semibold;
|
||||
text-decoration: none;
|
||||
padding: $size-6 $size-8 $size-8;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: background-color $speed, border-color $speed;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
border-color: $grey-light;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $ui-gray-050;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
&.is-active {
|
||||
border-color: $blue !important;
|
||||
color: $blue !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,16 @@
|
|||
{{#if (eq @mode "edit")}}
|
||||
<form onsubmit={{action "onSaveChanges"}} data-test-pricing-metrics-config-form>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{@model}} @errorMessage={{this.error}} />
|
||||
{{#each @model.configAttrs as |attr|}}
|
||||
{{#if (and (eq attr.type "string") (eq attr.options.editType "boolean"))}}
|
||||
<label class="is-label">Usage data collection</label>
|
||||
{{#if attr.options.helpText}}
|
||||
<p class="sub-text">
|
||||
{{attr.options.helpText}}
|
||||
{{#if attr.options.docLink}}
|
||||
<a href={{attr.options.docLink}} target="_blank" rel="noopener noreferrer">
|
||||
See our documentation
|
||||
</a>
|
||||
for help.
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<div class="control is-flex has-bottom-margin-l">
|
||||
<input
|
||||
data-test-field
|
||||
type="checkbox"
|
||||
id={{attr.name}}
|
||||
name={{attr.name}}
|
||||
class="switch is-rounded is-success is-small"
|
||||
checked={{eq (get @model attr.name) attr.options.trueValue}}
|
||||
onchange={{action (action "updateBooleanValue" attr) value="target.checked"}}
|
||||
/>
|
||||
<label for={{attr.name}}>
|
||||
{{#if (eq @model.enabled "Off")}}
|
||||
Data collection is off
|
||||
{{else}}
|
||||
Data collection is on
|
||||
{{/if}}
|
||||
</label>
|
||||
</div>
|
||||
{{else if (eq attr.type "number")}}
|
||||
<div class="has-top-margin-s">
|
||||
<label for={{attr.name}} class="is-label">
|
||||
{{attr.options.label}}
|
||||
</label>
|
||||
{{#if attr.options.subText}}
|
||||
{{#if @isLoading}}
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
{{#if (eq @mode "edit")}}
|
||||
<form onsubmit={{action "onSaveChanges"}} data-test-pricing-metrics-config-form>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{@model}} @errorMessage={{this.error}} />
|
||||
{{#each @model.configAttrs as |attr|}}
|
||||
{{#if (and (eq attr.type "string") (eq attr.options.editType "boolean"))}}
|
||||
<label class="is-label">Usage data collection</label>
|
||||
{{#if attr.options.helpText}}
|
||||
<p class="sub-text">
|
||||
{{attr.options.subText}}
|
||||
{{attr.options.helpText}}
|
||||
{{#if attr.options.docLink}}
|
||||
<a href={{attr.options.docLink}} target="_blank" rel="noopener noreferrer">
|
||||
See our documentation
|
||||
|
@ -50,82 +19,117 @@
|
|||
{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<div class="control">
|
||||
<div class="control is-flex has-bottom-margin-l">
|
||||
<input
|
||||
data-test-field
|
||||
type="checkbox"
|
||||
id={{attr.name}}
|
||||
disabled={{eq @model.enabled "Off"}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
onchange={{action (mut (get @model attr.name)) value="target.value"}}
|
||||
value={{or (get @model attr.name) attr.options.defaultValue}}
|
||||
class="input"
|
||||
maxLength={{attr.options.characterLimit}}
|
||||
name={{attr.name}}
|
||||
class="switch is-rounded is-success is-small"
|
||||
checked={{eq (get @model attr.name) attr.options.trueValue}}
|
||||
onchange={{action (action "updateBooleanValue" attr) value="target.checked"}}
|
||||
/>
|
||||
<label for={{attr.name}}>
|
||||
{{#if (eq @model.enabled "Off")}}
|
||||
Data collection is off
|
||||
{{else}}
|
||||
Data collection is on
|
||||
{{/if}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{else if (eq attr.type "number")}}
|
||||
<div class="has-top-margin-s">
|
||||
<label for={{attr.name}} class="is-label">
|
||||
{{attr.options.label}}
|
||||
</label>
|
||||
{{#if attr.options.subText}}
|
||||
<p class="sub-text">
|
||||
{{attr.options.subText}}
|
||||
{{#if attr.options.docLink}}
|
||||
<a href={{attr.options.docLink}} target="_blank" rel="noopener noreferrer">
|
||||
See our documentation
|
||||
</a>
|
||||
for help.
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<div class="control">
|
||||
<input
|
||||
data-test-field
|
||||
id={{attr.name}}
|
||||
disabled={{eq @model.enabled "Off"}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
onchange={{action (mut (get @model attr.name)) value="target.value"}}
|
||||
value={{or (get @model attr.name) attr.options.defaultValue}}
|
||||
class="input"
|
||||
maxLength={{attr.options.characterLimit}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={{this.buttonDisabled}}
|
||||
class="button is-primary"
|
||||
data-test-edit-metrics-config-save={{true}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<LinkTo @route="vault.cluster.clients.config" class="button">
|
||||
Cancel
|
||||
</LinkTo>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Modal
|
||||
@title={{this.modalTitle}}
|
||||
@onClose={{action (mut this.modalOpen) false}}
|
||||
@isActive={{this.modalOpen}}
|
||||
@type="warning"
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
{{#if (eq @model.enabled "On")}}
|
||||
<p class="has-bottom-margin-s">
|
||||
Vault will start tracking data starting from today’s date,
|
||||
{{date-format (now) "d MMMM yyyy"}}. You will not be able to see or query usage until the end of the month.
|
||||
</p>
|
||||
<p>If you’ve previously enabled usage tracking, that historical data will still be available to you.</p>
|
||||
{{else}}
|
||||
<p class="has-bottom-margin-s">
|
||||
Turning usage tracking off means that all data for the current month will be deleted. You will still be able to
|
||||
query previous months.
|
||||
</p>
|
||||
<p>Are you sure?</p>
|
||||
{{/if}}
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut this.modalOpen) false}}
|
||||
data-test-metrics-config-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="button is-primary" onclick={{perform this.save}}>
|
||||
Continue
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
{{else}}
|
||||
<div
|
||||
class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless"
|
||||
data-test-pricing-metrics-config-table
|
||||
>
|
||||
{{#each this.infoRows as |item|}}
|
||||
<InfoTableRow @label={{item.label}} @helperText={{item.helperText}} @value={{get @model item.valueKey}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={{this.buttonDisabled}}
|
||||
class="button is-primary"
|
||||
data-test-edit-metrics-config-save={{true}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="config"}} class="button">
|
||||
Cancel
|
||||
</LinkTo>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Modal
|
||||
@title={{this.modalTitle}}
|
||||
@onClose={{action (mut this.modalOpen) false}}
|
||||
@isActive={{this.modalOpen}}
|
||||
@type="warning"
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
{{#if (eq @model.enabled "On")}}
|
||||
<p class="has-bottom-margin-s">
|
||||
Vault will start tracking data starting from today’s date,
|
||||
{{date-format (now) "d MMMM yyyy"}}. You will not be able to see or query usage until the end of the month.
|
||||
</p>
|
||||
<p>If you’ve previously enabled usage tracking, that historical data will still be available to you.</p>
|
||||
{{else}}
|
||||
<p class="has-bottom-margin-s">
|
||||
Turning usage tracking off means that all data for the current month will be deleted. You will still be able to
|
||||
query previous months.
|
||||
</p>
|
||||
<p>Are you sure?</p>
|
||||
{{/if}}
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-secondary"
|
||||
onclick={{action (mut this.modalOpen) false}}
|
||||
data-test-metrics-config-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="button is-primary" onclick={{perform this.save}}>
|
||||
Continue
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
{{else}}
|
||||
<div
|
||||
class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless"
|
||||
data-test-pricing-metrics-config-table
|
||||
>
|
||||
{{#each this.infoRows as |item|}}
|
||||
<InfoTableRow @label={{item.label}} @helperText={{item.helperText}} @value={{get @model item.valueKey}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -8,31 +8,29 @@
|
|||
@message="Tracking is disabled and data is not being collected. To turn it on edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
<LinkTo @route="vault.cluster.clients.config">
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
{{#if this.totalClientsData}}
|
||||
<div class="is-subtitle-gray has-bottom-margin-m">
|
||||
FILTERS
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<SearchSelect
|
||||
@id="namespace-search-select-monthly"
|
||||
@options={{this.namespaceArray}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.selectNamespace}}
|
||||
@placeholder={{"Filter by namespace"}}
|
||||
@displayInherit={{true}}
|
||||
/>
|
||||
</ToolbarFilters>
|
||||
</Toolbar>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="is-subtitle-gray has-bottom-margin-m">
|
||||
FILTERS
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<SearchSelect
|
||||
@id="namespace-search-select-monthly"
|
||||
@options={{this.namespaceArray}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.selectNamespace}}
|
||||
@placeholder={{"Filter by namespace"}}
|
||||
@displayInherit={{true}}
|
||||
/>
|
||||
</ToolbarFilters>
|
||||
</Toolbar>
|
||||
</div>
|
||||
{{#if this.countsIncludeOlderData}}
|
||||
<AlertBanner @type="warning" @title="Warning">
|
||||
{{concat "You upgraded to Vault " this.firstUpgradeVersion " on " (date-format this.upgradeDate "MMMM d, yyyy.")}}
|
||||
|
@ -46,20 +44,24 @@
|
|||
{{#if @isLoading}}
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
<Clients::UsageStats
|
||||
@title={{date-format this.responseTimestamp "MMMM"}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
/>
|
||||
{{#if this.totalClientsData}}
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientsData={{this.totalClientsData}}
|
||||
{{#if this.totalUsageCounts}}
|
||||
<Clients::UsageStats
|
||||
@title={{date-format this.responseTimestamp "MMMM"}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
|
||||
@isDateRange={{false}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{#if this.totalClientsData}}
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientsData={{this.totalClientsData}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
|
||||
@isDateRange={{false}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<EmptyState @title={{concat "No partial history"}} @message="There is no data in the current month yet." />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
|
|
@ -1,218 +1,26 @@
|
|||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
<p class="has-bottom-margin-xl">
|
||||
This data is presented by full month. If there is data missing, it’s possible that tracking was turned off at the time.
|
||||
Vault will only show data for contiguous blocks of time during which tracking was on. Documentation is available
|
||||
<DocLink @path="/docs/concepts/client-count">here</DocLink>.
|
||||
</p>
|
||||
<h1 data-test-client-count-title class="title is-6 has-bottom-margin-xs">
|
||||
Billing start month
|
||||
</h1>
|
||||
<div class="is-flex-align-baseline">
|
||||
<p class="is-size-6">{{this.startTimeDisplay}}</p>
|
||||
<button type="button" class="button is-link" {{on "click" (fn (mut this.isEditStartMonthOpen) true)}}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<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
|
||||
is not reliable.
|
||||
</p>
|
||||
{{#if (eq @model.config.queriesAvailable false)}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title={{concat "No monthly history " (if this.noActivityDate "from ") this.noActivityDate}}
|
||||
@message="There is no data in the monthly history yet. We collect it at the end of each month, so your data will be available on the first of next month."
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Data tracking is disabled"
|
||||
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<p>
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="config"}}>
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
</p>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (eq @model.config.enabled "Off")}}
|
||||
<AlertBanner data-test-tracking-disabled @type="warning" @title="Tracking is disabled">
|
||||
Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need to
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
edit the configuration
|
||||
</LinkTo>
|
||||
to enable tracking again.
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
{{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}}
|
||||
{{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}}
|
||||
<div class="is-subtitle-gray has-bottom-margin-m">
|
||||
FILTERS
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<CalendarWidget
|
||||
@arrayOfMonths={{this.arrayOfMonths}}
|
||||
@endTimeDisplay={{this.endTimeDisplay}}
|
||||
@endTimeFromResponse={{this.endTimeFromResponse}}
|
||||
@handleClientActivityQuery={{this.handleClientActivityQuery}}
|
||||
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
|
||||
@startTimeDisplay={{this.startTimeDisplay}}
|
||||
/>
|
||||
{{#if this.namespaceArray}}
|
||||
<SearchSelect
|
||||
@id="namespace-search-select"
|
||||
@options={{this.namespaceArray}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.selectNamespace}}
|
||||
@placeholder={{"Filter by namespace"}}
|
||||
@displayInherit={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
</ToolbarFilters>
|
||||
</Toolbar>
|
||||
</div>
|
||||
{{#if this.countsIncludeOlderData}}
|
||||
<AlertBanner @type="warning" @title="Warning">
|
||||
<ul>
|
||||
{{#if this.responseRangeDiffMessage}}
|
||||
<li>{{this.responseRangeDiffMessage}}</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
{{concat
|
||||
"You upgraded to Vault "
|
||||
this.firstUpgradeVersion
|
||||
" on "
|
||||
(date-format this.upgradeDate "MMMM d, yyyy.")
|
||||
}}
|
||||
How we count clients changed in 1.9, so please keep in mind when looking at the data below. Furthermore,
|
||||
namespace attribution is available only for 1.9 data.
|
||||
<DocLink
|
||||
@path="/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts"
|
||||
>
|
||||
Learn more here.
|
||||
</DocLink>
|
||||
</li>
|
||||
</ul>
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
{{#if @isLoading}}
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
<Clients::UsageStats @title="Total usage" @totalUsageCounts={{this.totalUsageCounts}} />
|
||||
{{#if this.totalClientsData}}
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientsData={{this.totalClientsData}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@startTimeDisplay={{this.startTimeDisplay}}
|
||||
@endTimeDisplay={{this.endTimeDisplay}}
|
||||
@isDateRange={{this.isDateRange}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@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."
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{! this dashboard template displays in three routes so @model varies slightly: index, history and config }}
|
||||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
Vault Client Count
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{! BILLING START DATE MODAL }}
|
||||
<Modal
|
||||
@title="Edit start month"
|
||||
@onClose={{action (mut this.isEditStartMonthOpen) false}}
|
||||
@isActive={{this.isEditStartMonthOpen}}
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
<p class="has-bottom-margin-s">
|
||||
This date comes from your license, and defines when client counting starts. Without this starting point, the data
|
||||
shown is not reliable.
|
||||
</p>
|
||||
<p class="has-bottom-margin-s"><strong>Billing contract start month</strong></p>
|
||||
<div class="modal-radio-button">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
onclick={{queue
|
||||
(action "handleClientActivityQuery" this.startMonth this.startYear "startTime")
|
||||
(action (mut this.isEditStartMonthOpen) false)
|
||||
}}
|
||||
disabled={{if (and this.startMonth this.startYear) false true}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-secondary"
|
||||
{{on
|
||||
"click"
|
||||
(queue (fn this.handleClientActivityQuery 0 0 "cancel") (action (mut this.isEditStartMonthOpen) false))
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
<div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless" data-test-pricing-metrics>
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
<LinkTo @route="vault.cluster.clients.index" class="nav-tab-link">
|
||||
Current month
|
||||
</LinkTo>
|
||||
<LinkTo @route="vault.cluster.clients.history" class="nav-tab-link">
|
||||
History
|
||||
</LinkTo>
|
||||
{{#if (or @model.config.configPath.canRead @model.configPath.canRead)}}
|
||||
<LinkTo @route="vault.cluster.clients.config" class="nav-tab-link">
|
||||
Configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
|
@ -0,0 +1,213 @@
|
|||
{{#if (and (eq @tab "history") (eq @model.config.queriesAvailable false))}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title="No monthly history"
|
||||
@message="There is no data in the monthly history yet. We collect it at the end of each month, so your data will be available on the first of next month."
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Data tracking is disabled"
|
||||
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<p>
|
||||
<LinkTo @route="vault.cluster.clients.config">
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
</p>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
{{#if (eq @tab "current")}}
|
||||
<h1 data-test-client-count-title class="title is-4 has-bottom-margin-s">
|
||||
Current month
|
||||
</h1>
|
||||
<p class="has-bottom-margin-s">
|
||||
The below data is for the current month starting from the first day. For historical data, see the monthly history
|
||||
tab.
|
||||
</p>
|
||||
{{#if (eq @model.config.enabled "Off")}}
|
||||
<EmptyState
|
||||
@title="Tracking is disabled"
|
||||
@message="Tracking is disabled and data is not being collected. To turn it on edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<LinkTo @route="vault.cluster.clients.config">
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (eq @model.config.enabled "Off")}}
|
||||
<AlertBanner data-test-tracking-disabled @type="warning" @title="Tracking is disabled">
|
||||
Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need
|
||||
to
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
edit the configuration
|
||||
</LinkTo>
|
||||
to enable tracking again.
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
<h1 data-test-client-count-title class="title is-4 has-bottom-margin-s">
|
||||
Monthly history
|
||||
</h1>
|
||||
<p class="has-bottom-margin-s">
|
||||
This data is presented by full month. If there is data missing, it's possible that tracking was turned off at the
|
||||
time. Vault will only show data for contiguous blocks of time during which tracking was on.
|
||||
</p>
|
||||
<PricingMetricsDates
|
||||
@queryStart={{@model.queryStart}}
|
||||
@queryEnd={{@model.queryEnd}}
|
||||
@resultStart={{@model.activity.startTime}}
|
||||
@resultEnd={{@model.activity.endTime}}
|
||||
@defaultSpan={{or @model.config.defaultReportMonths 12}}
|
||||
@retentionMonths={{@model.config.retentionMonths}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @isLoading}}
|
||||
<LayoutLoading />
|
||||
{{else if this.hasClientData}}
|
||||
<div class="card has-bottom-margin-m">
|
||||
<div class="card-content">
|
||||
<div class="is-flex is-flex-center">
|
||||
<div class="is-flex-1">
|
||||
<h2 class="title is-5 is-marginless">
|
||||
Total usage
|
||||
</h2>
|
||||
<p class="sub-text">
|
||||
These totals are within this namespace and all its children.
|
||||
</p>
|
||||
</div>
|
||||
<LearnLink @path="/tutorials/vault/usage-metrics">
|
||||
Learn more
|
||||
</LearnLink>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="columns">
|
||||
<div class="column" data-test-client-count-stats>
|
||||
<StatText
|
||||
@label="Total active clients"
|
||||
@value={{or @model.activity.clients @model.activity.total.clients "0"}}
|
||||
@size="l"
|
||||
@subText="The sum of unique entities and non-entity tokens; Vault's primary billing metric."
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<StatText
|
||||
class="column"
|
||||
@label="Unique entities"
|
||||
@value={{or @model.activity.distinct_entities @model.activity.total.distinct_entities "0"}}
|
||||
@size="l"
|
||||
@subText="Representation of a particular user, client or application that created a token via login."
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<StatText
|
||||
class="column"
|
||||
@label="Non-entity tokens"
|
||||
@value={{or @model.activity.non_entity_tokens @model.activity.total.non_entity_tokens "0"}}
|
||||
@size="l"
|
||||
@subText="Tokens created via a method that is not associated with an entity."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.showGraphs}}
|
||||
<div class="columns has-bottom-margin-m" {{did-update this.resetData}} {{did-insert this.resetData}}>
|
||||
<div class="column is-two-thirds" data-test-client-count-graph>
|
||||
<BarChart
|
||||
@title="Top 10 Namespaces"
|
||||
@description="Each namespace's client count includes clients in child namespaces."
|
||||
@dataset={{this.barChartDataset}}
|
||||
@tooltipData={{or @model.activity.clients @model.activity.total.clients}}
|
||||
@onClick={{action this.selectNamespace}}
|
||||
@mapLegend={{array
|
||||
(hash key="non_entity_tokens" label="Non-entity tokens")
|
||||
(hash key="distinct_entities" label="Unique entities")
|
||||
}}
|
||||
>
|
||||
<DownloadCsv
|
||||
@label={{"Export all namespace data"}}
|
||||
@csvData={{this.getCsvData}}
|
||||
@fileName={{this.getCsvFileName}}
|
||||
/>
|
||||
</BarChart>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
{{#if (and this.barChartSelection this.selectedNamespace)}}
|
||||
<label class="title is-5 has-bottom-margin-m">Single namespace</label>
|
||||
<ul class="has-bottom-margin-l search-select-list">
|
||||
<li class="search-select-list-item">
|
||||
<div>
|
||||
{{or this.selectedNamespace.namespace_path "root"}}
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-ghost" {{action "resetData"}}>
|
||||
<Icon @name="trash" class="has-text-grey" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{{else}}
|
||||
<SearchSelect
|
||||
@id="namespaces"
|
||||
@labelClass="title is-5"
|
||||
@disallowNewItems={{true}}
|
||||
@onChange={{action this.selectNamespace}}
|
||||
@label="Single namespace"
|
||||
@options={{or this.searchDataset (array)}}
|
||||
@searchField="namespace_path"
|
||||
@selectLimit={{1}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.selectedNamespace}}
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<StatText @label="Active clients" @value={{this.selectedNamespace.counts.clients}} @size="l" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<StatText
|
||||
@label="Unique entities"
|
||||
@value={{this.selectedNamespace.counts.distinct_entities}}
|
||||
@size="m"
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<StatText
|
||||
@label="Non-entity tokens"
|
||||
@value={{this.selectedNamespace.counts.non_entity_tokens}}
|
||||
@size="m"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="No namespace selected"
|
||||
@message="Click on a namespace in the Top 10 chart or type its name in the box to view it's individual client counts."
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else if (eq @tab "current")}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title="No data received"
|
||||
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<EmptyState @title="No data received" @message="No data exists for that query period. Try searching again." />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,213 +1,222 @@
|
|||
{{#if (and (eq @tab "history") (eq @model.config.queriesAvailable false))}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title="No monthly history"
|
||||
@message="There is no data in the monthly history yet. We collect it at the end of each month, so your data will be available on the first of next month."
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Data tracking is disabled"
|
||||
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<p>
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="config"}}>
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
</p>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
{{#if (eq @tab "current")}}
|
||||
<h1 data-test-client-count-title class="title is-4 has-bottom-margin-s">
|
||||
Current month
|
||||
</h1>
|
||||
<p class="has-bottom-margin-s">
|
||||
The below data is for the current month starting from the first day. For historical data, see the monthly history
|
||||
tab.
|
||||
</p>
|
||||
{{#if (eq @model.config.enabled "Off")}}
|
||||
<EmptyState
|
||||
@title="Tracking is disabled"
|
||||
@message="Tracking is disabled and data is not being collected. To turn it on edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
<p class="has-bottom-margin-xl">
|
||||
This data is presented by full month. If there is data missing, it’s possible that tracking was turned off at the time.
|
||||
Vault will only show data for contiguous blocks of time during which tracking was on. Documentation is available
|
||||
<DocLink @path="/docs/concepts/client-count">here</DocLink>.
|
||||
</p>
|
||||
<h1 class="title is-6 has-bottom-margin-xs">
|
||||
Billing start month
|
||||
</h1>
|
||||
<div data-test-start-date-editor class="is-flex-align-baseline">
|
||||
<p class="is-size-6">{{this.startTimeDisplay}}</p>
|
||||
<button type="button" class="button is-link" {{on "click" (fn (mut this.isEditStartMonthOpen) true)}}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<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
|
||||
is not reliable.
|
||||
</p>
|
||||
{{#if (eq @model.config.queriesAvailable false)}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title={{concat "No monthly history " (if this.noActivityDate "from ") this.noActivityDate}}
|
||||
@message="There is no data in the monthly history yet. We collect it at the end of each month, so your data will be available on the first of next month."
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Data tracking is disabled"
|
||||
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<p>
|
||||
<LinkTo @route="vault.cluster.clients.config">
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (eq @model.config.enabled "Off")}}
|
||||
<AlertBanner data-test-tracking-disabled @type="warning" @title="Tracking is disabled">
|
||||
Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need
|
||||
to
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
edit the configuration
|
||||
</LinkTo>
|
||||
to enable tracking again.
|
||||
</p>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (eq @model.config.enabled "Off")}}
|
||||
<AlertBanner data-test-tracking-disabled @type="warning" @title="Tracking is disabled">
|
||||
Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need to
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
edit the configuration
|
||||
</LinkTo>
|
||||
to enable tracking again.
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
{{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}}
|
||||
{{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}}
|
||||
<div class="is-subtitle-gray has-bottom-margin-m">
|
||||
FILTERS
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<CalendarWidget
|
||||
@arrayOfMonths={{this.arrayOfMonths}}
|
||||
@endTimeDisplay={{this.endTimeDisplay}}
|
||||
@endTimeFromResponse={{this.endTimeFromResponse}}
|
||||
@handleClientActivityQuery={{this.handleClientActivityQuery}}
|
||||
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
|
||||
@startTimeDisplay={{this.startTimeDisplay}}
|
||||
/>
|
||||
{{#if this.namespaceArray}}
|
||||
<SearchSelect
|
||||
@id="namespace-search-select"
|
||||
@options={{this.namespaceArray}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.selectNamespace}}
|
||||
@placeholder={{"Filter by namespace"}}
|
||||
@displayInherit={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
</ToolbarFilters>
|
||||
</Toolbar>
|
||||
</div>
|
||||
{{#if this.countsIncludeOlderData}}
|
||||
<AlertBanner @type="warning" @title="Warning">
|
||||
<ul>
|
||||
{{#if this.responseRangeDiffMessage}}
|
||||
<li>{{this.responseRangeDiffMessage}}</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
{{concat
|
||||
"You upgraded to Vault "
|
||||
this.firstUpgradeVersion
|
||||
" on "
|
||||
(date-format this.upgradeDate "MMMM d, yyyy.")
|
||||
}}
|
||||
How we count clients changed in 1.9, so please keep in mind when looking at the data below. Furthermore,
|
||||
namespace attribution is available only for 1.9 data.
|
||||
<DocLink
|
||||
@path="/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts"
|
||||
>
|
||||
Learn more here.
|
||||
</DocLink>
|
||||
</li>
|
||||
</ul>
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
<h1 data-test-client-count-title class="title is-4 has-bottom-margin-s">
|
||||
Monthly history
|
||||
</h1>
|
||||
<p class="has-bottom-margin-s">
|
||||
This data is presented by full month. If there is data missing, it's possible that tracking was turned off at the
|
||||
time. Vault will only show data for contiguous blocks of time during which tracking was on.
|
||||
</p>
|
||||
<PricingMetricsDates
|
||||
@queryStart={{@model.queryStart}}
|
||||
@queryEnd={{@model.queryEnd}}
|
||||
@resultStart={{@model.activity.startTime}}
|
||||
@resultEnd={{@model.activity.endTime}}
|
||||
@defaultSpan={{or @model.config.defaultReportMonths 12}}
|
||||
@retentionMonths={{@model.config.retentionMonths}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @isLoading}}
|
||||
<LayoutLoading />
|
||||
{{else if this.hasClientData}}
|
||||
<div class="card has-bottom-margin-m">
|
||||
<div class="card-content">
|
||||
<div class="is-flex is-flex-center">
|
||||
<div class="is-flex-1">
|
||||
<h2 class="title is-5 is-marginless">
|
||||
Total usage
|
||||
</h2>
|
||||
<p class="sub-text">
|
||||
These totals are within this namespace and all its children.
|
||||
</p>
|
||||
</div>
|
||||
<LearnLink @path="/tutorials/vault/usage-metrics">
|
||||
Learn more
|
||||
</LearnLink>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="columns">
|
||||
<div class="column" data-test-client-count-stats>
|
||||
<StatText
|
||||
@label="Total active clients"
|
||||
@value={{or @model.activity.clients @model.activity.total.clients "0"}}
|
||||
@size="l"
|
||||
@subText="The sum of unique entities and non-entity tokens; Vault's primary billing metric."
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<StatText
|
||||
class="column"
|
||||
@label="Unique entities"
|
||||
@value={{or @model.activity.distinct_entities @model.activity.total.distinct_entities "0"}}
|
||||
@size="l"
|
||||
@subText="Representation of a particular user, client or application that created a token via login."
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<StatText
|
||||
class="column"
|
||||
@label="Non-entity tokens"
|
||||
@value={{or @model.activity.non_entity_tokens @model.activity.total.non_entity_tokens "0"}}
|
||||
@size="l"
|
||||
@subText="Tokens created via a method that is not associated with an entity."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.showGraphs}}
|
||||
<div class="columns has-bottom-margin-m" {{did-update this.resetData}} {{did-insert this.resetData}}>
|
||||
<div class="column is-two-thirds" data-test-client-count-graph>
|
||||
<BarChart
|
||||
@title="Top 10 Namespaces"
|
||||
@description="Each namespace's client count includes clients in child namespaces."
|
||||
@dataset={{this.barChartDataset}}
|
||||
@tooltipData={{or @model.activity.clients @model.activity.total.clients}}
|
||||
@onClick={{action this.selectNamespace}}
|
||||
@mapLegend={{array
|
||||
(hash key="non_entity_tokens" label="Non-entity tokens")
|
||||
(hash key="distinct_entities" label="Unique entities")
|
||||
}}
|
||||
>
|
||||
<DownloadCsv
|
||||
@label={{"Export all namespace data"}}
|
||||
@csvData={{this.getCsvData}}
|
||||
@fileName={{this.getCsvFileName}}
|
||||
/>
|
||||
</BarChart>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
{{#if (and this.barChartSelection this.selectedNamespace)}}
|
||||
<label class="title is-5 has-bottom-margin-m">Single namespace</label>
|
||||
<ul class="has-bottom-margin-l search-select-list">
|
||||
<li class="search-select-list-item">
|
||||
<div>
|
||||
{{or this.selectedNamespace.namespace_path "root"}}
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-ghost" {{action "resetData"}}>
|
||||
<Icon @name="trash" class="has-text-grey" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{{else}}
|
||||
<SearchSelect
|
||||
@id="namespaces"
|
||||
@labelClass="title is-5"
|
||||
@disallowNewItems={{true}}
|
||||
@onChange={{action this.selectNamespace}}
|
||||
@label="Single namespace"
|
||||
@options={{or this.searchDataset (array)}}
|
||||
@searchField="namespace_path"
|
||||
@selectLimit={{1}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.selectedNamespace}}
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<StatText @label="Active clients" @value={{this.selectedNamespace.counts.clients}} @size="l" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<StatText
|
||||
@label="Unique entities"
|
||||
@value={{this.selectedNamespace.counts.distinct_entities}}
|
||||
@size="m"
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<StatText
|
||||
@label="Non-entity tokens"
|
||||
@value={{this.selectedNamespace.counts.non_entity_tokens}}
|
||||
@size="m"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="No namespace selected"
|
||||
@message="Click on a namespace in the Top 10 chart or type its name in the box to view it's individual client counts."
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else if (eq @tab "current")}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title="No data received"
|
||||
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
|
||||
/>
|
||||
{{#if @isLoading}}
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
{{#if this.totalUsageCounts}}
|
||||
<Clients::UsageStats @title="Total usage" @totalUsageCounts={{this.totalUsageCounts}} />
|
||||
{{#if this.totalClientsData}}
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientsData={{this.totalClientsData}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@startTimeDisplay={{this.startTimeDisplay}}
|
||||
@endTimeDisplay={{this.endTimeDisplay}}
|
||||
@isDateRange={{this.isDateRange}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<EmptyState @title="No data received" @message="No data exists for that query period. Try searching again." />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<EmptyState @title="No data received" @message="No data exists for that query period. Try searching again." />
|
||||
<EmptyState
|
||||
@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."
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{! BILLING START DATE MODAL }}
|
||||
<Modal
|
||||
@title="Edit start month"
|
||||
@onClose={{action (mut this.isEditStartMonthOpen) false}}
|
||||
@isActive={{this.isEditStartMonthOpen}}
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
<p class="has-bottom-margin-s">
|
||||
This date comes from your license, and defines when client counting starts. Without this starting point, the data
|
||||
shown is not reliable.
|
||||
</p>
|
||||
<p class="has-bottom-margin-s"><strong>Billing contract start month</strong></p>
|
||||
<div class="modal-radio-button">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
onclick={{queue
|
||||
(action "handleClientActivityQuery" this.startMonth this.startYear "startTime")
|
||||
(action (mut this.isEditStartMonthOpen) false)
|
||||
}}
|
||||
disabled={{if (and this.startMonth this.startYear) false true}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-secondary"
|
||||
{{on
|
||||
"click"
|
||||
(queue (fn this.handleClientActivityQuery 0 0 "cancel") (action (mut this.isEditStartMonthOpen) false))
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
</div>
|
|
@ -1,4 +1,5 @@
|
|||
<svg
|
||||
data-test-horizontal-bar-chart
|
||||
class="chart is-horizontal"
|
||||
{{on "mouseleave" this.removeTooltip}}
|
||||
{{did-insert this.renderChart @dataset}}
|
||||
|
|
Before Width: | Height: | Size: 794 B After Width: | Height: | Size: 827 B |
|
@ -1,4 +1,4 @@
|
|||
<div class="chart-wrapper">
|
||||
<div class="chart-wrapper" data-test-usage-stats>
|
||||
<div class="chart-header has-header-link has-bottom-margin-m">
|
||||
<div class="header-left">
|
||||
<h2 class="chart-title">{{@title}}</h2>
|
||||
|
@ -12,7 +12,7 @@
|
|||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column" data-test-client-count-stats>
|
||||
<div class="column">
|
||||
<StatText
|
||||
@label="Total clients"
|
||||
@value={{or @totalUsageCounts.clients "0"}}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{{! TODO CMB CHECK THIS ENTIRE VIEW WITH DESIGN }}
|
||||
<Clients::Dashboard @model={{@model}} />
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if @model.configPath.canUpdate}}
|
||||
<LinkTo @route="vault.cluster.clients.edit" class="toolbar-link">
|
||||
Edit configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<Clients::Config @model={{@model}} @isLoading={{this.currentlyLoading}} />
|
|
@ -6,4 +6,4 @@
|
|||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<Clients::Config @model={{this.model}} @mode="edit" />
|
||||
<Clients::Config @model={{@model}} @mode="edit" />
|
|
@ -0,0 +1,3 @@
|
|||
<Clients::Dashboard @model={{@model}} />
|
||||
|
||||
<Clients::History @model={{@model}} @isLoading={{this.currentlyLoading}} />
|
|
@ -1,69 +1,2 @@
|
|||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
Vault Client Count
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless" data-test-pricing-metrics>
|
||||
{{! template-lint-configure no-unknown-arguments-for-builtin-components "warn" }}
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{{! template-lint-configure no-unknown-arguments-for-builtin-components "warn" }}
|
||||
<LinkTo
|
||||
@route="vault.cluster.clients.index"
|
||||
@query={{hash tab="current" start=null end=null}}
|
||||
@tagName="li"
|
||||
@activeClass="is-active"
|
||||
>
|
||||
<LinkTo
|
||||
@route="vault.cluster.clients.index"
|
||||
@query={{hash tab="current" start=null end=null}}
|
||||
data-test-usage-tab={{true}}
|
||||
>
|
||||
Current month
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="dashboard"}} @tagName="li" @activeClass="is-active">
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="dashboard"}} data-test-usage-tab={{true}}>
|
||||
History
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
{{#if this.model.config.configPath.canRead}}
|
||||
<LinkTo
|
||||
@route="vault.cluster.clients.index"
|
||||
@query={{hash tab="config" start=null end=null}}
|
||||
@tagName="li"
|
||||
@activeClass="is-active"
|
||||
>
|
||||
<LinkTo
|
||||
@route="vault.cluster.clients.index"
|
||||
@query={{hash tab="config" start=null end=null}}
|
||||
data-test-configuration-tab={{true}}
|
||||
>
|
||||
Configuration
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{! template-lint-configure no-unknown-arguments-for-builtin-components "on" }}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{#if (eq this.tab "config")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if this.model.config.configPath.canUpdate}}
|
||||
<LinkTo @route="vault.cluster.clients.edit" class="toolbar-link">
|
||||
Edit configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
<Clients::Config @model={{this.model.config}} @isLoading={{this.currentlyLoading}} />
|
||||
{{else if (eq this.tab "current")}}
|
||||
<Clients::Current @tab={{this.tab}} @model={{this.model}} @isLoading={{this.currentlyLoading}} />
|
||||
{{else if (eq this.tab "dashboard")}}
|
||||
<Clients::Dashboard @tab={{this.tab}} @model={{this.model}} @isLoading={{this.currentlyLoading}} />
|
||||
{{/if}}
|
||||
<Clients::Dashboard @model={{@model}} />
|
||||
<Clients::Current @model={{@model}} @isLoading={{this.currentlyLoading}} />
|
|
@ -9,52 +9,61 @@ module('Integration | Component | client count current', function (hooks) {
|
|||
hooks.beforeEach(function () {
|
||||
let model = EmberObject.create({
|
||||
config: {},
|
||||
activity: {},
|
||||
monthly: {},
|
||||
versionHistory: [],
|
||||
});
|
||||
this.model = model;
|
||||
this.tab = 'current';
|
||||
});
|
||||
|
||||
test('it shows empty state when disabled and no data available', async function (assert) {
|
||||
Object.assign(this.model.config, { enabled: 'Off', queriesAvailable: false });
|
||||
await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
|
||||
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Dashboard @model={{this.model}} />
|
||||
<Clients::Current @model={{this.model}} />
|
||||
`);
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('Tracking is disabled');
|
||||
});
|
||||
|
||||
test('it shows zeroes when enabled and no data', async function (assert) {
|
||||
test('it shows empty state when enabled and no data', async function (assert) {
|
||||
Object.assign(this.model.config, { enabled: 'On', queriesAvailable: false });
|
||||
Object.assign(this.model.activity, {
|
||||
clients: 0,
|
||||
distinct_entities: 0,
|
||||
non_entity_tokens: 0,
|
||||
});
|
||||
await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
|
||||
assert.dom('[data-test-component="empty-state"]').doesNotExist('Empty state does not exist');
|
||||
assert.dom('[data-test-client-count-stats]').exists('Client count data exists');
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Current @model={{this.model}} />`);
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No partial history');
|
||||
});
|
||||
|
||||
test('it shows zeroed data when enabled but no counts', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, enabled: 'On' });
|
||||
await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
|
||||
assert.dom('[data-test-pricing-metrics-form]').doesNotExist('Date range component should not exists');
|
||||
Object.assign(this.model.monthly, {
|
||||
total: { clients: 0, entity_clients: 0, non_entity_clients: 0 },
|
||||
});
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Current @model={{this.model}} />
|
||||
`);
|
||||
assert.dom('[data-test-component="empty-state"]').doesNotExist('Empty state does not exist');
|
||||
assert.dom('[data-test-client-count-stats]').exists('Client count data exists');
|
||||
assert.dom('[data-test-usage-stats]').exists('Client count data exists');
|
||||
assert.dom('[data-test-stat-text-container]').includesText('0');
|
||||
});
|
||||
|
||||
test('it shows data when available from query', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, configPath: { canRead: true } });
|
||||
Object.assign(this.model.activity, {
|
||||
clients: 1234,
|
||||
distinct_entities: 234,
|
||||
non_entity_tokens: 232,
|
||||
Object.assign(this.model.monthly, {
|
||||
total: {
|
||||
clients: 1234,
|
||||
entity_clients: 234,
|
||||
non_entity_clients: 232,
|
||||
},
|
||||
});
|
||||
|
||||
await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Current @model={{this.model}} />`);
|
||||
assert.dom('[data-test-pricing-metrics-form]').doesNotExist('Date range component should not exists');
|
||||
assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
|
||||
assert.dom('[data-test-client-count-stats]').exists('Client count data exists');
|
||||
assert.dom('[data-test-usage-stats]').exists('Client count data exists');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,19 +5,22 @@ import { setupRenderingTest } from 'ember-qunit';
|
|||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | client count history', function (hooks) {
|
||||
// TODO CMB add tests for calendar widget showing
|
||||
setupRenderingTest(hooks);
|
||||
hooks.beforeEach(function () {
|
||||
let model = EmberObject.create({
|
||||
config: {},
|
||||
activity: {},
|
||||
versionHistory: [],
|
||||
});
|
||||
this.model = model;
|
||||
this.tab = 'history';
|
||||
});
|
||||
|
||||
test('it shows empty state when disabled and no data available', async function (assert) {
|
||||
Object.assign(this.model.config, { enabled: 'Off', queriesAvailable: false });
|
||||
await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('Data tracking is disabled');
|
||||
|
@ -25,66 +28,76 @@ module('Integration | Component | client count history', function (hooks) {
|
|||
|
||||
test('it shows empty state when enabled and no data available', async function (assert) {
|
||||
Object.assign(this.model.config, { enabled: 'On', queriesAvailable: false });
|
||||
await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No monthly history');
|
||||
});
|
||||
|
||||
test('it shows empty state when data available but not returned', async function (assert) {
|
||||
test('it shows empty state when no data for queried date range', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true });
|
||||
await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
|
||||
assert.dom('[data-test-pricing-metrics-form]').exists('Date range form component exists');
|
||||
assert.dom('[data-test-pricing-result-dates]').doesNotExist('Date range form result dates are not shown');
|
||||
Object.assign(this.model, { startTimeFromLicense: ['2021', 5] });
|
||||
Object.assign(this.model.activity, {
|
||||
byNamespace: [
|
||||
{
|
||||
label: 'namespace24/',
|
||||
clients: 8301,
|
||||
entity_clients: 4387,
|
||||
non_entity_clients: 3914,
|
||||
mounts: [],
|
||||
},
|
||||
{
|
||||
label: 'namespace88/',
|
||||
clients: 7752,
|
||||
entity_clients: 3632,
|
||||
non_entity_clients: 4120,
|
||||
mounts: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
assert.dom('[data-test-start-date-editor]').exists('Billing start date editor exists');
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No data received');
|
||||
});
|
||||
|
||||
test('it shows warning when disabled and data available', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, enabled: 'Off' });
|
||||
await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
|
||||
assert.dom('[data-test-pricing-metrics-form]').exists('Date range form component exists');
|
||||
assert.dom('[data-test-start-date-editor]').exists('Billing start date editor exists');
|
||||
assert.dom('[data-test-tracking-disabled]').exists('Flash message exists');
|
||||
assert.dom('[data-test-tracking-disabled] .message-title').hasText('Tracking is disabled');
|
||||
});
|
||||
|
||||
// TODO CMB fix
|
||||
// test('it shows data when available from query', async function (assert) {
|
||||
// Object.assign(this.model.config, { queriesAvailable: true, configPath: { canRead: true } });
|
||||
// Object.assign(this.model.activity, {
|
||||
// byNamespace: [
|
||||
// {
|
||||
// counts: {
|
||||
// clients: 2725,
|
||||
// distinct_entities: 1137,
|
||||
// non_entity_tokens: 1588,
|
||||
// },
|
||||
// namespace_id: '8VIZc',
|
||||
// namespace_path: 'nsTest5/',
|
||||
// },
|
||||
// {
|
||||
// counts: {
|
||||
// clients: 200,
|
||||
// distinct_entities: 100,
|
||||
// non_entity_tokens: 100,
|
||||
// },
|
||||
// namespace_id: 'sd3Zc',
|
||||
// namespace_path: 'nsTest1/',
|
||||
// },
|
||||
// ],
|
||||
// total: {
|
||||
// clients: 1234,
|
||||
// distinct_entities: 234,
|
||||
// non_entity_tokens: 232,
|
||||
// },
|
||||
// });
|
||||
test('it shows data when available from query', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, configPath: { canRead: true } });
|
||||
Object.assign(this.model, { startTimeFromLicense: ['2021', 5] });
|
||||
Object.assign(this.model.activity, {
|
||||
byNamespace: [
|
||||
{ label: 'nsTest5/', clients: 2725, entity_clients: 1137, non_entity_clients: 1588 },
|
||||
{ label: 'nsTest1/', clients: 200, entity_clients: 100, non_entity_clients: 100 },
|
||||
],
|
||||
total: {
|
||||
clients: 1234,
|
||||
entity_clients: 234,
|
||||
non_entity_clients: 232,
|
||||
},
|
||||
});
|
||||
|
||||
// await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
|
||||
// assert.dom('[data-test-pricing-metrics-form]').exists('Date range form component exists');
|
||||
// assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
|
||||
// assert.dom('[data-test-client-count-stats]').exists('Client count data exists');
|
||||
// assert.dom('[data-test-client-count-graph]').exists('Top 10 namespaces chart exists');
|
||||
// assert.dom('[data-test-empty-state-title]').hasText('No namespace selected');
|
||||
// });
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
|
||||
assert.dom('[data-test-start-date-editor]').exists('Billing start date editor exists');
|
||||
assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
|
||||
assert.dom('[data-test-usage-stats]').exists('Client count data exists');
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('Horizontal bar chart exists');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue