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:
claire bontempo 2022-02-08 13:07:04 -08:00 committed by GitHub
parent 12b0e2a56b
commit 5f5bd1126e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1197 additions and 1115 deletions

View File

@ -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. `,
};

View File

@ -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;

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
import Controller from '@ember/controller';
export default class ConfigController extends Controller {}

View File

@ -0,0 +1,3 @@
import Controller from '@ember/controller';
export default class HistoryController extends Controller {}

View File

@ -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 {}

View File

@ -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' });

View File

@ -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;
});
}
}
}

View File

@ -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;
});
}
}
}

View File

@ -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;
});
},
},
});
}
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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 todays 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 youve 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 todays 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 youve 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}}

View File

@ -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}}

View File

@ -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, its 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>

View File

@ -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}}

View File

@ -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, its 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>

View File

@ -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

View File

@ -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"}}

View File

@ -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}} />

View File

@ -6,4 +6,4 @@
</p.levelLeft>
</PageHeader>
<Clients::Config @model={{this.model}} @mode="edit" />
<Clients::Config @model={{@model}} @mode="edit" />

View File

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

View File

@ -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}} />

View File

@ -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');
});
});

View File

@ -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');
});
});