UI: combine current + history client count tabs into one dashboard (#17575)
* WIP/initial routing-ish * refactor date dropdown to reuse in modal and allowe current month selection * swap linter disable line * refactor date-dropdown to return object * refactor calendar widget, add tests * change calendar start and end args to getters * refactor dashboard to use date objects instead of array of year, month * remove dashboard files for easier to follow git diff * comment out dashboard tab until route name updated * delete current tab and route * fix undefined banner time * cleanup version history serializer and upgrade data * first pass of updating tests * add changelog * update client count util test * validate end time is after start time * update comment * add current month to calendar widget * add comments for code changes to make following API update * Removed a modified file from pull request * address comments/cleanup * update variables to const * update test const * rename history -> dashboard, fix tests * fix timestamps for attribution chart * update release note * refactor using backend start and end time params * add test for adapter formatting time params * fix tests * cleanup adapter comment and query params * change back history file name for diff * rename file using cli * revert filenames * rename files via git cli * revert route file name * last cli rename * refactor mirage * hold off on running total changes * update params in test * refactor to remove conditional assertions * finish tests * fix firefox tooltip * remove current-when * refactor version history * add timezone/UTC note * final cleanup!!!! * fix test * fix client count date tests * fix date-dropdown test * clear datedropdown completely * update date selectors to accommodate new year (#18586) * Revert "hold off on running total changes" This reverts commit 8dc79a626d549df83bc47e290392a556c670f98f. * remove assumed 0 values * update average helper to only calculate for array of objects * remove passing in bar chart data, map in running totals component instead * cleanup usage stat component * clear ss filters for new queries * update csv export, add explanation to modal * update test copy * consistently return null if no upgrade during activity (instead of empty array) * update description, add clarifying comments * update tes * add more clarifying comments * fix historic single month chart * remove old test tag * Update ui/app/components/clients/dashboard.js
This commit is contained in:
parent
6d053a4c00
commit
4a9610f382
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: use the combined activity log (partial + historic) API for client count dashboard and remove use of monthly endpoint
|
||||
```
|
|
@ -1,38 +1,22 @@
|
|||
import Application from '../application';
|
||||
import { formatRFC3339 } from 'date-fns';
|
||||
import ApplicationAdapter from '../application';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
|
||||
export default Application.extend({
|
||||
// Since backend converts the timezone to UTC, sending the first (1) as start or end date can cause the month to change.
|
||||
// To mitigate this impact of timezone conversion, hard coding the dates to avoid month change.
|
||||
formatTimeParams(query) {
|
||||
let { start_time, end_time } = query;
|
||||
// check if it's an array, if it is, it's coming from an action like selecting a new startTime or new EndTime
|
||||
if (Array.isArray(start_time)) {
|
||||
const startYear = Number(start_time[0]);
|
||||
const startMonth = Number(start_time[1]);
|
||||
start_time = formatRFC3339(new Date(startYear, startMonth, 10));
|
||||
}
|
||||
if (end_time) {
|
||||
if (Array.isArray(end_time)) {
|
||||
const endYear = Number(end_time[0]);
|
||||
const endMonth = Number(end_time[1]);
|
||||
end_time = formatRFC3339(new Date(endYear, endMonth, 20));
|
||||
}
|
||||
export default class ActivityAdapter extends ApplicationAdapter {
|
||||
// javascript localizes new Date() objects but all activity log data is stored in UTC
|
||||
// create date object from user's input using Date.UTC() then send to backend as unix
|
||||
// time params from the backend are formatted as a zulu timestamp
|
||||
formatQueryParams(queryParams) {
|
||||
let { start_time, end_time } = queryParams;
|
||||
start_time = start_time.timestamp || getUnixTime(Date.UTC(start_time.year, start_time.monthIdx, 1));
|
||||
// day=0 for Date.UTC() returns the last day of the month before
|
||||
// increase monthIdx by one to get last day of queried month
|
||||
end_time = end_time.timestamp || getUnixTime(Date.UTC(end_time.year, end_time.monthIdx + 1, 0));
|
||||
return { start_time, end_time };
|
||||
}
|
||||
|
||||
return { start_time, end_time };
|
||||
} else {
|
||||
return { start_time };
|
||||
}
|
||||
},
|
||||
|
||||
// query comes in as either: {start_time: '2021-03-17T00:00:00Z'} or
|
||||
// {start_time: Array(2), end_time: Array(2)}
|
||||
// end_time: (2) ['2022', 0]
|
||||
// start_time: (2) ['2021', 2]
|
||||
queryRecord(store, type, query) {
|
||||
const url = `${this.buildURL()}/internal/counters/activity`;
|
||||
// check if start and/or end times are in RFC3395 format, if not convert with timezone UTC/zulu.
|
||||
const queryParams = this.formatTimeParams(query);
|
||||
const queryParams = this.formatQueryParams(query);
|
||||
if (queryParams) {
|
||||
return this.ajax(url, 'GET', { data: queryParams }).then((resp) => {
|
||||
const response = resp || {};
|
||||
|
@ -40,5 +24,5 @@ export default Application.extend({
|
|||
return response;
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import ApplicationAdapter from '../application';
|
||||
|
||||
export default class MonthlyAdapter extends ApplicationAdapter {
|
||||
queryRecord() {
|
||||
const url = `${this.buildURL()}/internal/counters/activity/monthly`;
|
||||
// Query has startTime defined. The API will return the endTime if none is provided.
|
||||
return this.ajax(url, 'GET').then((resp) => {
|
||||
const response = resp || {};
|
||||
response.id = response.request_id || 'no-data';
|
||||
return response;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import Application from '../application';
|
||||
import ApplicationAdapter from '../application';
|
||||
|
||||
export default Application.extend({
|
||||
export default class VersionHistoryAdapter extends ApplicationAdapter {
|
||||
findAll() {
|
||||
return this.ajax(this.buildURL() + '/version-history', 'GET', {
|
||||
data: {
|
||||
|
@ -9,5 +9,5 @@ export default Application.extend({
|
|||
}).then((resp) => {
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,141 +1,113 @@
|
|||
import Component from '@glimmer/component';
|
||||
import layout from '../templates/components/calendar-widget';
|
||||
import { setComponentTemplate } from '@ember/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
import { ARRAY_OF_MONTHS, parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { addYears, isSameYear, subYears } from 'date-fns';
|
||||
/**
|
||||
* @module CalendarWidget
|
||||
* CalendarWidget components are used in the client counts metrics. It helps users understand the ranges they can select.
|
||||
* CalendarWidget component is used in the client counts dashboard to select a month/year to query the /activity endpoint.
|
||||
* The component returns an object with selected date info, example: { dateType: 'endDate', monthIdx: 0, monthName: 'January', year: 2022 }
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <CalendarWidget
|
||||
* @param {array} arrayOfMonths - An array of all the months that the calendar widget iterates through.
|
||||
* @param {string} endTimeDisplay - The formatted display value of the endTime. Ex: January 2022.
|
||||
* @param {array} endTimeFromResponse - The value returned on the counters/activity endpoint, which shows the true endTime not the selected one, which can be different. Ex: ['2022', 0]
|
||||
* @param {function} handleClientActivityQuery - a function passed from parent. This component sends the month and year to the parent via this method which then calculates the new data.
|
||||
* @param {function} handleCurrentBillingPeriod - a function passed from parent. This component makes the parent aware that the user selected Current billing period and it handles resetting the data.
|
||||
* @param {string} startTimeDisplay - The formatted display value of the endTime. Ex: January 2022. This component is only responsible for modifying the endTime which is sends to the parent to make the network request.
|
||||
* />
|
||||
* <CalendarWidget @startTimestamp={{this.startTime}} @endTimestamp={{this.endTime}} @selectMonth={{this.handleSelection}} />
|
||||
*
|
||||
* @param {string} startTimestamp - ISO timestamp string of the calendar widget's start time, displays in dropdown trigger
|
||||
* @param {string} endTimestamp - ISO timestamp string for the calendar widget's end time, displays in dropdown trigger
|
||||
* @param {function} selectMonth - callback function from parent - fires when selecting a month or clicking "Current billing period"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
class CalendarWidget extends Component {
|
||||
export default class CalendarWidget extends Component {
|
||||
currentDate = new Date();
|
||||
currentYear = this.currentDate.getFullYear(); // integer
|
||||
currentMonth = parseInt(this.currentDate.getMonth()); // integer and zero index
|
||||
|
||||
@tracked allMonthsNodeList = [];
|
||||
@tracked displayYear = this.currentYear; // init to currentYear and then changes as a user clicks on the chevrons
|
||||
@tracked calendarDisplayDate = this.currentDate; // init to current date, updates when user clicks on calendar chevrons
|
||||
@tracked showCalendar = false;
|
||||
@tracked tooltipTarget = null;
|
||||
@tracked tooltipText = null;
|
||||
|
||||
get selectedMonthId() {
|
||||
if (!this.args.endTimeFromResponse) return '';
|
||||
const [year, monthIndex] = this.args.endTimeFromResponse;
|
||||
return `${monthIndex}-${year}`;
|
||||
// both date getters return a date object
|
||||
get startDate() {
|
||||
return parseAPITimestamp(this.args.startTimestamp);
|
||||
}
|
||||
get endDate() {
|
||||
return parseAPITimestamp(this.args.endTimestamp);
|
||||
}
|
||||
get displayYear() {
|
||||
return this.calendarDisplayDate.getFullYear();
|
||||
}
|
||||
get disableFutureYear() {
|
||||
return this.displayYear === this.currentYear;
|
||||
return isSameYear(this.calendarDisplayDate, this.currentDate);
|
||||
}
|
||||
get disablePastYear() {
|
||||
const startYear = parseInt(this.args.startTimeDisplay.split(' ')[1]);
|
||||
return this.displayYear === startYear; // if on startYear then don't let them click back to the year prior
|
||||
// calendar widget should only go as far back as the passed in start time
|
||||
return isSameYear(this.calendarDisplayDate, this.startDate);
|
||||
}
|
||||
get widgetMonths() {
|
||||
const displayYear = this.displayYear;
|
||||
const currentYear = this.currentYear;
|
||||
const currentMonthIdx = this.currentMonth;
|
||||
const [startMonth, startYear] = this.args.startTimeDisplay.split(' ');
|
||||
const startMonthIdx = this.args.arrayOfMonths.indexOf(startMonth);
|
||||
return this.args.arrayOfMonths.map((month, idx) => {
|
||||
const monthId = `${idx}-${displayYear}`;
|
||||
const startYear = this.startDate.getFullYear();
|
||||
const startMonthIdx = this.startDate.getMonth();
|
||||
return ARRAY_OF_MONTHS.map((month, index) => {
|
||||
let readonly = false;
|
||||
|
||||
// if widget is showing billing start year, disable if month is before start month
|
||||
if (parseInt(startYear) === displayYear && idx < startMonthIdx) {
|
||||
// if widget is showing same year as @startTimestamp year, disable if month is before start month
|
||||
if (startYear === this.displayYear && index < startMonthIdx) {
|
||||
readonly = true;
|
||||
}
|
||||
|
||||
// if widget showing current year, disable if month is current or later
|
||||
if (displayYear === currentYear && idx >= currentMonthIdx) {
|
||||
// if widget showing current year, disable if month is later than current month
|
||||
if (this.displayYear === this.currentDate.getFullYear() && index > this.currentDate.getMonth()) {
|
||||
readonly = true;
|
||||
}
|
||||
return {
|
||||
id: monthId,
|
||||
month,
|
||||
index,
|
||||
year: this.displayYear,
|
||||
name: month,
|
||||
readonly,
|
||||
current: monthId === `${currentMonthIdx}-${currentYear}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// HELPER FUNCTIONS (alphabetically) //
|
||||
addClass(element, classString) {
|
||||
element.classList.add(classString);
|
||||
}
|
||||
|
||||
removeClass(element, classString) {
|
||||
element.classList.remove(classString);
|
||||
}
|
||||
|
||||
resetDisplayYear() {
|
||||
let setYear = this.currentYear;
|
||||
if (this.args.endTimeDisplay) {
|
||||
try {
|
||||
const year = this.args.endTimeDisplay.split(' ')[1];
|
||||
setYear = parseInt(year);
|
||||
} catch (e) {
|
||||
console.debug('Error resetting display year', e); // eslint-disable-line
|
||||
}
|
||||
}
|
||||
this.displayYear = setYear;
|
||||
}
|
||||
|
||||
// ACTIONS (alphabetically) //
|
||||
@action
|
||||
addTooltip() {
|
||||
if (this.disablePastYear) {
|
||||
const previousYear = Number(this.displayYear) - 1;
|
||||
this.tooltipText = `${previousYear} is unavailable because it is before your billing start month. Change your billing start month to a date in ${previousYear} to see data for this year.`; // set tooltip text
|
||||
const previousYear = this.displayYear - 1;
|
||||
this.tooltipText = `${previousYear} is unavailable because it is before your start date. Change your start month to a date in ${previousYear} to see data for this year.`;
|
||||
this.tooltipTarget = '#previous-year';
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
addYear() {
|
||||
this.displayYear = this.displayYear + 1;
|
||||
}
|
||||
|
||||
@action removeTooltip() {
|
||||
removeTooltip() {
|
||||
this.tooltipTarget = null;
|
||||
}
|
||||
|
||||
@action
|
||||
selectCurrentBillingPeriod(D) {
|
||||
this.args.handleCurrentBillingPeriod(); // resets the billing startTime and endTime to what it is on init via the parent.
|
||||
this.showCalendar = false;
|
||||
D.actions.close(); // close the dropdown.
|
||||
}
|
||||
@action
|
||||
selectEndMonth(monthId, D) {
|
||||
const [monthIdx, year] = monthId.split('-');
|
||||
this.toggleShowCalendar();
|
||||
this.args.handleClientActivityQuery(parseInt(monthIdx), parseInt(year), 'endTime');
|
||||
D.actions.close(); // close the dropdown.
|
||||
addYear() {
|
||||
this.calendarDisplayDate = addYears(this.calendarDisplayDate, 1);
|
||||
}
|
||||
|
||||
@action
|
||||
subYear() {
|
||||
this.displayYear = this.displayYear - 1;
|
||||
this.calendarDisplayDate = subYears(this.calendarDisplayDate, 1);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleShowCalendar() {
|
||||
this.showCalendar = !this.showCalendar;
|
||||
this.resetDisplayYear();
|
||||
this.calendarDisplayDate = this.endDate;
|
||||
}
|
||||
|
||||
@action
|
||||
handleDateShortcut(dropdown, { target }) {
|
||||
this.args.selectMonth({ dateType: target.name }); // send clicked shortcut to parent callback
|
||||
this.showCalendar = false;
|
||||
dropdown.close();
|
||||
}
|
||||
|
||||
@action
|
||||
selectMonth(month, dropdown) {
|
||||
const { index, year, name } = month;
|
||||
this.toggleShowCalendar();
|
||||
this.args.selectMonth({ monthIdx: index, monthName: name, year, dateType: 'endDate' });
|
||||
dropdown.close();
|
||||
}
|
||||
}
|
||||
export default setComponentTemplate(layout, CalendarWidget);
|
||||
|
|
|
@ -2,6 +2,9 @@ import Component from '@glimmer/component';
|
|||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { format, isSameMonth } from 'date-fns';
|
||||
|
||||
/**
|
||||
* @module Attribution
|
||||
* Attribution components display the top 10 total client counts for namespaces or auth methods (mounts) during a billing period.
|
||||
|
@ -16,10 +19,11 @@ import { inject as service } from '@ember/service';
|
|||
* @totalClientAttribution={{this.totalClientAttribution}}
|
||||
* @newClientAttribution={{this.newClientAttribution}}
|
||||
* @selectedNamespace={{this.selectedNamespace}}
|
||||
* @startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
|
||||
* @isDateRange={{this.isDateRange}}
|
||||
* @isCurrentMonth={{false}}
|
||||
* @timestamp={{this.responseTimestamp}}
|
||||
* @startTimestamp={{this.startTime}}
|
||||
* @endTimestamp={{this.endTime}}
|
||||
* @isHistoricalMonth={{false}}
|
||||
* @responseTimestamp={{this.responseTimestamp}}
|
||||
* @upgradeExplanation="We added monthly breakdowns and mount level attribution starting in 1.10, so keep that in mind when looking at the data."
|
||||
* />
|
||||
* ```
|
||||
* @param {array} chartLegend - (passed to child) array of objects with key names 'key' and 'label' so data can be stacked
|
||||
|
@ -28,23 +32,31 @@ import { inject as service } from '@ember/service';
|
|||
* @param {array} totalClientAttribution - array of objects containing a label and breakdown of client counts for total clients
|
||||
* @param {array} newClientAttribution - array of objects containing a label and breakdown of client counts for new clients
|
||||
* @param {string} selectedNamespace - namespace selected from filter bar
|
||||
* @param {string} startTimeDisplay - string that displays as start date for CSV modal
|
||||
* @param {string} endTimeDisplay - string that displays as end date for CSV modal
|
||||
* @param {boolean} isDateRange - getter calculated in parent to relay if dataset is a date range or single month and display text accordingly
|
||||
* @param {boolean} isCurrentMonth - boolean to determine if rendered in current month tab or not
|
||||
* @param {string} timestamp - ISO timestamp created in serializer to timestamp the response
|
||||
* @param {string} startTimestamp - timestamp string from activity response to render start date for CSV modal and whether copy reads 'month' or 'date range'
|
||||
* @param {string} endTimestamp - timestamp string from activity response to render end date for CSV modal and whether copy reads 'month' or 'date range'
|
||||
* @param {string} responseTimestamp - ISO timestamp created in serializer to timestamp the response, renders in bottom left corner below attribution chart
|
||||
* @param {boolean} isHistoricalMonth - when true data is from a single, historical month so side-by-side charts should display for attribution data
|
||||
* @param {boolean} upgradeExplanation - if data contains an upgrade, explanation is generated by the parent to be rendered in the export modal
|
||||
*/
|
||||
|
||||
export default class Attribution extends Component {
|
||||
@tracked showCSVDownloadModal = false;
|
||||
@service download;
|
||||
|
||||
get hasCsvData() {
|
||||
return this.args.totalClientAttribution ? this.args.totalClientAttribution.length > 0 : false;
|
||||
get formattedStartDate() {
|
||||
if (!this.args.startTimestamp) return null;
|
||||
return parseAPITimestamp(this.args.startTimestamp, 'MMMM yyyy');
|
||||
}
|
||||
get formattedEndDate() {
|
||||
if (!this.args.startTimestamp && !this.args.endTimestamp) return null;
|
||||
// displays on CSV export modal, no need to display duplicate months and years
|
||||
const startDateObject = parseAPITimestamp(this.args.startTimestamp);
|
||||
const endDateObject = parseAPITimestamp(this.args.endTimestamp);
|
||||
return isSameMonth(startDateObject, endDateObject) ? null : format(endDateObject, 'MMMM yyyy');
|
||||
}
|
||||
|
||||
get isDateRange() {
|
||||
return this.args.isDateRange;
|
||||
get hasCsvData() {
|
||||
return this.args.totalClientAttribution ? this.args.totalClientAttribution.length > 0 : false;
|
||||
}
|
||||
|
||||
get isSingleNamespace() {
|
||||
|
@ -75,7 +87,7 @@ export default class Attribution extends Component {
|
|||
}
|
||||
|
||||
get chartText() {
|
||||
const dateText = this.isDateRange ? 'date range' : 'month';
|
||||
const dateText = this.formattedEndDate ? 'date range' : 'month';
|
||||
switch (this.isSingleNamespace) {
|
||||
case true:
|
||||
return {
|
||||
|
@ -118,7 +130,7 @@ export default class Attribution extends Component {
|
|||
const otherColumns = newColumns ? [...totalColumns, ...newColumns] : [...totalColumns];
|
||||
return [
|
||||
`${typeof namespaceColumn === 'string' ? namespaceColumn : namespaceColumn.label}`,
|
||||
`${mountColumn ? mountColumn.label : ''}`,
|
||||
`${mountColumn ? mountColumn.label : '*'}`,
|
||||
...otherColumns,
|
||||
];
|
||||
}
|
||||
|
@ -127,9 +139,14 @@ export default class Attribution extends Component {
|
|||
const totalAttribution = this.args.totalClientAttribution;
|
||||
const newAttribution = this.barChartNewClients ? this.args.newClientAttribution : null;
|
||||
const csvData = [];
|
||||
// added to clarify that the row of namespace totals without an auth method (blank) are not additional clients
|
||||
// but indicate the total clients for that ns, including its auth methods
|
||||
const descriptionOfBlanks = this.isSingleNamespace
|
||||
? ''
|
||||
: `\n *namespace totals, inclusive of auth method clients`;
|
||||
const csvHeader = [
|
||||
'Namespace path',
|
||||
'Authentication method',
|
||||
`"Authentication method ${descriptionOfBlanks}"`,
|
||||
'Total clients',
|
||||
'Entity clients',
|
||||
'Non-entity clients',
|
||||
|
@ -173,15 +190,14 @@ export default class Attribution extends Component {
|
|||
return csvData.map((d) => d.join()).join('\n');
|
||||
}
|
||||
|
||||
get getCsvFileName() {
|
||||
const endRange = this.isDateRange ? `-${this.args.endTimeDisplay}` : '';
|
||||
const csvDateRange = this.args.startTimeDisplay + endRange;
|
||||
get formattedCsvFileName() {
|
||||
const endRange = this.formattedEndDate ? `-${this.formattedEndDate}` : '';
|
||||
const csvDateRange = this.formattedStartDate + endRange;
|
||||
return this.isSingleNamespace
|
||||
? `clients_by_auth_method_${csvDateRange}`
|
||||
: `clients_by_namespace_${csvDateRange}`;
|
||||
}
|
||||
|
||||
// ACTIONS
|
||||
@action
|
||||
exportChartData(filename) {
|
||||
const contents = this.generateCsvData();
|
||||
|
|
|
@ -1,152 +0,0 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { isAfter, startOfMonth } from 'date-fns';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class Current extends Component {
|
||||
chartLegend = [
|
||||
{ key: 'entity_clients', label: 'entity clients' },
|
||||
{ key: 'non_entity_clients', label: 'non-entity clients' },
|
||||
];
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked namespaceArray = this.byNamespace.map((namespace) => {
|
||||
return { name: namespace['label'], id: namespace['label'] };
|
||||
});
|
||||
|
||||
@tracked selectedAuthMethod = null;
|
||||
@tracked authMethodOptions = [];
|
||||
|
||||
get upgradeVersionHistory() {
|
||||
const versionHistory = this.args.model.versionHistory;
|
||||
if (!versionHistory || versionHistory.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get upgrade data for initial upgrade to 1.9 and/or 1.10
|
||||
const relevantUpgrades = [];
|
||||
const importantUpgrades = ['1.9', '1.10'];
|
||||
importantUpgrades.forEach((version) => {
|
||||
const findUpgrade = versionHistory.find((versionData) => versionData.id.match(version));
|
||||
if (findUpgrade) relevantUpgrades.push(findUpgrade);
|
||||
});
|
||||
// array of upgrade data objects for noteworthy upgrades
|
||||
return relevantUpgrades;
|
||||
}
|
||||
|
||||
// Response client count data by namespace for current/partial month
|
||||
get byNamespace() {
|
||||
return this.args.model.monthly?.byNamespace || [];
|
||||
}
|
||||
|
||||
get isGatheringData() {
|
||||
// return true if tracking IS enabled but no data collected yet
|
||||
return this.args.model.config?.enabled === 'On' && this.byNamespace.length === 0;
|
||||
}
|
||||
|
||||
get hasAttributionData() {
|
||||
if (this.selectedAuthMethod) return false;
|
||||
if (this.selectedNamespace) {
|
||||
return this.authMethodOptions.length > 0;
|
||||
}
|
||||
return this.totalUsageCounts.clients !== 0 && !!this.totalClientAttribution;
|
||||
}
|
||||
|
||||
get filteredCurrentData() {
|
||||
const namespace = this.selectedNamespace;
|
||||
const auth = this.selectedAuthMethod;
|
||||
if (!namespace && !auth) {
|
||||
return this.byNamespace;
|
||||
}
|
||||
if (!auth) {
|
||||
return this.byNamespace.find((ns) => ns.label === namespace);
|
||||
}
|
||||
return this.byNamespace
|
||||
.find((ns) => ns.label === namespace)
|
||||
.mounts?.find((mount) => mount.label === auth);
|
||||
}
|
||||
|
||||
get upgradeDuringCurrentMonth() {
|
||||
if (!this.upgradeVersionHistory) {
|
||||
return null;
|
||||
}
|
||||
const upgradesWithinData = this.upgradeVersionHistory.filter((upgrade) => {
|
||||
// TODO how do timezones affect this?
|
||||
const upgradeDate = new Date(upgrade.timestampInstalled);
|
||||
return isAfter(upgradeDate, startOfMonth(new Date()));
|
||||
});
|
||||
// return all upgrades that happened within date range of queried activity
|
||||
return upgradesWithinData.length === 0 ? null : upgradesWithinData;
|
||||
}
|
||||
|
||||
get upgradeVersionAndDate() {
|
||||
if (!this.upgradeDuringCurrentMonth) {
|
||||
return null;
|
||||
}
|
||||
if (this.upgradeDuringCurrentMonth.length === 2) {
|
||||
const versions = this.upgradeDuringCurrentMonth.map((upgrade) => upgrade.id).join(' and ');
|
||||
return `Vault was upgraded to ${versions} during this month.`;
|
||||
} else {
|
||||
const version = this.upgradeDuringCurrentMonth[0];
|
||||
return `Vault was upgraded to ${version.id} on this month.`;
|
||||
}
|
||||
}
|
||||
|
||||
get versionSpecificText() {
|
||||
if (!this.upgradeDuringCurrentMonth) {
|
||||
return null;
|
||||
}
|
||||
if (this.upgradeDuringCurrentMonth.length === 1) {
|
||||
const version = this.upgradeDuringCurrentMonth[0].id;
|
||||
if (version.match('1.9')) {
|
||||
return ' How we count clients changed in 1.9, so keep that in mind when looking at the data below.';
|
||||
}
|
||||
if (version.match('1.10')) {
|
||||
return ' We added mount level attribution starting in 1.10, so keep that in mind when looking at the data below.';
|
||||
}
|
||||
}
|
||||
// return combined explanation if spans multiple upgrades
|
||||
return ' How we count clients changed in 1.9 and we added mount level attribution starting in 1.10. Keep this in mind when looking at the data below.';
|
||||
}
|
||||
|
||||
// top level TOTAL client counts for current/partial month
|
||||
get totalUsageCounts() {
|
||||
return this.selectedNamespace ? this.filteredCurrentData : this.args.model.monthly?.total;
|
||||
}
|
||||
|
||||
// total client attribution data for horizontal bar chart in attribution component
|
||||
get totalClientAttribution() {
|
||||
if (this.selectedNamespace) {
|
||||
return this.filteredCurrentData?.mounts || null;
|
||||
} else {
|
||||
return this.byNamespace;
|
||||
}
|
||||
}
|
||||
|
||||
get responseTimestamp() {
|
||||
return this.args.model.monthly?.responseTimestamp;
|
||||
}
|
||||
|
||||
// ACTIONS
|
||||
@action
|
||||
selectNamespace([value]) {
|
||||
// value comes in as [namespace0]
|
||||
this.selectedNamespace = value;
|
||||
if (!value) {
|
||||
this.authMethodOptions = [];
|
||||
// on clear, also make sure auth method is cleared
|
||||
this.selectedAuthMethod = null;
|
||||
} else {
|
||||
// Side effect: set auth namespaces
|
||||
const mounts = this.filteredCurrentData.mounts?.map((mount) => ({
|
||||
id: mount.label,
|
||||
name: mount.label,
|
||||
}));
|
||||
this.authMethodOptions = mounts;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
setAuthMethod([authMount]) {
|
||||
this.selectedAuthMethod = authMount;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,371 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { isAfter, isBefore, isSameMonth, format } from 'date-fns';
|
||||
import getStorage from 'vault/lib/token-storage';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
// my sincere apologies to the next dev who has to refactor/debug this (⇀‸↼‶)
|
||||
export default class Dashboard extends Component {
|
||||
@service store;
|
||||
@service version;
|
||||
|
||||
chartLegend = [
|
||||
{ key: 'entity_clients', label: 'entity clients' },
|
||||
{ key: 'non_entity_clients', label: 'non-entity clients' },
|
||||
];
|
||||
|
||||
// RESPONSE
|
||||
@tracked startMonthTimestamp; // when user queries, updates to first month object of response
|
||||
@tracked endMonthTimestamp; // when user queries, updates to last month object of response
|
||||
@tracked queriedActivityResponse = null;
|
||||
// track params sent to /activity request
|
||||
@tracked activityQueryParams = {
|
||||
start: {}, // updates when user edits billing start month
|
||||
end: {}, // updates when user queries end dates via calendar widget
|
||||
};
|
||||
|
||||
// SEARCH SELECT FILTERS
|
||||
get namespaceArray() {
|
||||
return this.getActivityResponse.byNamespace
|
||||
? this.getActivityResponse.byNamespace.map((namespace) => ({
|
||||
name: namespace.label,
|
||||
id: namespace.label,
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked selectedAuthMethod = null;
|
||||
@tracked authMethodOptions = [];
|
||||
|
||||
// TEMPLATE VIEW
|
||||
@tracked noActivityData;
|
||||
@tracked showBillingStartModal = false;
|
||||
@tracked isLoadingQuery = false;
|
||||
@tracked errorObject = null;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.startMonthTimestamp = this.args.model.licenseStartTimestamp;
|
||||
this.endMonthTimestamp = this.args.model.currentDate;
|
||||
this.activityQueryParams.start.timestamp = this.args.model.licenseStartTimestamp;
|
||||
this.activityQueryParams.end.timestamp = this.args.model.currentDate;
|
||||
this.noActivityData = this.args.model.activity.id === 'no-data' ? true : false;
|
||||
}
|
||||
|
||||
// returns text for empty state message if noActivityData
|
||||
get dateRangeMessage() {
|
||||
if (!this.startMonthTimestamp && !this.endMonthTimestamp) return null;
|
||||
const endMonth = isSameMonth(
|
||||
parseAPITimestamp(this.startMonthTimestamp),
|
||||
parseAPITimestamp(this.endMonthTimestamp)
|
||||
)
|
||||
? ''
|
||||
: ` to ${parseAPITimestamp(this.endMonthTimestamp, 'MMMM yyyy')}`;
|
||||
// completes the message 'No data received from { dateRangeMessage }'
|
||||
return `from ${parseAPITimestamp(this.startMonthTimestamp, 'MMMM yyyy')}` + endMonth;
|
||||
}
|
||||
|
||||
get versionText() {
|
||||
return this.version.isEnterprise
|
||||
? {
|
||||
label: 'Billing start month',
|
||||
description:
|
||||
'This date comes from your license, and defines when client counting starts. Without this starting point, the data shown is not reliable.',
|
||||
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.',
|
||||
}
|
||||
: {
|
||||
label: 'Client counting start date',
|
||||
description:
|
||||
'This date is when client counting starts. Without this starting point, the data shown is not reliable.',
|
||||
title: 'No start date found',
|
||||
message:
|
||||
'In order to get the most from this data, please enter a start month above. Vault will calculate new clients starting from that month.',
|
||||
};
|
||||
}
|
||||
|
||||
get isDateRange() {
|
||||
return !isSameMonth(
|
||||
parseAPITimestamp(this.getActivityResponse.startTime),
|
||||
parseAPITimestamp(this.getActivityResponse.endTime)
|
||||
);
|
||||
}
|
||||
|
||||
get isCurrentMonth() {
|
||||
return (
|
||||
isSameMonth(
|
||||
parseAPITimestamp(this.getActivityResponse.startTime),
|
||||
parseAPITimestamp(this.args.model.currentDate)
|
||||
) &&
|
||||
isSameMonth(
|
||||
parseAPITimestamp(this.getActivityResponse.endTime),
|
||||
parseAPITimestamp(this.args.model.currentDate)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
get startTimeDiscrepancy() {
|
||||
// show banner if startTime returned from activity log (response) is after the queried startTime
|
||||
const activityStartDateObject = parseAPITimestamp(this.getActivityResponse.startTime);
|
||||
const queryStartDateObject = parseAPITimestamp(this.startMonthTimestamp);
|
||||
let message = 'You requested data from';
|
||||
if (this.startMonthTimestamp === this.args.model.licenseStartTimestamp && this.version.isEnterprise) {
|
||||
// on init, date is automatically pulled from license start date and user hasn't queried anything yet
|
||||
message = 'Your license start date is';
|
||||
}
|
||||
if (
|
||||
isAfter(activityStartDateObject, queryStartDateObject) &&
|
||||
!isSameMonth(activityStartDateObject, queryStartDateObject)
|
||||
) {
|
||||
return `${message} ${parseAPITimestamp(this.startMonthTimestamp, 'MMMM yyyy')}.
|
||||
We only have data from ${parseAPITimestamp(this.getActivityResponse.startTime, 'MMMM yyyy')},
|
||||
and that is what is being shown here.`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get upgradeDuringActivity() {
|
||||
const versionHistory = this.args.model.versionHistory;
|
||||
if (!versionHistory || versionHistory.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// filter for upgrade data of noteworthy upgrades (1.9 and/or 1.10)
|
||||
const upgradeVersionHistory = versionHistory.filter(
|
||||
({ version }) => version.match('1.9') || version.match('1.10')
|
||||
);
|
||||
if (!upgradeVersionHistory || upgradeVersionHistory.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activityStart = parseAPITimestamp(this.getActivityResponse.startTime);
|
||||
const activityEnd = parseAPITimestamp(this.getActivityResponse.endTime);
|
||||
// filter and return all upgrades that happened within date range of queried activity
|
||||
const upgradesWithinData = upgradeVersionHistory.filter(({ timestampInstalled }) => {
|
||||
const upgradeDate = parseAPITimestamp(timestampInstalled);
|
||||
return isAfter(upgradeDate, activityStart) && isBefore(upgradeDate, activityEnd);
|
||||
});
|
||||
return upgradesWithinData.length === 0 ? null : upgradesWithinData;
|
||||
}
|
||||
|
||||
get upgradeVersionAndDate() {
|
||||
if (!this.upgradeDuringActivity) return null;
|
||||
|
||||
if (this.upgradeDuringActivity.length === 2) {
|
||||
const [firstUpgrade, secondUpgrade] = this.upgradeDuringActivity;
|
||||
const firstDate = parseAPITimestamp(firstUpgrade.timestampInstalled, 'MMM d, yyyy');
|
||||
const secondDate = parseAPITimestamp(secondUpgrade.timestampInstalled, 'MMM d, yyyy');
|
||||
return `Vault was upgraded to ${firstUpgrade.version} (${firstDate}) and ${secondUpgrade.version} (${secondDate}) during this time range.`;
|
||||
} else {
|
||||
const [upgrade] = this.upgradeDuringActivity;
|
||||
return `Vault was upgraded to ${upgrade.version} on ${parseAPITimestamp(
|
||||
upgrade.timestampInstalled,
|
||||
'MMM d, yyyy'
|
||||
)}.`;
|
||||
}
|
||||
}
|
||||
|
||||
get upgradeExplanation() {
|
||||
if (!this.upgradeDuringActivity) return null;
|
||||
if (this.upgradeDuringActivity.length === 1) {
|
||||
const version = this.upgradeDuringActivity[0].version;
|
||||
if (version.match('1.9')) {
|
||||
return ' How we count clients changed in 1.9, so keep that in mind when looking at the data.';
|
||||
}
|
||||
if (version.match('1.10')) {
|
||||
return ' We added monthly breakdowns and mount level attribution starting in 1.10, so keep that in mind when looking at the data.';
|
||||
}
|
||||
}
|
||||
// return combined explanation if spans multiple upgrades
|
||||
return ' How we count clients changed in 1.9 and we added monthly breakdowns and mount level attribution starting in 1.10. Keep this in mind when looking at the data.';
|
||||
}
|
||||
|
||||
get formattedStartDate() {
|
||||
if (!this.startMonthTimestamp) return null;
|
||||
return parseAPITimestamp(this.startMonthTimestamp, 'MMMM yyyy');
|
||||
}
|
||||
|
||||
// GETTERS FOR RESPONSE & DATA
|
||||
|
||||
// on init API response uses license start_date, getter updates when user queries dates
|
||||
get getActivityResponse() {
|
||||
return this.queriedActivityResponse || this.args.model.activity;
|
||||
}
|
||||
|
||||
get byMonthActivityData() {
|
||||
if (this.selectedNamespace) {
|
||||
return this.filteredActivityByMonth;
|
||||
} else {
|
||||
return this.getActivityResponse?.byMonth;
|
||||
}
|
||||
}
|
||||
|
||||
get hasAttributionData() {
|
||||
if (this.selectedAuthMethod) return false;
|
||||
if (this.selectedNamespace) {
|
||||
return this.authMethodOptions.length > 0;
|
||||
}
|
||||
return !!this.totalClientAttribution && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
|
||||
}
|
||||
|
||||
// (object) top level TOTAL client counts for given date range
|
||||
get totalUsageCounts() {
|
||||
return this.selectedNamespace ? this.filteredActivityByNamespace : this.getActivityResponse.total;
|
||||
}
|
||||
|
||||
// (object) single month new client data with total counts + array of namespace breakdown
|
||||
get newClientCounts() {
|
||||
return this.isDateRange ? null : this.byMonthActivityData[0]?.new_clients;
|
||||
}
|
||||
|
||||
// total client data for horizontal bar chart in attribution component
|
||||
get totalClientAttribution() {
|
||||
if (this.selectedNamespace) {
|
||||
return this.filteredActivityByNamespace?.mounts || null;
|
||||
} else {
|
||||
return this.getActivityResponse?.byNamespace || null;
|
||||
}
|
||||
}
|
||||
|
||||
// new client data for horizontal bar chart
|
||||
get newClientAttribution() {
|
||||
// new client attribution only available in a single, historical month (not a date range or current month)
|
||||
if (this.isDateRange || this.isCurrentMonth) return null;
|
||||
|
||||
if (this.selectedNamespace) {
|
||||
return this.newClientCounts?.mounts || null;
|
||||
} else {
|
||||
return this.newClientCounts?.namespaces || null;
|
||||
}
|
||||
}
|
||||
|
||||
get responseTimestamp() {
|
||||
return this.getActivityResponse.responseTimestamp;
|
||||
}
|
||||
|
||||
// FILTERS
|
||||
get filteredActivityByNamespace() {
|
||||
const namespace = this.selectedNamespace;
|
||||
const auth = this.selectedAuthMethod;
|
||||
if (!namespace && !auth) {
|
||||
return this.getActivityResponse;
|
||||
}
|
||||
if (!auth) {
|
||||
return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace);
|
||||
}
|
||||
return this.getActivityResponse.byNamespace
|
||||
.find((ns) => ns.label === namespace)
|
||||
.mounts?.find((mount) => mount.label === auth);
|
||||
}
|
||||
|
||||
get filteredActivityByMonth() {
|
||||
const namespace = this.selectedNamespace;
|
||||
const auth = this.selectedAuthMethod;
|
||||
if (!namespace && !auth) {
|
||||
return this.getActivityResponse?.byMonth;
|
||||
}
|
||||
const namespaceData = this.getActivityResponse?.byMonth
|
||||
.map((m) => m.namespaces_by_key[namespace])
|
||||
.filter((d) => d !== undefined);
|
||||
if (!auth) {
|
||||
return namespaceData.length === 0 ? null : namespaceData;
|
||||
}
|
||||
const mountData = namespaceData
|
||||
.map((namespace) => namespace.mounts_by_key[auth])
|
||||
.filter((d) => d !== undefined);
|
||||
return mountData.length === 0 ? null : mountData;
|
||||
}
|
||||
|
||||
@action
|
||||
async handleClientActivityQuery({ dateType, monthIdx, year }) {
|
||||
this.showBillingStartModal = false;
|
||||
switch (dateType) {
|
||||
case 'cancel':
|
||||
return;
|
||||
case 'reset': // clicked 'Current billing period' in calendar widget -> reset to initial start/end dates
|
||||
this.activityQueryParams.start.timestamp = this.args.model.licenseStartTimestamp;
|
||||
this.activityQueryParams.end.timestamp = this.args.model.currentDate;
|
||||
break;
|
||||
case 'currentMonth': // clicked 'Current month' from calendar widget
|
||||
this.activityQueryParams.start.timestamp = this.args.model.currentDate;
|
||||
this.activityQueryParams.end.timestamp = this.args.model.currentDate;
|
||||
break;
|
||||
case 'startDate': // from "Edit billing start" modal
|
||||
this.activityQueryParams.start = { monthIdx, year };
|
||||
this.activityQueryParams.end.timestamp = this.args.model.currentDate;
|
||||
break;
|
||||
case 'endDate': // selected month and year from calendar widget
|
||||
this.activityQueryParams.end = { monthIdx, year };
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
try {
|
||||
this.isLoadingQuery = true;
|
||||
const response = await this.store.queryRecord('clients/activity', {
|
||||
start_time: this.activityQueryParams.start,
|
||||
end_time: this.activityQueryParams.end,
|
||||
});
|
||||
// preference for byMonth timestamps because those correspond to a user's query
|
||||
const { byMonth } = response;
|
||||
this.startMonthTimestamp = byMonth[0]?.timestamp || response.startTime;
|
||||
this.endMonthTimestamp = byMonth[byMonth.length - 1]?.timestamp || response.endTime;
|
||||
if (response.id === 'no-data') {
|
||||
this.noActivityData = true;
|
||||
} else {
|
||||
this.noActivityData = false;
|
||||
getStorage().setItem('vault:ui-inputted-start-date', this.startMonthTimestamp);
|
||||
}
|
||||
this.queriedActivityResponse = response;
|
||||
|
||||
// reset search-select filters
|
||||
this.selectedNamespace = null;
|
||||
this.selectedAuthMethod = null;
|
||||
this.authMethodOptions = [];
|
||||
} catch (e) {
|
||||
this.errorObject = e;
|
||||
return e;
|
||||
} finally {
|
||||
this.isLoadingQuery = false;
|
||||
}
|
||||
}
|
||||
|
||||
get hasMultipleMonthsData() {
|
||||
return this.byMonthActivityData && this.byMonthActivityData.length > 1;
|
||||
}
|
||||
|
||||
@action
|
||||
selectNamespace([value]) {
|
||||
this.selectedNamespace = value;
|
||||
if (!value) {
|
||||
this.authMethodOptions = [];
|
||||
// on clear, also make sure auth method is cleared
|
||||
this.selectedAuthMethod = null;
|
||||
} else {
|
||||
// Side effect: set auth namespaces
|
||||
const mounts = this.filteredActivityByNamespace.mounts?.map((mount) => ({
|
||||
id: mount.label,
|
||||
name: mount.label,
|
||||
}));
|
||||
this.authMethodOptions = mounts;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
setAuthMethod([authMount]) {
|
||||
this.selectedAuthMethod = authMount;
|
||||
}
|
||||
|
||||
// validation function sent to <DateDropdown> selecting 'endDate'
|
||||
@action
|
||||
isEndBeforeStart(selection) {
|
||||
let { start } = this.activityQueryParams;
|
||||
start = start?.timestamp ? parseAPITimestamp(start.timestamp) : new Date(start.year, start.monthIdx);
|
||||
return isBefore(selection, start) && !isSameMonth(start, selection)
|
||||
? `End date must be after ${format(start, 'MMMM yyyy')}`
|
||||
: false;
|
||||
}
|
||||
}
|
|
@ -1,393 +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, isBefore } from 'date-fns';
|
||||
import getStorage from 'vault/lib/token-storage';
|
||||
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
|
||||
import { dateFormat } from 'core/helpers/date-format';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
|
||||
const INPUTTED_START_DATE = 'vault:ui-inputted-start-date';
|
||||
|
||||
export default class History extends Component {
|
||||
@service store;
|
||||
@service version;
|
||||
|
||||
arrayOfMonths = ARRAY_OF_MONTHS;
|
||||
|
||||
chartLegend = [
|
||||
{ key: 'entity_clients', label: 'entity clients' },
|
||||
{ key: 'non_entity_clients', label: 'non-entity clients' },
|
||||
];
|
||||
|
||||
// FOR START DATE EDIT & MODAL //
|
||||
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;
|
||||
});
|
||||
currentDate = new Date();
|
||||
currentYear = this.currentDate.getFullYear(); // integer of year
|
||||
currentMonth = this.currentDate.getMonth(); // index of month
|
||||
|
||||
@tracked isEditStartMonthOpen = false;
|
||||
@tracked startMonth = null;
|
||||
@tracked startYear = null;
|
||||
@tracked allowedMonthMax = 12;
|
||||
@tracked disabledYear = null;
|
||||
|
||||
// FOR HISTORY COMPONENT //
|
||||
|
||||
// RESPONSE
|
||||
@tracked endTimeFromResponse = this.args.model.endTimeFromResponse;
|
||||
@tracked startTimeFromResponse = this.args.model.startTimeFromLicense; // ex: ['2021', 3] is April 2021 (0 indexed)
|
||||
@tracked startTimeRequested = null;
|
||||
@tracked queriedActivityResponse = null;
|
||||
|
||||
// SEARCH SELECT
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked namespaceArray = this.getActivityResponse.byNamespace
|
||||
? this.getActivityResponse.byNamespace.map((namespace) => ({
|
||||
name: namespace.label,
|
||||
id: namespace.label,
|
||||
}))
|
||||
: [];
|
||||
@tracked selectedAuthMethod = null;
|
||||
@tracked authMethodOptions = [];
|
||||
|
||||
// TEMPLATE MESSAGING
|
||||
@tracked noActivityDate = '';
|
||||
@tracked responseRangeDiffMessage = null;
|
||||
@tracked isLoadingQuery = false;
|
||||
@tracked licenseStartIsCurrentMonth = this.args.model.activity?.isLicenseDateError || false;
|
||||
@tracked errorObject = null;
|
||||
|
||||
get versionText() {
|
||||
return this.version.isEnterprise
|
||||
? {
|
||||
label: 'Billing start month',
|
||||
description:
|
||||
'This date comes from your license, and defines when client counting starts. Without this starting point, the data shown is not reliable.',
|
||||
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.',
|
||||
}
|
||||
: {
|
||||
label: 'Client counting start date',
|
||||
description:
|
||||
'This date is when client counting starts. Without this starting point, the data shown is not reliable.',
|
||||
title: 'No start date found',
|
||||
message:
|
||||
'In order to get the most from this data, please enter a start month above. Vault will calculate new clients starting from that month.',
|
||||
};
|
||||
}
|
||||
|
||||
get isDateRange() {
|
||||
return !isSameMonth(
|
||||
parseAPITimestamp(this.getActivityResponse.startTime),
|
||||
parseAPITimestamp(this.getActivityResponse.endTime)
|
||||
);
|
||||
}
|
||||
|
||||
get upgradeVersionHistory() {
|
||||
const versionHistory = this.args.model.versionHistory;
|
||||
if (!versionHistory || versionHistory.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get upgrade data for initial upgrade to 1.9 and/or 1.10
|
||||
const relevantUpgrades = [];
|
||||
const importantUpgrades = ['1.9', '1.10'];
|
||||
importantUpgrades.forEach((version) => {
|
||||
const findUpgrade = versionHistory.find((versionData) => versionData.id.match(version));
|
||||
if (findUpgrade) relevantUpgrades.push(findUpgrade);
|
||||
});
|
||||
|
||||
// array of upgrade data objects for noteworthy upgrades
|
||||
return relevantUpgrades;
|
||||
}
|
||||
|
||||
get upgradeDuringActivity() {
|
||||
if (!this.upgradeVersionHistory) {
|
||||
return null;
|
||||
}
|
||||
const activityStart = new Date(this.getActivityResponse.startTime);
|
||||
const activityEnd = new Date(this.getActivityResponse.endTime);
|
||||
const upgradesWithinData = this.upgradeVersionHistory.filter((upgrade) => {
|
||||
// TODO how do timezones affect this?
|
||||
const upgradeDate = new Date(upgrade.timestampInstalled);
|
||||
return isAfter(upgradeDate, activityStart) && isBefore(upgradeDate, activityEnd);
|
||||
});
|
||||
// return all upgrades that happened within date range of queried activity
|
||||
return upgradesWithinData.length === 0 ? null : upgradesWithinData;
|
||||
}
|
||||
|
||||
get upgradeVersionAndDate() {
|
||||
if (!this.upgradeDuringActivity) {
|
||||
return null;
|
||||
}
|
||||
if (this.upgradeDuringActivity.length === 2) {
|
||||
const firstUpgrade = this.upgradeDuringActivity[0];
|
||||
const secondUpgrade = this.upgradeDuringActivity[1];
|
||||
const firstDate = dateFormat([firstUpgrade.timestampInstalled, 'MMM d, yyyy'], { isFormatted: true });
|
||||
const secondDate = dateFormat([secondUpgrade.timestampInstalled, 'MMM d, yyyy'], { isFormatted: true });
|
||||
return `Vault was upgraded to ${firstUpgrade.id} (${firstDate}) and ${secondUpgrade.id} (${secondDate}) during this time range.`;
|
||||
} else {
|
||||
const upgrade = this.upgradeDuringActivity[0];
|
||||
return `Vault was upgraded to ${upgrade.id} on ${dateFormat(
|
||||
[upgrade.timestampInstalled, 'MMM d, yyyy'],
|
||||
{ isFormatted: true }
|
||||
)}.`;
|
||||
}
|
||||
}
|
||||
|
||||
get versionSpecificText() {
|
||||
if (!this.upgradeDuringActivity) {
|
||||
return null;
|
||||
}
|
||||
if (this.upgradeDuringActivity.length === 1) {
|
||||
const version = this.upgradeDuringActivity[0].id;
|
||||
if (version.match('1.9')) {
|
||||
return ' How we count clients changed in 1.9, so keep that in mind when looking at the data below.';
|
||||
}
|
||||
if (version.match('1.10')) {
|
||||
return ' We added monthly breakdowns and mount level attribution starting in 1.10, so keep that in mind when looking at the data below.';
|
||||
}
|
||||
}
|
||||
// return combined explanation if spans multiple upgrades
|
||||
return ' How we count clients changed in 1.9 and we added monthly breakdowns and mount level attribution starting in 1.10. Keep this in mind when looking at the data below.';
|
||||
}
|
||||
|
||||
get startTimeDisplay() {
|
||||
if (!this.startTimeFromResponse) {
|
||||
return null;
|
||||
}
|
||||
const month = this.startTimeFromResponse[1];
|
||||
const year = this.startTimeFromResponse[0];
|
||||
return `${this.arrayOfMonths[month]} ${year}`;
|
||||
}
|
||||
|
||||
get endTimeDisplay() {
|
||||
if (!this.endTimeFromResponse) {
|
||||
return null;
|
||||
}
|
||||
const month = this.endTimeFromResponse[1];
|
||||
const year = this.endTimeFromResponse[0];
|
||||
return `${this.arrayOfMonths[month]} ${year}`;
|
||||
}
|
||||
|
||||
// GETTERS FOR RESPONSE & DATA
|
||||
|
||||
// on init API response uses license start_date, getter updates when user queries dates
|
||||
get getActivityResponse() {
|
||||
return this.queriedActivityResponse || this.args.model.activity;
|
||||
}
|
||||
|
||||
get byMonthActivityData() {
|
||||
if (this.selectedNamespace) {
|
||||
return this.filteredActivityByMonth;
|
||||
} else {
|
||||
return this.getActivityResponse?.byMonth;
|
||||
}
|
||||
}
|
||||
|
||||
get byMonthNewClients() {
|
||||
if (this.byMonthActivityData) {
|
||||
return this.byMonthActivityData?.map((m) => m.new_clients);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get hasAttributionData() {
|
||||
if (this.selectedAuthMethod) return false;
|
||||
if (this.selectedNamespace) {
|
||||
return this.authMethodOptions.length > 0;
|
||||
}
|
||||
return !!this.totalClientAttribution && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
|
||||
}
|
||||
|
||||
// (object) top level TOTAL client counts for given date range
|
||||
get totalUsageCounts() {
|
||||
return this.selectedNamespace ? this.filteredActivityByNamespace : this.getActivityResponse.total;
|
||||
}
|
||||
|
||||
// (object) single month new client data with total counts + array of namespace breakdown
|
||||
get newClientCounts() {
|
||||
return this.isDateRange ? null : this.byMonthActivityData[0]?.new_clients;
|
||||
}
|
||||
|
||||
// total client data for horizontal bar chart in attribution component
|
||||
get totalClientAttribution() {
|
||||
if (this.selectedNamespace) {
|
||||
return this.filteredActivityByNamespace?.mounts || null;
|
||||
} else {
|
||||
return this.getActivityResponse?.byNamespace || null;
|
||||
}
|
||||
}
|
||||
|
||||
// new client data for horizontal bar chart
|
||||
get newClientAttribution() {
|
||||
// new client attribution only available in a single, historical month (not a date range)
|
||||
if (this.isDateRange) return null;
|
||||
|
||||
if (this.selectedNamespace) {
|
||||
return this.newClientCounts?.mounts || null;
|
||||
} else {
|
||||
return this.newClientCounts?.namespaces || null;
|
||||
}
|
||||
}
|
||||
|
||||
get responseTimestamp() {
|
||||
return this.getActivityResponse.responseTimestamp;
|
||||
}
|
||||
|
||||
// FILTERS
|
||||
get filteredActivityByNamespace() {
|
||||
const namespace = this.selectedNamespace;
|
||||
const auth = this.selectedAuthMethod;
|
||||
if (!namespace && !auth) {
|
||||
return this.getActivityResponse;
|
||||
}
|
||||
if (!auth) {
|
||||
return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace);
|
||||
}
|
||||
return this.getActivityResponse.byNamespace
|
||||
.find((ns) => ns.label === namespace)
|
||||
.mounts?.find((mount) => mount.label === auth);
|
||||
}
|
||||
|
||||
get filteredActivityByMonth() {
|
||||
const namespace = this.selectedNamespace;
|
||||
const auth = this.selectedAuthMethod;
|
||||
if (!namespace && !auth) {
|
||||
return this.getActivityResponse?.byMonth;
|
||||
}
|
||||
const namespaceData = this.getActivityResponse?.byMonth
|
||||
.map((m) => m.namespaces_by_key[namespace])
|
||||
.filter((d) => d !== undefined);
|
||||
if (!auth) {
|
||||
return namespaceData.length === 0 ? null : namespaceData;
|
||||
}
|
||||
const mountData = namespaceData
|
||||
.map((namespace) => namespace.mounts_by_key[auth])
|
||||
.filter((d) => d !== undefined);
|
||||
return mountData.length === 0 ? null : mountData;
|
||||
}
|
||||
|
||||
@action
|
||||
async handleClientActivityQuery(month, year, dateType) {
|
||||
this.isEditStartMonthOpen = false;
|
||||
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 History which opens a modal.
|
||||
if (dateType === 'startTime') {
|
||||
const monthIndex = this.arrayOfMonths.indexOf(month);
|
||||
this.startTimeRequested = [year.toString(), monthIndex]; // ['2021', 0] (e.g. January 2021)
|
||||
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 {
|
||||
this.isLoadingQuery = true;
|
||||
const response = await this.store.queryRecord('clients/activity', {
|
||||
start_time: this.startTimeRequested,
|
||||
end_time: this.endTimeRequested,
|
||||
});
|
||||
if (response.id === 'no-data') {
|
||||
// empty response (204) 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 the @tracked startTimeFromResponse
|
||||
this.startTimeFromResponse = response.formattedStartTime;
|
||||
this.endTimeFromResponse = response.formattedEndTime;
|
||||
this.storage().setItem(INPUTTED_START_DATE, this.startTimeFromResponse);
|
||||
}
|
||||
this.queriedActivityResponse = response;
|
||||
this.licenseStartIsCurrentMonth = response.isLicenseDateError;
|
||||
// compare if the response startTime comes after the requested startTime. If true throw a warning.
|
||||
// only display if they selected a startTime
|
||||
if (
|
||||
dateType === 'startTime' &&
|
||||
isAfter(
|
||||
new Date(this.getActivityResponse.startTime),
|
||||
new Date(this.startTimeRequested[0], this.startTimeRequested[1])
|
||||
)
|
||||
) {
|
||||
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;
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorObject = e;
|
||||
return e;
|
||||
} finally {
|
||||
this.isLoadingQuery = false;
|
||||
}
|
||||
}
|
||||
|
||||
get hasMultipleMonthsData() {
|
||||
return this.byMonthActivityData && this.byMonthActivityData.length > 1;
|
||||
}
|
||||
|
||||
@action
|
||||
handleCurrentBillingPeriod() {
|
||||
this.handleClientActivityQuery(0, 0, 'reset');
|
||||
}
|
||||
|
||||
@action
|
||||
selectNamespace([value]) {
|
||||
// value comes in as [namespace0]
|
||||
this.selectedNamespace = value;
|
||||
if (!value) {
|
||||
this.authMethodOptions = [];
|
||||
// on clear, also make sure auth method is cleared
|
||||
this.selectedAuthMethod = null;
|
||||
} else {
|
||||
// Side effect: set auth namespaces
|
||||
const mounts = this.filteredActivityByNamespace.mounts?.map((mount) => ({
|
||||
id: mount.label,
|
||||
name: mount.label,
|
||||
}));
|
||||
this.authMethodOptions = mounts;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
setAuthMethod([authMount]) {
|
||||
this.selectedAuthMethod = authMount;
|
||||
}
|
||||
|
||||
// FOR START DATE MODAL
|
||||
@action
|
||||
selectStartMonth(month, event) {
|
||||
this.startMonth = month;
|
||||
// disables months if in the future
|
||||
this.disabledYear = this.months.indexOf(month) >= this.currentMonth ? this.currentYear : null;
|
||||
event.close();
|
||||
}
|
||||
|
||||
@action
|
||||
selectStartYear(year, event) {
|
||||
this.startYear = year;
|
||||
this.allowedMonthMax = year === this.currentYear ? this.currentMonth : 12;
|
||||
event.close();
|
||||
}
|
||||
|
||||
storage() {
|
||||
return getStorage();
|
||||
}
|
||||
}
|
|
@ -54,7 +54,7 @@ export default class LineChart extends Component {
|
|||
} else if (!Object.keys(upgradeData[0]).includes('timestampInstalled')) {
|
||||
// eslint-disable-next-line
|
||||
console.debug(
|
||||
`upgrade must be an object with the following key names: ['id', 'previousVersion', 'timestampInstalled']`
|
||||
`upgrade must be an object with the following key names: ['version', 'previousVersion', 'timestampInstalled']`
|
||||
);
|
||||
return null;
|
||||
} else {
|
||||
|
@ -184,9 +184,9 @@ export default class LineChart extends Component {
|
|||
this.tooltipUpgradeText = '';
|
||||
const upgradeInfo = findUpgradeData(data);
|
||||
if (upgradeInfo) {
|
||||
const { id, previousVersion } = upgradeInfo;
|
||||
const { version, previousVersion } = upgradeInfo;
|
||||
this.tooltipUpgradeText = `Vault was upgraded
|
||||
${previousVersion ? 'from ' + previousVersion : ''} to ${id}`;
|
||||
${previousVersion ? 'from ' + previousVersion : ''} to ${version}`;
|
||||
}
|
||||
|
||||
const node = hoverCircles.filter((plot) => plot[this.xKey] === data[this.xKey]).node();
|
||||
|
|
|
@ -9,7 +9,7 @@ import { calculateAverage } from 'vault/utils/chart-helpers';
|
|||
* ```js
|
||||
<Clients::MonthlyUsage
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
@responseTimestamp={{this.responseTimestamp}}
|
||||
@verticalBarChartData={{this.byMonthActivityData}}
|
||||
/>
|
||||
* ```
|
||||
|
|
|
@ -12,8 +12,7 @@ import { calculateAverage } from 'vault/utils/chart-helpers';
|
|||
<Clients::RunningTotal
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@barChartData={{this.byMonthNewClients}}
|
||||
@lineChartData={{this.byMonth}}
|
||||
@byMonthActivityData={{this.byMonth}}
|
||||
@runningTotals={{this.runningTotals}}
|
||||
@upgradeData={{if this.countsIncludeOlderData this.latestUpgradeData}}
|
||||
/>
|
||||
|
@ -21,38 +20,44 @@ import { calculateAverage } from 'vault/utils/chart-helpers';
|
|||
|
||||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||
* @param {string} selectedAuthMethod - string of auth method label for empty state message in bar chart
|
||||
* @param {array} barChartData - array of objects from /activity response, from the 'months' key
|
||||
object example: {
|
||||
* @param {array} byMonthActivityData - array of objects from /activity response, from the 'months' key, includes total and new clients per month
|
||||
object structure: {
|
||||
month: '1/22',
|
||||
entity_clients: 23,
|
||||
non_entity_clients: 45,
|
||||
total: 68,
|
||||
clients: 68,
|
||||
namespaces: [],
|
||||
new_clients: {
|
||||
entity_clients: 11,
|
||||
non_entity_clients: 36,
|
||||
total: 47,
|
||||
clients: 47,
|
||||
namespaces: [],
|
||||
},
|
||||
};
|
||||
* @param {array} lineChartData - array of objects from /activity response, from the 'months' key
|
||||
* @param {object} runningTotals - top level totals from /activity response { clients: 3517, entity_clients: 1593, non_entity_clients: 1924 }
|
||||
* @param {object} upgradeData - object containing version upgrade data e.g.: {id: '1.9.0', previousVersion: null, timestampInstalled: '2021-11-03T10:23:16Z'}
|
||||
* @param {object} upgradeData - object containing version upgrade data e.g.: {version: '1.9.0', previousVersion: null, timestampInstalled: '2021-11-03T10:23:16Z'}
|
||||
* @param {string} timestamp - ISO timestamp created in serializer to timestamp the response
|
||||
*
|
||||
*/
|
||||
export default class RunningTotal extends Component {
|
||||
get byMonthNewClients() {
|
||||
if (this.args.byMonthActivityData) {
|
||||
return this.args.byMonthActivityData?.map((m) => m.new_clients);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get entityClientData() {
|
||||
return {
|
||||
runningTotal: this.args.runningTotals.entity_clients,
|
||||
averageNewClients: calculateAverage(this.args.barChartData, 'entity_clients') || '0',
|
||||
averageNewClients: calculateAverage(this.byMonthNewClients, 'entity_clients'),
|
||||
};
|
||||
}
|
||||
|
||||
get nonEntityClientData() {
|
||||
return {
|
||||
runningTotal: this.args.runningTotals.non_entity_clients,
|
||||
averageNewClients: calculateAverage(this.args.barChartData, 'non_entity_clients') || '0',
|
||||
averageNewClients: calculateAverage(this.byMonthNewClients, 'non_entity_clients'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -70,22 +75,7 @@ export default class RunningTotal extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
get showSingleMonth() {
|
||||
if (this.args.lineChartData?.length === 1) {
|
||||
const monthData = this.args?.lineChartData[0];
|
||||
return {
|
||||
total: {
|
||||
total: monthData.clients,
|
||||
entityClients: monthData.entity_clients,
|
||||
nonEntityClients: monthData.non_entity_clients,
|
||||
},
|
||||
new: {
|
||||
total: monthData.new_clients.clients,
|
||||
entityClients: monthData.new_clients.entity_clients,
|
||||
nonEntityClients: monthData.new_clients.non_entity_clients,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
get singleMonthData() {
|
||||
return this.args?.byMonthActivityData[0];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,13 +93,15 @@ export default class VerticalBarChart extends Component {
|
|||
const tooltipTether = chartSvg
|
||||
.append('g')
|
||||
.attr('transform', `translate(${BAR_WIDTH / 2})`)
|
||||
.attr('data-test-vertical-chart', 'tool-tip-tethers')
|
||||
.attr('data-test-vertical-chart', 'tooltip-tethers')
|
||||
.selectAll('circle')
|
||||
.data(filteredData)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.style('opacity', '0')
|
||||
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
|
||||
.attr('cx', (d) => xScale(d[this.xKey]));
|
||||
.attr('cx', (d) => xScale(d[this.xKey]))
|
||||
.attr('r', 1);
|
||||
|
||||
// MAKE AXES //
|
||||
const yAxisScale = scaleLinear()
|
||||
|
|
|
@ -1,53 +1,79 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
|
||||
/**
|
||||
* @module DateDropdown
|
||||
* DateDropdown components are used to display a dropdown of months and years to handle date selection
|
||||
* DateDropdown components are used to display a dropdown of months and years to handle date selection. Future dates are disabled (current month and year are selectable).
|
||||
* The component returns an object with selected date info, example: { dateType: 'start', monthIdx: 0, monthName: 'January', year: 2022 }
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <DateDropdown @handleDateSelection={this.actionFromParent} @name={{"startTime"}} @submitText="Save"/>
|
||||
* <DateDropdown @handleSubmit={{this.actionFromParent}} @name="startTime" @submitText="Save" @handleCancel={{this.onCancel}}/>
|
||||
* ```
|
||||
* @param {function} handleDateSelection - is the action from the parent that the date picker triggers
|
||||
* @param {string} [name] - optional argument passed from date dropdown to parent function
|
||||
* @param {function} handleSubmit - callback function from parent that the date picker triggers on submit
|
||||
* @param {function} [handleCancel] - optional callback for cancel action, if exists then buttons appear modal style with a light gray background
|
||||
* @param {string} [dateType] - optional argument to give the selected month/year a type
|
||||
* @param {string} [submitText] - optional argument to change submit button text
|
||||
* @param {function} [validateDate] - parent function to validate date selection, receives date object and returns an error message that's passed to the inline alert
|
||||
*/
|
||||
export default class DateDropdown extends Component {
|
||||
currentDate = new Date();
|
||||
currentYear = this.currentDate.getFullYear(); // integer of year
|
||||
currentMonth = this.currentDate.getMonth(); // index of month
|
||||
currentMonthIdx = this.currentDate.getMonth(); // integer of month, 0 indexed
|
||||
dropdownMonths = ARRAY_OF_MONTHS.map((m, i) => ({ name: m, index: i }));
|
||||
dropdownYears = Array.from({ length: 5 }, (item, i) => this.currentYear - i);
|
||||
|
||||
@tracked allowedMonthMax = 12;
|
||||
@tracked disabledYear = null;
|
||||
@tracked startMonth = null;
|
||||
@tracked startYear = null;
|
||||
|
||||
months = Array.from({ length: 12 }, (item, i) => {
|
||||
return new Date(0, i).toLocaleString('en-US', { month: 'long' });
|
||||
});
|
||||
years = Array.from({ length: 5 }, (item, i) => {
|
||||
return new Date().getFullYear() - i;
|
||||
});
|
||||
@tracked maxMonthIdx = 11; // disables months with index greater than this number, initially all months are selectable
|
||||
@tracked disabledYear = null; // year as integer if current year should be disabled
|
||||
@tracked selectedMonth = null;
|
||||
@tracked selectedYear = null;
|
||||
@tracked invalidDate = null;
|
||||
|
||||
@action
|
||||
selectStartMonth(month, event) {
|
||||
this.startMonth = month;
|
||||
// disables months if in the future
|
||||
this.disabledYear = this.months.indexOf(month) >= this.currentMonth ? this.currentYear : null;
|
||||
event.close();
|
||||
selectMonth(month, dropdown) {
|
||||
this.selectedMonth = month;
|
||||
// disable current year if selected month is later than current month
|
||||
this.disabledYear = month.index > this.currentMonthIdx ? this.currentYear : null;
|
||||
dropdown.close();
|
||||
}
|
||||
|
||||
@action
|
||||
selectStartYear(year, event) {
|
||||
this.startYear = year;
|
||||
this.allowedMonthMax = year === this.currentYear ? this.currentMonth : 12;
|
||||
event.close();
|
||||
selectYear(year, dropdown) {
|
||||
this.selectedYear = year;
|
||||
// disable months after current month if selected year is current year
|
||||
this.maxMonthIdx = year === this.currentYear ? this.currentMonthIdx : 11;
|
||||
dropdown.close();
|
||||
}
|
||||
|
||||
@action
|
||||
saveDateSelection() {
|
||||
this.args.handleDateSelection(this.startMonth, this.startYear, this.args.name);
|
||||
handleSubmit() {
|
||||
if (this.args.validateDate) {
|
||||
this.invalidDate = null;
|
||||
this.invalidDate = this.args.validateDate(new Date(this.selectedYear, this.selectedMonth.index));
|
||||
if (this.invalidDate) return;
|
||||
}
|
||||
const { index, name } = this.selectedMonth;
|
||||
this.args.handleSubmit({
|
||||
monthIdx: index,
|
||||
monthName: name,
|
||||
year: this.selectedYear,
|
||||
dateType: this.args.dateType,
|
||||
});
|
||||
this.resetDropdown();
|
||||
}
|
||||
|
||||
@action
|
||||
handleCancel() {
|
||||
this.args.handleCancel();
|
||||
this.resetDropdown();
|
||||
}
|
||||
|
||||
resetDropdown() {
|
||||
this.maxMonthIdx = 11;
|
||||
this.disabledYear = null;
|
||||
this.selectedMonth = null;
|
||||
this.selectedYear = null;
|
||||
this.invalidDate = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@ export default class Activity extends Model {
|
|||
@attr('array') byMonth;
|
||||
@attr('array') byNamespace;
|
||||
@attr('object') total;
|
||||
@attr('array') formattedEndTime;
|
||||
@attr('array') formattedStartTime;
|
||||
@attr('string') startTime;
|
||||
@attr('string') endTime;
|
||||
@attr('string') responseTimestamp;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
|||
import { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
|
||||
const M = Model.extend({
|
||||
queriesAvailable: attr('boolean'),
|
||||
queriesAvailable: attr('boolean'), // true only if historical data exists, will be false if there is only current month data
|
||||
retentionMonths: attr('number', {
|
||||
label: 'Retention period',
|
||||
subText: 'The number of months of activity logs to maintain for client tracking.',
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
export default class MonthlyModel extends Model {
|
||||
@attr('string') responseTimestamp;
|
||||
@attr('object') total; // total clients during the current/partial month
|
||||
@attr('array') byNamespace;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
export default class VersionHistoryModel extends Model {
|
||||
@attr('string') version;
|
||||
@attr('string') previousVersion;
|
||||
@attr('string') timestampInstalled;
|
||||
}
|
||||
|
|
|
@ -20,8 +20,7 @@ Router.map(function () {
|
|||
this.route('license');
|
||||
this.route('mfa-setup');
|
||||
this.route('clients', function () {
|
||||
this.route('current');
|
||||
this.route('history');
|
||||
this.route('dashboard');
|
||||
this.route('config');
|
||||
this.route('edit');
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import RSVP from 'rsvp';
|
||||
import { hash } from 'rsvp';
|
||||
import { action } from '@ember/object';
|
||||
import getStorage from 'vault/lib/token-storage';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
@ -8,32 +8,24 @@ const INPUTTED_START_DATE = 'vault:ui-inputted-start-date';
|
|||
export default class ClientsRoute extends Route {
|
||||
@service store;
|
||||
async getVersionHistory() {
|
||||
try {
|
||||
const arrayOfModels = [];
|
||||
const response = await this.store.findAll('clients/version-history'); // returns a class with nested models
|
||||
response.forEach((model) => {
|
||||
arrayOfModels.push({
|
||||
id: model.id,
|
||||
previousVersion: model.previousVersion,
|
||||
timestampInstalled: model.timestampInstalled,
|
||||
return this.store
|
||||
.findAll('clients/version-history')
|
||||
.then((response) => {
|
||||
return response.map(({ version, previousVersion, timestampInstalled }) => {
|
||||
return {
|
||||
version,
|
||||
previousVersion,
|
||||
timestampInstalled,
|
||||
};
|
||||
});
|
||||
});
|
||||
return arrayOfModels;
|
||||
} catch (e) {
|
||||
console.debug(e); // eslint-disable-line
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.catch(() => []);
|
||||
}
|
||||
|
||||
async model() {
|
||||
const config = await this.store.queryRecord('clients/config', {}).catch((e) => {
|
||||
console.debug(e); // eslint-disable-line
|
||||
// swallowing error so activity can show if no config permissions
|
||||
return {};
|
||||
});
|
||||
|
||||
return RSVP.hash({
|
||||
config,
|
||||
model() {
|
||||
// swallow config error so activity can show if no config permissions
|
||||
return hash({
|
||||
config: this.store.queryRecord('clients/config', {}).catch(() => {}),
|
||||
versionHistory: this.getVersionHistory(),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import RSVP from 'rsvp';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class CurrentRoute extends Route {
|
||||
@service store;
|
||||
|
||||
async model() {
|
||||
const parentModel = this.modelFor('vault.cluster.clients');
|
||||
|
||||
return RSVP.hash({
|
||||
config: parentModel.config,
|
||||
monthly: await this.store.queryRecord('clients/monthly', {}),
|
||||
versionHistory: parentModel.versionHistory,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import getStorage from 'vault/lib/token-storage';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class DashboardRoute extends Route {
|
||||
@service store;
|
||||
currentDate = new Date().toISOString();
|
||||
|
||||
async getActivity(start_time) {
|
||||
// on init ONLY make network request if we have a start_time
|
||||
return start_time
|
||||
? await this.store.queryRecord('clients/activity', {
|
||||
start_time: { timestamp: start_time },
|
||||
end_time: { timestamp: this.currentDate },
|
||||
})
|
||||
: {};
|
||||
}
|
||||
|
||||
async getLicenseStartTime() {
|
||||
try {
|
||||
const license = await this.store.queryRecord('license', {});
|
||||
// if license.startTime is 'undefined' return 'null' for consistency
|
||||
return license.startTime || getStorage().getItem('vault:ui-inputted-start-date') || null;
|
||||
} catch (e) {
|
||||
// return null so user can input date manually
|
||||
// if already inputted manually, will be in localStorage
|
||||
return getStorage().getItem('vault:ui-inputted-start-date') || null;
|
||||
}
|
||||
}
|
||||
|
||||
async model() {
|
||||
const { config, versionHistory } = this.modelFor('vault.cluster.clients');
|
||||
const licenseStart = await this.getLicenseStartTime();
|
||||
const activity = await this.getActivity(licenseStart);
|
||||
return {
|
||||
config,
|
||||
versionHistory,
|
||||
activity,
|
||||
licenseStartTimestamp: licenseStart,
|
||||
currentDate: this.currentDate,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { isSameMonth } from 'date-fns';
|
||||
import RSVP from 'rsvp';
|
||||
import getStorage from 'vault/lib/token-storage';
|
||||
import { parseRFC3339 } from 'core/utils/date-formatters';
|
||||
import { inject as service } from '@ember/service';
|
||||
const INPUTTED_START_DATE = 'vault:ui-inputted-start-date';
|
||||
|
||||
export default class HistoryRoute extends Route {
|
||||
@service store;
|
||||
|
||||
async getActivity(start_time) {
|
||||
if (isSameMonth(new Date(start_time), new Date())) {
|
||||
// triggers empty state to manually enter date if license begins in current month
|
||||
return { isLicenseDateError: true };
|
||||
}
|
||||
// on init ONLY make network request if we have a start_time
|
||||
return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {};
|
||||
}
|
||||
|
||||
async getLicenseStartTime() {
|
||||
try {
|
||||
const license = await this.store.queryRecord('license', {});
|
||||
// if license.startTime is 'undefined' return 'null' for consistency
|
||||
return license.startTime || getStorage().getItem(INPUTTED_START_DATE) || null;
|
||||
} catch (e) {
|
||||
// return null so user can input date manually
|
||||
// if already inputted manually, will be in localStorage
|
||||
return getStorage().getItem(INPUTTED_START_DATE) || null;
|
||||
}
|
||||
}
|
||||
|
||||
async model() {
|
||||
const parentModel = this.modelFor('vault.cluster.clients');
|
||||
const licenseStart = await this.getLicenseStartTime();
|
||||
const activity = await this.getActivity(licenseStart);
|
||||
|
||||
return RSVP.hash({
|
||||
config: parentModel.config,
|
||||
activity,
|
||||
startTimeFromLicense: parseRFC3339(licenseStart),
|
||||
endTimeFromResponse: parseRFC3339(activity?.endTime),
|
||||
versionHistory: parentModel.versionHistory,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { parseRFC3339 } from 'core/utils/date-formatters';
|
||||
import { formatByMonths, formatByNamespace, homogenizeClientNaming } from 'core/utils/client-count-utils';
|
||||
export default class ActivitySerializer extends ApplicationSerializer {
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
|
@ -14,8 +13,6 @@ export default class ActivitySerializer extends ApplicationSerializer {
|
|||
by_namespace: formatByNamespace(payload.data.by_namespace),
|
||||
by_month: formatByMonths(payload.data.months),
|
||||
total: homogenizeClientNaming(payload.data.total),
|
||||
formatted_end_time: parseRFC3339(payload.data.end_time),
|
||||
formatted_start_time: parseRFC3339(payload.data.start_time),
|
||||
};
|
||||
delete payload.data.by_namespace;
|
||||
delete payload.data.months;
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { formatByNamespace, homogenizeClientNaming } from 'core/utils/client-count-utils';
|
||||
|
||||
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);
|
||||
}
|
||||
const response_timestamp = formatISO(new Date());
|
||||
// TODO CMB: the following is assumed, need to confirm
|
||||
// the months array will always include a single object: a timestamp of the current month and new/total count data, if available
|
||||
const transformedPayload = {
|
||||
...payload,
|
||||
response_timestamp,
|
||||
by_namespace: formatByNamespace(payload.data.by_namespace),
|
||||
// nest within 'total' object to mimic /activity response shape
|
||||
total: homogenizeClientNaming(payload.data),
|
||||
};
|
||||
delete payload.data.by_namespace;
|
||||
delete payload.data.months;
|
||||
delete payload.data.total;
|
||||
return super.normalizeResponse(store, primaryModelClass, transformedPayload, id, requestType);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
export default class VersionHistorySerializer extends ApplicationSerializer {
|
||||
primaryKey = 'version';
|
||||
|
||||
normalizeItems(payload) {
|
||||
if (payload.data.keys && Array.isArray(payload.data.keys)) {
|
||||
return payload.data.keys.map((key) => {
|
||||
const model = payload.data.key_info[key];
|
||||
model.id = key;
|
||||
return model;
|
||||
});
|
||||
return payload.data.keys.map((key) => ({ version: key, ...payload.data.key_info[key] }));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,12 +82,6 @@ $dark-gray: #535f73;
|
|||
color: lighten($dark-gray, 30%);
|
||||
pointer-events: none;
|
||||
}
|
||||
&.is-selected-month {
|
||||
background-color: lighten($dark-gray, 30%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -91,3 +91,20 @@ pre {
|
|||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// customize spacing (.modal-card-body is restricted to padding: 20px)
|
||||
.modal-card-custom {
|
||||
background-color: white;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: auto;
|
||||
|
||||
&.has-padding-m {
|
||||
padding: $spacing-m;
|
||||
}
|
||||
|
||||
&.has-padding-btm-left {
|
||||
padding-bottom: $spacing-m;
|
||||
padding-left: $spacing-m;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -225,6 +225,9 @@
|
|||
.has-top-padding-l {
|
||||
padding-top: $spacing-l;
|
||||
}
|
||||
.has-left-padding-l {
|
||||
padding-left: $spacing-l;
|
||||
}
|
||||
.has-top-padding-xxl {
|
||||
padding-top: $spacing-xxl;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
>
|
||||
{{@startTimeDisplay}}
|
||||
{{date-format this.startDate "MMM yyyy"}}
|
||||
-
|
||||
{{@endTimeDisplay}}
|
||||
{{date-format this.endDate "MMM yyyy"}}
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</D.Trigger>
|
||||
<D.Content @defaultClass={{concat "popup-menu-content calendar-content" (if this.showCalendar " calendar-open")}}>
|
||||
|
@ -15,12 +15,24 @@
|
|||
DATE OPTIONS
|
||||
</div>
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
<button
|
||||
data-test-current-month
|
||||
class="link link-plain has-text-weight-semibold is-ghost"
|
||||
type="button"
|
||||
name="currentMonth"
|
||||
{{on "click" (fn this.handleDateShortcut D.actions)}}
|
||||
>
|
||||
Current month
|
||||
</button>
|
||||
</li>
|
||||
<li class="action">
|
||||
<button
|
||||
data-test-current-billing-period
|
||||
class="link link-plain has-text-weight-semibold is-ghost"
|
||||
type="button"
|
||||
{{on "click" (fn this.selectCurrentBillingPeriod D)}}
|
||||
name="reset"
|
||||
{{on "click" (fn this.handleDateShortcut D.actions)}}
|
||||
>
|
||||
Current billing period
|
||||
</button>
|
||||
|
@ -65,7 +77,7 @@
|
|||
{{this.displayYear}}
|
||||
</p>
|
||||
<button
|
||||
data-test-future-year
|
||||
data-test-next-year
|
||||
type="button"
|
||||
class={{concat "button is-transparent " (if (or this.tooltipTarget) "negative-margin" "padding-right")}}
|
||||
disabled={{this.disableFutureYear}}
|
||||
|
@ -93,15 +105,15 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
<div class="calendar-widget-grid calendar-widget">
|
||||
{{#each this.widgetMonths as |m|}}
|
||||
{{#each this.widgetMonths as |month|}}
|
||||
<button
|
||||
data-test-calendar-month={{m.month}}
|
||||
data-test-calendar-month={{month.name}}
|
||||
type="button"
|
||||
class="is-month-list {{if m.readonly 'is-readOnly'}} {{if (eq m.id this.selectedMonthId) 'is-selected-month'}}"
|
||||
id={{m.id}}
|
||||
{{on "click" (fn this.selectEndMonth m.id D)}}
|
||||
class="is-month-list {{if month.readonly 'is-readOnly'}}"
|
||||
id={{month.index}}
|
||||
{{on "click" (fn this.selectMonth month D.actions)}}
|
||||
>
|
||||
{{m.month}}
|
||||
{{month.name}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{{! show single horizontal bar chart unless data is from a single, historical month (isDateRange = false) }}
|
||||
{{! only show side-by-side horizontal bar charts if data is from a single, historical month }}
|
||||
<div
|
||||
class={{concat "chart-wrapper" (if (or @isCurrentMonth @isDateRange) " single-chart-grid" " dual-chart-grid")}}
|
||||
class={{concat "chart-wrapper" (if @isHistoricalMonth " dual-chart-grid" " single-chart-grid")}}
|
||||
data-test-clients-attribution
|
||||
>
|
||||
<div class="chart-header has-header-link has-bottom-margin-m">
|
||||
|
@ -21,12 +21,32 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.barChartTotalClients}}
|
||||
{{#if (or @isDateRange @isCurrentMonth)}}
|
||||
{{#if @isHistoricalMonth}}
|
||||
<div class="chart-container-left" data-test-chart-container="new-clients">
|
||||
<h2 class="chart-title">New clients</h2>
|
||||
<p class="chart-description">{{this.chartText.newCopy}}</p>
|
||||
<Clients::HorizontalBarChart
|
||||
@dataset={{this.barChartNewClients}}
|
||||
@chartLegend={{@chartLegend}}
|
||||
@totalCounts={{@newUsageCounts}}
|
||||
@noDataMessage="There are no new clients for this namespace during this time period."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="chart-container-right" data-test-chart-container="total-clients">
|
||||
<h2 class="chart-title">Total clients</h2>
|
||||
<p class="chart-description">{{this.chartText.totalCopy}}</p>
|
||||
<Clients::HorizontalBarChart
|
||||
@dataset={{this.barChartTotalClients}}
|
||||
@chartLegend={{@chartLegend}}
|
||||
@totalCounts={{@totalUsageCounts}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<div
|
||||
class={{concat (unless this.barChartTotalClients "chart-empty-state ") "chart-container-wide"}}
|
||||
data-test-chart-container="total-clients"
|
||||
data-test-chart-container="single-chart"
|
||||
>
|
||||
<Clients::HorizontalBarChart
|
||||
@dataset={{this.barChartTotalClients}}
|
||||
|
@ -47,27 +67,6 @@
|
|||
<h3 class="data-details">Clients in {{this.attributionBreakdown}}</h3>
|
||||
<p class="data-details">{{format-number this.topClientCounts.clients}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="chart-container-left" data-test-chart-container="new-clients">
|
||||
<h2 class="chart-title">New clients</h2>
|
||||
<p class="chart-description">{{this.chartText.newCopy}}</p>
|
||||
<Clients::HorizontalBarChart
|
||||
@dataset={{this.barChartNewClients}}
|
||||
@chartLegend={{@chartLegend}}
|
||||
@totalCounts={{@newUsageCounts}}
|
||||
@noDataMessage={{"There are no new clients for this namespace during this time period."}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="chart-container-right" data-test-chart-container="total-clients">
|
||||
<h2 class="chart-title">Total clients</h2>
|
||||
<p class="chart-description">{{this.chartText.totalCopy}}</p>
|
||||
<Clients::HorizontalBarChart
|
||||
@dataset={{this.barChartTotalClients}}
|
||||
@chartLegend={{@chartLegend}}
|
||||
@totalCounts={{@totalUsageCounts}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="legend-center">
|
||||
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
||||
|
@ -79,12 +78,13 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
<div class="timestamp" data-test-attribution-timestamp>
|
||||
{{#if @timestamp}}
|
||||
{{#if @responseTimestamp}}
|
||||
Updated
|
||||
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
|
||||
{{date-format @responseTimestamp "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{! MODAL FOR CSV DOWNLOAD }}
|
||||
<Modal
|
||||
@title="Export attribution data"
|
||||
|
@ -97,17 +97,30 @@
|
|||
<p class="has-bottom-margin-s">
|
||||
This export will include the namespace path, authentication method path, and the associated total, entity, and
|
||||
non-entity clients for the below
|
||||
{{if @isCurrentMonth "month" "date range"}}.
|
||||
{{if this.formattedEndDate "date range" "month"}}.
|
||||
</p>
|
||||
<p class="has-bottom-margin-s is-subtitle-gray">SELECTED DATE {{if @endTimeDisplay " RANGE"}}</p>
|
||||
<p class="has-bottom-margin-s">{{@startTimeDisplay}} {{if @endTimeDisplay "-"}} {{@endTimeDisplay}}</p>
|
||||
<p class="has-bottom-margin-s is-subtitle-gray">SELECTED DATE {{if this.formattedEndDate " RANGE"}}</p>
|
||||
<p class="has-bottom-margin-s">{{this.formattedStartDate}} {{if this.formattedEndDate "-"}} {{this.formattedEndDate}}</p>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button type="button" class="button is-primary" {{on "click" (fn this.exportChartData this.getCsvFileName)}}>
|
||||
<button type="button" class="button is-primary" {{on "click" (fn this.exportChartData this.formattedCsvFileName)}}>
|
||||
Export
|
||||
</button>
|
||||
<button type="button" class="button is-secondary" onclick={{action (mut this.showCSVDownloadModal) false}}>
|
||||
Cancel
|
||||
</button>
|
||||
{{#if @upgradeExplanation}}
|
||||
<div class="has-text-grey is-size-8">
|
||||
<AlertInline @type="warning" @isMarginless={{true}}>
|
||||
Your data contains an upgrade.
|
||||
<DocLink
|
||||
@path="/vault/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts"
|
||||
>
|
||||
Learn more here.
|
||||
</DocLink>
|
||||
</AlertInline>
|
||||
<p class="has-left-padding-l">{{@upgradeExplanation}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</footer>
|
||||
</Modal>
|
|
@ -1,86 +0,0 @@
|
|||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
<p class="has-bottom-margin-s">
|
||||
The below data is for the current month starting from the first day. For historical data, see the 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>
|
||||
{{else if this.isGatheringData}}
|
||||
<EmptyState
|
||||
@title="No data received"
|
||||
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
|
||||
/>
|
||||
{{else if (eq @model.monthly.id "no-data")}}
|
||||
<EmptyState
|
||||
@title="No data available"
|
||||
@message="Tracking may be turned off. Check with your administrator or check back later."
|
||||
/>
|
||||
{{else}}
|
||||
<div class="is-subtitle-gray has-bottom-margin-m">
|
||||
FILTERS
|
||||
<Toolbar>
|
||||
<ToolbarFilters data-test-clients-filter-bar>
|
||||
<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}}
|
||||
class="is-marginless"
|
||||
/>
|
||||
{{#if (not (is-empty this.authMethodOptions))}}
|
||||
<SearchSelect
|
||||
@id="auth-method-search-select"
|
||||
@options={{this.authMethodOptions}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.setAuthMethod}}
|
||||
@placeholder={{"Filter by auth method"}}
|
||||
@displayInherit={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
</ToolbarFilters>
|
||||
</Toolbar>
|
||||
</div>
|
||||
{{#if this.upgradeDuringCurrentMonth}}
|
||||
<AlertBanner @type="warning" @title="Warning">
|
||||
{{this.upgradeVersionAndDate}}
|
||||
{{this.versionSpecificText}}
|
||||
<DocLink @path="/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts">
|
||||
Learn more here.
|
||||
</DocLink>
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
{{#if this.totalUsageCounts}}
|
||||
<Clients::UsageStats
|
||||
@title={{date-format this.responseTimestamp "MMMM"}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
/>
|
||||
{{#if this.hasAttributionData}}
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@newUsageCounts={{this.newUsageCounts}}
|
||||
@totalClientAttribution={{this.totalClientAttribution}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
|
||||
@isCurrentMonth={{true}}
|
||||
@isDateRange={{false}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,208 @@
|
|||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
<p class="has-bottom-margin-xl">
|
||||
This dashboard will surface Vault client usage over time. Clients represent a user or service that has authenticated to
|
||||
Vault. Documentation is available
|
||||
<DocLink @path="/vault/docs/concepts/client-count">here.</DocLink>
|
||||
Date queries are sent in UTC.
|
||||
</p>
|
||||
<h2 class="title is-6 has-bottom-margin-xs">
|
||||
{{this.versionText.label}}
|
||||
</h2>
|
||||
<div data-test-start-date-editor class="is-flex-align-baseline">
|
||||
{{#if this.formattedStartDate}}
|
||||
<p class="is-size-6" data-test-date-display>{{this.formattedStartDate}}</p>
|
||||
<button type="button" class="button is-link" {{on "click" (fn (mut this.showBillingStartModal) true)}}>
|
||||
Edit
|
||||
</button>
|
||||
{{else}}
|
||||
<DateDropdown @handleSubmit={{this.handleClientActivityQuery}} @dateType="startDate" @submitText="Save" />
|
||||
{{/if}}
|
||||
</div>
|
||||
<p class="is-8 has-text-grey has-bottom-margin-xl">
|
||||
{{this.versionText.description}}
|
||||
</p>
|
||||
{{#if this.noActivityData}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title="No data received {{if this.dateRangeMessage this.dateRangeMessage}}"
|
||||
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
|
||||
/>
|
||||
{{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 if this.errorObject}}
|
||||
<Clients::Error @error={{this.errorObject}} />
|
||||
{{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}}
|
||||
{{#if (or this.totalUsageCounts this.hasAttributionData)}}
|
||||
<div class="is-subtitle-gray has-bottom-margin-m">
|
||||
FILTERS
|
||||
<Toolbar data-test-clients-filter-bar>
|
||||
<ToolbarFilters>
|
||||
<CalendarWidget
|
||||
@startTimestamp={{this.startMonthTimestamp}}
|
||||
@endTimestamp={{this.endMonthTimestamp}}
|
||||
@selectMonth={{this.handleClientActivityQuery}}
|
||||
/>
|
||||
{{#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}}
|
||||
class="is-marginless"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (not (is-empty this.authMethodOptions))}}
|
||||
<SearchSelect
|
||||
@id="auth-method-search-select"
|
||||
@options={{this.authMethodOptions}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.setAuthMethod}}
|
||||
@placeholder={{"Filter by auth method"}}
|
||||
@displayInherit={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
</ToolbarFilters>
|
||||
</Toolbar>
|
||||
</div>
|
||||
|
||||
{{#if (or this.upgradeDuringActivity this.startTimeDiscrepancy)}}
|
||||
<AlertBanner @type="warning" @title="Warning">
|
||||
<ul class={{if (and this.versionUpdateText this.startTimeDiscrepancy) "bullet"}}>
|
||||
{{#if this.startTimeDiscrepancy}}
|
||||
<li>{{this.startTimeDiscrepancy}}</li>
|
||||
{{/if}}
|
||||
{{#if this.upgradeDuringActivity}}
|
||||
<li>
|
||||
{{this.upgradeVersionAndDate}}
|
||||
{{this.upgradeExplanation}}
|
||||
<DocLink
|
||||
@path="/vault/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts"
|
||||
>
|
||||
Learn more here.
|
||||
</DocLink>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
{{#if this.isLoadingQuery}}
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
{{#if this.totalUsageCounts}}
|
||||
{{#unless this.byMonthActivityData}}
|
||||
{{! UsageStats render when viewing a single, historical month AND activity data predates new client breakdown (< v1.10.0)
|
||||
or viewing the current month filtered down to auth method }}
|
||||
<Clients::UsageStats
|
||||
@title="Total usage"
|
||||
@description="These totals are within
|
||||
{{if this.selectedAuthMethod this.selectedAuthMethod 'this namespace and all its children'}}.
|
||||
{{if
|
||||
this.isCurrentMonth
|
||||
"Only totals are available when viewing the current month. To see a breakdown of new and total clients for this month, select the 'Current Billing Period' filter."
|
||||
}}"
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
/>
|
||||
{{/unless}}
|
||||
{{#if this.byMonthActivityData}}
|
||||
<Clients::RunningTotal
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@selectedAuthMethod={{this.selectedAuthMethod}}
|
||||
@byMonthActivityData={{this.byMonthActivityData}}
|
||||
@isHistoricalMonth={{and (not this.isCurrentMonth) (not this.isDateRange)}}
|
||||
@isCurrentMonth={{this.isCurrentMonth}}
|
||||
@runningTotals={{this.totalUsageCounts}}
|
||||
@upgradeData={{this.upgradeDuringActivity}}
|
||||
@responseTimestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.hasAttributionData}}
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@newUsageCounts={{this.newClientCounts}}
|
||||
@totalClientAttribution={{this.totalClientAttribution}}
|
||||
@newClientAttribution={{this.newClientAttribution}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@startTimestamp={{this.startMonthTimestamp}}
|
||||
@endTimestamp={{this.endMonthTimestamp}}
|
||||
@responseTimestamp={{this.responseTimestamp}}
|
||||
@isHistoricalMonth={{and (not this.isCurrentMonth) (not this.isDateRange)}}
|
||||
@upgradeExplanation={{this.upgradeExplanation}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.hasMultipleMonthsData}}
|
||||
<Clients::MonthlyUsage
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@verticalBarChartData={{this.byMonthActivityData}}
|
||||
@responseTimestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else if (and (not @model.licenseStartTimestamp) (not this.startMonthTimestamp))}}
|
||||
{{! Empty state for no billing/license start date }}
|
||||
<EmptyState @title={{this.versionText.title}} @message={{this.versionText.message}} />
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="No data received {{if this.dateRangeMessage this.dateRangeMessage}}"
|
||||
@message="Select a different start date above, or choose a different end date here:"
|
||||
>
|
||||
<DateDropdown
|
||||
@handleSubmit={{this.handleClientActivityQuery}}
|
||||
@validateDate={{this.isEndBeforeStart}}
|
||||
@dateType="endDate"
|
||||
@submitText="View"
|
||||
/>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{! BILLING START DATE MODAL }}
|
||||
|
||||
<Modal
|
||||
@title="Edit start month"
|
||||
@onClose={{action (mut this.showBillingStartModal) false}}
|
||||
@isActive={{this.showBillingStartModal}}
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-custom has-padding-m">
|
||||
<p class="has-bottom-margin-s">
|
||||
{{this.versionText.description}}
|
||||
</p>
|
||||
<p><strong>{{this.versionText.label}}</strong></p>
|
||||
</section>
|
||||
<DateDropdown
|
||||
@handleSubmit={{this.handleClientActivityQuery}}
|
||||
@dateType="startDate"
|
||||
@submitText="Save"
|
||||
@handleCancel={{fn this.handleClientActivityQuery (hash dateType="cancel")}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
|
@ -1,262 +0,0 @@
|
|||
<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>
|
||||
<h2 class="title is-6 has-bottom-margin-xs">
|
||||
{{this.versionText.label}}
|
||||
</h2>
|
||||
<div data-test-start-date-editor class="is-flex-align-baseline">
|
||||
{{#if this.startTimeDisplay}}
|
||||
<p class="is-size-6" data-test-date-display>{{this.startTimeDisplay}}</p>
|
||||
<button type="button" class="button is-link" {{on "click" (fn (mut this.isEditStartMonthOpen) true)}}>
|
||||
Edit
|
||||
</button>
|
||||
{{else}}
|
||||
<DateDropdown @handleDateSelection={{this.handleClientActivityQuery}} @name={{"startTime"}} @submitText="Save" />
|
||||
{{/if}}
|
||||
</div>
|
||||
<p class="is-8 has-text-grey has-bottom-margin-xl">
|
||||
{{this.versionText.description}}
|
||||
</p>
|
||||
{{#if this.licenseStartIsCurrentMonth}}
|
||||
<EmptyState
|
||||
@title="No data for this billing period"
|
||||
@subTitle="Your billing period has just begun, so there is no data yet. Data will be available here on the first of next month."
|
||||
@message="To view data from a previous billing period, you can enter your previous billing start date."
|
||||
@bottomBorder={{true}}
|
||||
>
|
||||
<DateDropdown @handleDateSelection={{this.handleClientActivityQuery}} @name={{"startTime"}} @submitText="View" />
|
||||
</EmptyState>
|
||||
{{else 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>
|
||||
</p>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else if this.errorObject}}
|
||||
<Clients::Error @error={{this.errorObject}} />
|
||||
{{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}}
|
||||
{{#if (or this.totalUsageCounts this.hasAttributionData)}}
|
||||
<div class="is-subtitle-gray has-bottom-margin-m">
|
||||
FILTERS
|
||||
<Toolbar data-test-clients-filter-bar>
|
||||
<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}}
|
||||
class="is-marginless"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (not (is-empty this.authMethodOptions))}}
|
||||
<SearchSelect
|
||||
@id="auth-method-search-select"
|
||||
@options={{this.authMethodOptions}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.setAuthMethod}}
|
||||
@placeholder={{"Filter by auth method"}}
|
||||
@displayInherit={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
</ToolbarFilters>
|
||||
</Toolbar>
|
||||
</div>
|
||||
|
||||
{{#if (or this.upgradeDuringActivity this.responseRangeDiffMessage)}}
|
||||
<AlertBanner @type="warning" @title="Warning">
|
||||
<ul class={{if (and this.versionUpdateText this.responseRangeDiffMessage) "bullet"}}>
|
||||
{{#if this.responseRangeDiffMessage}}
|
||||
<li>{{this.responseRangeDiffMessage}}</li>
|
||||
{{/if}}
|
||||
{{#if this.upgradeDuringActivity}}
|
||||
<li>
|
||||
{{this.upgradeVersionAndDate}}
|
||||
{{this.versionSpecificText}}
|
||||
<DocLink
|
||||
@path="/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts"
|
||||
>
|
||||
Learn more here.
|
||||
</DocLink>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
{{#if this.isLoadingQuery}}
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
{{#if this.totalUsageCounts}}
|
||||
{{#unless this.byMonthActivityData}}
|
||||
<Clients::UsageStats @title="Total usage" @totalUsageCounts={{this.totalUsageCounts}} />
|
||||
{{/unless}}
|
||||
{{#if this.byMonthActivityData}}
|
||||
<Clients::RunningTotal
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@selectedAuthMethod={{this.selectedAuthMethod}}
|
||||
@lineChartData={{this.byMonthActivityData}}
|
||||
@barChartData={{this.byMonthNewClients}}
|
||||
@runningTotals={{this.totalUsageCounts}}
|
||||
@upgradeData={{this.upgradeDuringActivity}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.hasAttributionData}}
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@newUsageCounts={{this.newClientCounts}}
|
||||
@totalClientAttribution={{this.totalClientAttribution}}
|
||||
@newClientAttribution={{this.newClientAttribution}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@startTimeDisplay={{this.startTimeDisplay}}
|
||||
@endTimeDisplay={{this.endTimeDisplay}}
|
||||
@isDateRange={{this.isDateRange}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.hasMultipleMonthsData}}
|
||||
<Clients::MonthlyUsage
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@verticalBarChartData={{this.byMonthActivityData}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else if (and (not @model.startTimeFromLicense) (not this.startTimeFromResponse))}}
|
||||
{{! Empty state for no billing/license start date }}
|
||||
<EmptyState @title={{this.versionText.title}} @message={{this.versionText.message}} />
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title={{concat "No data received " (if this.noActivityDate "from ") this.noActivityDate}}
|
||||
@message="No data exists for that query period. Try searching again."
|
||||
/>
|
||||
{{/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.versionText.description}}
|
||||
</p>
|
||||
<p class="has-bottom-margin-s"><strong>{{this.versionText.label}}</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="month"
|
||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
>
|
||||
{{or this.startMonth "Month"}}
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</D.Trigger>
|
||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||
<nav class="box menu scroll" aria-label="months">
|
||||
<ul class="menu-list">
|
||||
{{#each this.months as |month index|}}
|
||||
<button
|
||||
type="button"
|
||||
class="button link"
|
||||
data-test-date-modal-month={{month}}
|
||||
disabled={{if (lt index this.allowedMonthMax) false true}}
|
||||
{{on "click" (fn this.selectStartMonth month D.actions)}}
|
||||
>
|
||||
{{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="year"
|
||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
>
|
||||
{{or this.startYear "Year"}}
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</D.Trigger>
|
||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||
<nav class="box menu" aria-label="years">
|
||||
<ul class="menu-list">
|
||||
{{#each this.years as |year|}}
|
||||
<button
|
||||
type="button"
|
||||
class="button link"
|
||||
data-test-date-modal-year={{year}}
|
||||
disabled={{if (eq year this.disabledYear) true false}}
|
||||
{{on "click" (fn this.selectStartYear year D.actions)}}
|
||||
>
|
||||
{{year}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
</D.Content>
|
||||
</BasicDropdown>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
data-test-modal-save
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
disabled={{or (if (and this.startMonth this.startYear) false true)}}
|
||||
{{on "click" (fn this.handleClientActivityQuery this.startMonth this.startYear "startTime")}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="button is-secondary" {{on "click" (fn this.handleClientActivityQuery 0 0 "cancel")}}>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
</div>
|
|
@ -32,9 +32,9 @@
|
|||
</div>
|
||||
|
||||
<div data-test-monthly-usage-timestamp class="timestamp">
|
||||
{{#if @timestamp}}
|
||||
{{#if @responseTimestamp}}
|
||||
Updated
|
||||
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
|
||||
{{date-format @responseTimestamp "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,46 +1,4 @@
|
|||
{{#if this.showSingleMonth}}
|
||||
<div class="chart-wrapper single-month-grid" data-test-running-total="single-month-stats">
|
||||
<div class="chart-header has-bottom-margin-sm">
|
||||
<h2 class="chart-title">Vault client counts</h2>
|
||||
<p class="chart-description">
|
||||
A client is any user or service that interacts with Vault. They are made up of entity clients and non-entity clients.
|
||||
The total client count number is an important consideration for Vault billing.
|
||||
</p>
|
||||
</div>
|
||||
<div class="single-month-stats" data-test-new>
|
||||
<div class="single-month-section-title">
|
||||
<StatText
|
||||
@label="New clients"
|
||||
@subText="This is the number of clients which were created in Vault for the first time in the selected month."
|
||||
@value={{this.showSingleMonth.new.total}}
|
||||
@size="l"
|
||||
/>
|
||||
</div>
|
||||
<div class="single-month-breakdown-entity">
|
||||
<StatText @label="Entity clients" @value={{this.showSingleMonth.new.entityClients}} @size="m" />
|
||||
</div>
|
||||
<div class="single-month-breakdown-nonentity">
|
||||
<StatText @label="Non-entity clients" @value={{this.showSingleMonth.new.nonEntityClients}} @size="m" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="single-month-stats" data-test-total>
|
||||
<div class="single-month-section-title">
|
||||
<StatText
|
||||
@label="Total monthly clients"
|
||||
@subText="This is the number of total clients which used Vault for the given month, both new and previous."
|
||||
@value={{this.showSingleMonth.total.total}}
|
||||
@size="l"
|
||||
/>
|
||||
</div>
|
||||
<div class="single-month-breakdown-entity">
|
||||
<StatText @label="Entity clients" @value={{this.showSingleMonth.total.entityClients}} @size="m" />
|
||||
</div>
|
||||
<div class="single-month-breakdown-nonentity">
|
||||
<StatText @label="Non-entity clients" @value={{this.showSingleMonth.total.nonEntityClients}} @size="m" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if (gt @byMonthActivityData.length 1)}}
|
||||
<div class="chart-wrapper stacked-charts" data-test-running-total="monthly-charts">
|
||||
<div class="single-chart-grid">
|
||||
<div class="chart-header has-bottom-margin-xl">
|
||||
|
@ -51,8 +9,8 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class={{concat (unless @lineChartData "chart-empty-state ") "chart-container-wide"}}>
|
||||
<Clients::LineChart @dataset={{@lineChartData}} @upgradeData={{@upgradeData}} />
|
||||
<div class={{concat (unless @byMonthActivityData "chart-empty-state ") "chart-container-wide"}}>
|
||||
<Clients::LineChart @dataset={{@byMonthActivityData}} @upgradeData={{@upgradeData}} />
|
||||
</div>
|
||||
|
||||
<div class="chart-subTitle">
|
||||
|
@ -77,11 +35,11 @@
|
|||
<div class="single-chart-grid">
|
||||
<div class={{concat (unless this.hasAverageNewClients "chart-empty-state ") "chart-container-wide"}}>
|
||||
<Clients::VerticalBarChart
|
||||
@dataset={{if this.hasAverageNewClients @barChartData false}}
|
||||
@dataset={{if this.hasAverageNewClients this.byMonthNewClients false}}
|
||||
@chartLegend={{@chartLegend}}
|
||||
@noDataTitle="No new clients"
|
||||
@noDataMessage={{concat
|
||||
"There are no new clients for this "
|
||||
"There is no new client data available for this "
|
||||
(if @selectedAuthMethod "auth method" "namespace")
|
||||
" in this date range"
|
||||
}}
|
||||
|
@ -95,24 +53,26 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-top" data-test-running-new-entity>
|
||||
<h3 class="data-details">Average new entity clients per month</h3>
|
||||
<p class="data-details">
|
||||
{{format-number this.entityClientData.averageNewClients}}
|
||||
</p>
|
||||
</div>
|
||||
{{#if this.hasAverageNewClients}}
|
||||
<div class="data-details-top" data-test-running-new-entity>
|
||||
<h3 class="data-details">Average new entity clients per month</h3>
|
||||
<p class="data-details">
|
||||
{{format-number this.entityClientData.averageNewClients}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-bottom" data-test-running-new-nonentity>
|
||||
<h3 class="data-details">Average new non-entity clients per month</h3>
|
||||
<p class="data-details">
|
||||
{{format-number this.nonEntityClientData.averageNewClients}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="data-details-bottom" data-test-running-new-nonentity>
|
||||
<h3 class="data-details">Average new non-entity clients per month</h3>
|
||||
<p class="data-details">
|
||||
{{format-number this.nonEntityClientData.averageNewClients}}
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="timestamp" data-test-running-total-timestamp>
|
||||
{{#if @timestamp}}
|
||||
{{#if @responseTimestamp}}
|
||||
Updated
|
||||
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
|
||||
{{date-format @responseTimestamp "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
@ -124,4 +84,61 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if (and @isHistoricalMonth this.singleMonthData.new_clients.clients)}}
|
||||
<div class="chart-wrapper single-month-grid" data-test-running-total="single-month-stats">
|
||||
<div class="chart-header has-bottom-margin-sm">
|
||||
<h2 class="chart-title">Vault client counts</h2>
|
||||
<p class="chart-description">
|
||||
A client is any user or service that interacts with Vault. They are made up of entity clients and non-entity
|
||||
clients. The total client count number is an important consideration for Vault billing.
|
||||
</p>
|
||||
</div>
|
||||
<div class="single-month-stats" data-test-new>
|
||||
<div class="single-month-section-title">
|
||||
<StatText
|
||||
@label="New clients"
|
||||
@subText="This is the number of clients which were created in Vault for the first time in the selected month."
|
||||
@value={{this.singleMonthData.new_clients.clients}}
|
||||
@size="l"
|
||||
/>
|
||||
</div>
|
||||
<div class="single-month-breakdown-entity">
|
||||
<StatText @label="Entity clients" @value={{this.singleMonthData.new_clients.entity_clients}} @size="m" />
|
||||
</div>
|
||||
<div class="single-month-breakdown-nonentity">
|
||||
<StatText @label="Non-entity clients" @value={{this.singleMonthData.new_clients.non_entity_clients}} @size="m" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="single-month-stats" data-test-total>
|
||||
<div class="single-month-section-title">
|
||||
<StatText
|
||||
@label="Total monthly clients"
|
||||
@subText="This is the number of total clients which used Vault for the given month, both new and previous."
|
||||
@value={{this.singleMonthData.clients}}
|
||||
@size="l"
|
||||
/>
|
||||
</div>
|
||||
<div class="single-month-breakdown-entity">
|
||||
<StatText @label="Entity clients" @value={{this.singleMonthData.entity_clients}} @size="m" />
|
||||
</div>
|
||||
<div class="single-month-breakdown-nonentity">
|
||||
<StatText @label="Non-entity clients" @value={{this.singleMonthData.non_entity_clients}} @size="m" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{! This renders when either:
|
||||
-> viewing the current month and all namespaces (no filters)
|
||||
-> filtering by a namespace with no month over month data
|
||||
if filtering by a mount with no month over month data <UsageStats> in dashboard.hbs renders }}
|
||||
<Clients::UsageStats
|
||||
@title="Total usage"
|
||||
@description="These totals are within this namespace and all its children. {{if
|
||||
@isCurrentMonth
|
||||
"Only totals are available when viewing the current month. To see a breakdown of new and total clients for this month, select the 'Current Billing Period' filter."
|
||||
}}"
|
||||
@totalUsageCounts={{@runningTotals}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -2,7 +2,7 @@
|
|||
<div class="chart-header has-header-link has-bottom-margin-m">
|
||||
<div class="header-left">
|
||||
<h2 class="chart-title">{{@title}}</h2>
|
||||
<p class="chart-description"> These totals are within this namespace and all its children. </p>
|
||||
<p class="chart-description"> {{or @description "These totals are within this namespace and all its children."}}</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<DocLink @path="/vault/tutorials/monitoring/usage-metrics">
|
||||
|
@ -15,7 +15,7 @@
|
|||
<div class="column">
|
||||
<StatText
|
||||
@label="Total clients"
|
||||
@value={{or @totalUsageCounts.clients "0"}}
|
||||
@value={{@totalUsageCounts.clients}}
|
||||
@size="l"
|
||||
@subText="The sum of entity clientIDs and non-entity clientIDs; this is Vault’s primary billing metric."
|
||||
data-test-stat-text="total-clients"
|
||||
|
@ -25,7 +25,7 @@
|
|||
<StatText
|
||||
class="column"
|
||||
@label="Entity clients"
|
||||
@value={{or @totalUsageCounts.entity_clients "0"}}
|
||||
@value={{@totalUsageCounts.entity_clients}}
|
||||
@size="l"
|
||||
@subText="Representations of a particular user, client, or application that created a token via login."
|
||||
data-test-stat-text="entity-clients"
|
||||
|
@ -35,7 +35,7 @@
|
|||
<StatText
|
||||
class="column"
|
||||
@label="Non-entity clients"
|
||||
@value={{or @totalUsageCounts.non_entity_clients "0"}}
|
||||
@value={{@totalUsageCounts.non_entity_clients}}
|
||||
@size="l"
|
||||
@subText="Clients created with a shared set of permissions, but not associated with an entity. "
|
||||
data-test-stat-text="non-entity-clients"
|
||||
|
|
|
@ -136,7 +136,7 @@
|
|||
{{#if (and (has-permission "clients" routeParams="activity") (not @cluster.dr.isSecondary) this.auth.currentToken)}}
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
<LinkTo @route="vault.cluster.clients.current" {{on "click" @onLinkClick}}>
|
||||
<LinkTo @route="vault.cluster.clients.dashboard" {{on "click" @onLinkClick}}>
|
||||
<div class="level is-mobile">
|
||||
<span class="level-left">Client count</span>
|
||||
<Chevron class="has-text-grey-light level-right" />
|
||||
|
|
|
@ -1,61 +1,86 @@
|
|||
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
|
||||
<D.Trigger
|
||||
data-test-popup-menu-trigger="month"
|
||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
>
|
||||
{{or this.startMonth "Month"}}
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</D.Trigger>
|
||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||
<nav class="box menu scroll" aria-label="months">
|
||||
<ul data-test-month-list class="menu-list">
|
||||
{{#each this.months as |month index|}}
|
||||
<button
|
||||
type="button"
|
||||
class="button link"
|
||||
disabled={{if (lt index this.allowedMonthMax) false true}}
|
||||
{{on "click" (fn this.selectStartMonth month D.actions)}}
|
||||
>
|
||||
{{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="year"
|
||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
>
|
||||
{{or this.startYear "Year"}}
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</D.Trigger>
|
||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||
<nav class="box menu" aria-label="years">
|
||||
<ul data-test-year-list class="menu-list">
|
||||
{{#each this.years as |year|}}
|
||||
<button
|
||||
type="button"
|
||||
class="button link"
|
||||
disabled={{if (eq year this.disabledYear) true false}}
|
||||
{{on "click" (fn this.selectStartYear year D.actions)}}
|
||||
>
|
||||
{{year}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
</D.Content>
|
||||
</BasicDropdown>
|
||||
<button
|
||||
data-test-date-dropdown-submit
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
disabled={{if (and this.startMonth this.startYear) false true}}
|
||||
{{on "click" this.saveDateSelection}}
|
||||
>
|
||||
{{or @submitText "Submit"}}
|
||||
</button>
|
||||
<div class="modal-card-custom has-padding-btm-left">
|
||||
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
|
||||
<D.Trigger
|
||||
data-test-popup-menu-trigger="month"
|
||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
>
|
||||
{{or this.selectedMonth.name "Month"}}
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</D.Trigger>
|
||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||
<nav class="box menu scroll" aria-label="months">
|
||||
<ul data-test-month-list class="menu-list">
|
||||
{{#each this.dropdownMonths as |month|}}
|
||||
<button
|
||||
data-test-dropdown-month={{month.name}}
|
||||
type="button"
|
||||
class="button link"
|
||||
disabled={{if (gt month.index this.maxMonthIdx) true false}}
|
||||
{{on "click" (fn this.selectMonth month D.actions)}}
|
||||
>
|
||||
{{month.name}}
|
||||
</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="year"
|
||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
>
|
||||
{{or this.selectedYear "Year"}}
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</D.Trigger>
|
||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||
<nav class="box menu" aria-label="years">
|
||||
<ul data-test-year-list class="menu-list">
|
||||
{{#each this.dropdownYears as |year|}}
|
||||
<button
|
||||
data-test-dropdown-year={{year}}
|
||||
type="button"
|
||||
class="button link"
|
||||
disabled={{if (eq year this.disabledYear) true false}}
|
||||
{{on "click" (fn this.selectYear year D.actions)}}
|
||||
>
|
||||
{{year}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
</D.Content>
|
||||
</BasicDropdown>
|
||||
{{#unless @handleCancel}}
|
||||
<button
|
||||
data-test-date-dropdown-submit
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
disabled={{if (and this.selectedMonth this.selectedYear) false true}}
|
||||
{{on "click" this.handleSubmit}}
|
||||
>
|
||||
{{or @submitText "Submit"}}
|
||||
</button>
|
||||
{{/unless}}
|
||||
{{#if this.invalidDate}}
|
||||
<AlertInline @type="danger" @message={{this.invalidDate}} @paddingTop={{true}} @mimicRefresh={{true}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if @handleCancel}}
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
data-test-date-dropdown-submit
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
disabled={{if (and this.selectedMonth this.selectedYear) false true}}
|
||||
{{on "click" this.handleSubmit}}
|
||||
>
|
||||
{{or @submitText "Submit"}}
|
||||
</button>
|
||||
<button data-test-date-dropdown-cancel type="button" class="button is-secondary" {{on "click" this.handleCancel}}>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
{{/if}}
|
|
@ -78,11 +78,7 @@
|
|||
)}}
|
||||
<div class="navbar-sections">
|
||||
<div class={{if (is-active-route "vault.cluster.clients") "is-active"}}>
|
||||
<LinkTo
|
||||
@route="vault.cluster.clients.history"
|
||||
@current-when="vault.cluster.clients.history"
|
||||
data-test-navbar-item="metrics"
|
||||
>
|
||||
<LinkTo @route="vault.cluster.clients.dashboard" data-test-navbar-item="metrics">
|
||||
Client count
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
|
|
@ -6,14 +6,11 @@
|
|||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless" data-test-pricing-metrics>
|
||||
<div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless">
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
<LinkTo @route="vault.cluster.clients.current" data-test-current-month>
|
||||
Current month
|
||||
</LinkTo>
|
||||
<LinkTo @route="vault.cluster.clients.history" data-test-history>
|
||||
History
|
||||
<LinkTo @route="vault.cluster.clients.dashboard" data-test-dashboard>
|
||||
Dashboard
|
||||
</LinkTo>
|
||||
{{#if (or @model.config.configPath.canRead @model.configPath.canRead)}}
|
||||
<LinkTo @route="vault.cluster.clients.config">
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<Clients::Current @model={{@model}} />
|
|
@ -0,0 +1 @@
|
|||
<Clients::Dashboard @model={{@model}} />
|
|
@ -1 +0,0 @@
|
|||
<Clients::History @model={{@model}} />
|
|
@ -28,10 +28,13 @@ export function formatTooltipNumber(value) {
|
|||
}
|
||||
|
||||
export function calculateAverage(dataset, objectKey) {
|
||||
if (!Array.isArray(dataset) || dataset?.length === 0) return null;
|
||||
// if an array of objects, objectKey of the integer we want to calculate, ex: 'entity_clients'
|
||||
// if d[objectKey] is undefined there is no value, so return 0
|
||||
const getIntegers = objectKey ? dataset?.map((d) => (d[objectKey] ? d[objectKey] : 0)) : dataset;
|
||||
const checkIntegers = getIntegers.every((n) => Number.isInteger(n)); // decimals will be false
|
||||
return checkIntegers ? Math.round(mean(getIntegers)) : null;
|
||||
// before mapping for values, check that the objectKey exists at least once in the dataset because
|
||||
// map returns 0 when dataset[objectKey] is undefined in order to calculate average
|
||||
if (!Array.isArray(dataset) || !objectKey || !dataset.some((d) => Object.keys(d).includes(objectKey))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const integers = dataset.map((d) => (d[objectKey] ? d[objectKey] : 0));
|
||||
const checkIntegers = integers.every((n) => Number.isInteger(n)); // decimals will be false
|
||||
return checkIntegers ? Math.round(mean(integers)) : null;
|
||||
}
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
{{#if @subText}}
|
||||
<div class="stat-text">{{@subText}}</div>
|
||||
{{/if}}
|
||||
<div class="stat-value">{{format-number @value}}</div>
|
||||
{{! Ember reads 0 as falsy, so only render a dash if the value is truly undefined}}
|
||||
<div class="stat-value">{{if (or @value (eq @value 0)) (format-number @value) "-"}}</div>
|
||||
</div>
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
* @module StatText
|
||||
* StatText components are used to display a label and associated value beneath, with the option to include a description.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <StatText @label="Active Clients" @value="4,198" @size="l" @subText="These are the active client counts"/>
|
||||
* ```
|
||||
* @param {string} label=null - The label for the statistic
|
||||
* @param {number} value=null - Value passed in, usually a number or statistic
|
||||
* @param {string} size=null - Sizing changes whether or not there is subtext. If there is subtext 's' and 'l' are valid sizes. If no subtext, then 'm' is also acceptable.
|
||||
* @param {string} [subText] - SubText is optional and will display below the label
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import layout from '../templates/components/stat-text';
|
||||
import { setComponentTemplate } from '@ember/component';
|
||||
/* eslint ember/no-empty-glimmer-component-classes: 'warn' */
|
||||
class StatTextComponent extends Component {}
|
||||
|
||||
export default setComponentTemplate(layout, StatTextComponent);
|
|
@ -1,16 +1,11 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
|
||||
export function dateFormat([date, style], { isFormatted = false, dateOnly = false }) {
|
||||
export function dateFormat([date, style], { isFormatted = false, withTimeZone = false }) {
|
||||
// see format breaking in upgrade to date-fns 2.x https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#changed-5
|
||||
if (isFormatted) {
|
||||
return format(new Date(date), style);
|
||||
}
|
||||
// when date is in '2021-09-01T00:00:00Z' format
|
||||
// remove hours so date displays unaffected by timezone
|
||||
if (dateOnly && typeof date === 'string') {
|
||||
date = date.split('T')[0];
|
||||
}
|
||||
let number = typeof date === 'string' ? parseISO(date) : date;
|
||||
if (!number) {
|
||||
return;
|
||||
|
@ -18,7 +13,15 @@ export function dateFormat([date, style], { isFormatted = false, dateOnly = fals
|
|||
if (number.toString().length === 10) {
|
||||
number = new Date(number * 1000);
|
||||
}
|
||||
return format(number, style);
|
||||
let zone; // local timezone ex: 'PST'
|
||||
try {
|
||||
// passing undefined means default to the browser's locale
|
||||
zone = ' ' + number.toLocaleTimeString(undefined, { timeZoneName: 'short' }).split(' ')[2];
|
||||
} catch (e) {
|
||||
zone = '';
|
||||
}
|
||||
zone = withTimeZone ? zone : '';
|
||||
return format(number, style) + zone;
|
||||
}
|
||||
|
||||
export default helper(dateFormat);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { parseAPITimestamp } from 'core/utils/date-formatters';
|
|||
import { compareAsc } from 'date-fns';
|
||||
|
||||
export const formatByMonths = (monthsArray) => {
|
||||
// the months array will always include a timestamp of the month and either new/total client data or counts = null
|
||||
// the monthsArray will always include a timestamp of the month and either new/total client data or counts = null
|
||||
if (!Array.isArray(monthsArray)) return monthsArray;
|
||||
|
||||
const sortedPayload = sortMonthsByTimestamp(monthsArray);
|
||||
|
@ -15,11 +15,13 @@ export const formatByMonths = (monthsArray) => {
|
|||
const newCounts = m.new_clients ? flattenDataset(m.new_clients) : {};
|
||||
return {
|
||||
month,
|
||||
timestamp: m.timestamp,
|
||||
...totalCounts,
|
||||
namespaces: formatByNamespace(m.namespaces) || [],
|
||||
namespaces_by_key: namespaceArrayToObject(totalClientsByNamespace, newClientsByNamespace, month),
|
||||
new_clients: {
|
||||
month,
|
||||
timestamp: m.timestamp,
|
||||
...newCounts,
|
||||
namespaces: formatByNamespace(m.new_clients?.namespaces) || [],
|
||||
},
|
||||
|
@ -116,6 +118,7 @@ export const namespaceArrayToObject = (totalClientsByNamespace, newClientsByName
|
|||
clients,
|
||||
entity_clients,
|
||||
non_entity_clients,
|
||||
mounts: newClientsByMount,
|
||||
},
|
||||
mounts: [...nestNewClientsWithinMounts],
|
||||
};
|
||||
|
@ -149,98 +152,103 @@ export const namespaceArrayToObject = (totalClientsByNamespace, newClientsByName
|
|||
};
|
||||
});
|
||||
return namespaces_by_key;
|
||||
// structure of object returned
|
||||
// namespace_by_key: {
|
||||
// "namespace_label": {
|
||||
// month: "3/22",
|
||||
// clients: 32,
|
||||
// entity_clients: 16,
|
||||
// non_entity_clients: 16,
|
||||
// new_clients: {
|
||||
// month: "3/22",
|
||||
// clients: 5,
|
||||
// entity_clients: 2,
|
||||
// non_entity_clients: 3,
|
||||
// },
|
||||
// mounts_by_key: {
|
||||
// "mount_label": {
|
||||
// month: "3/22",
|
||||
// clients: 3,
|
||||
// entity_clients: 2,
|
||||
// non_entity_clients: 1,
|
||||
// new_clients: {
|
||||
// month: "3/22",
|
||||
// clients: 5,
|
||||
// entity_clients: 2,
|
||||
// non_entity_clients: 3,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
/*
|
||||
structure of object returned
|
||||
namespace_by_key: {
|
||||
"namespace_label": {
|
||||
month: "3/22",
|
||||
clients: 32,
|
||||
entity_clients: 16,
|
||||
non_entity_clients: 16,
|
||||
new_clients: {
|
||||
month: "3/22",
|
||||
clients: 5,
|
||||
entity_clients: 2,
|
||||
non_entity_clients: 3,
|
||||
mounts: [...array of this namespace's mounts and their new client counts],
|
||||
},
|
||||
mounts_by_key: {
|
||||
"mount_label": {
|
||||
month: "3/22",
|
||||
clients: 3,
|
||||
entity_clients: 2,
|
||||
non_entity_clients: 1,
|
||||
new_clients: {
|
||||
month: "3/22",
|
||||
clients: 5,
|
||||
entity_clients: 2,
|
||||
non_entity_clients: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
*/
|
||||
};
|
||||
|
||||
// API RESPONSE STRUCTURE:
|
||||
// data: {
|
||||
// ** by_namespace organized in descending order of client count number **
|
||||
// by_namespace: [
|
||||
// {
|
||||
// namespace_id: '96OwG',
|
||||
// namespace_path: 'test-ns/',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'path-1', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// ** months organized in ascending order of timestamps, oldest to most recent
|
||||
// months: [
|
||||
// {
|
||||
// timestamp: '2022-03-01T00:00:00Z',
|
||||
// counts: {},
|
||||
// namespaces: [
|
||||
// {
|
||||
// namespace_id: 'root',
|
||||
// namespace_path: '',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// new_clients: {
|
||||
// counts: {},
|
||||
// namespaces: [
|
||||
// {
|
||||
// namespace_id: 'root',
|
||||
// namespace_path: '',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// timestamp: '2022-04-01T00:00:00Z',
|
||||
// counts: {},
|
||||
// namespaces: [
|
||||
// {
|
||||
// namespace_id: 'root',
|
||||
// namespace_path: '',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// new_clients: {
|
||||
// counts: {},
|
||||
// namespaces: [
|
||||
// {
|
||||
// namespace_id: 'root',
|
||||
// namespace_path: '',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// start_time: 'start timestamp string',
|
||||
// end_time: 'end timestamp string',
|
||||
// total: { clients: 300, non_entity_clients: 100, entity_clients: 400} ,
|
||||
// }
|
||||
/*
|
||||
API RESPONSE STRUCTURE:
|
||||
data: {
|
||||
** by_namespace organized in descending order of client count number **
|
||||
by_namespace: [
|
||||
{
|
||||
namespace_id: '96OwG',
|
||||
namespace_path: 'test-ns/',
|
||||
counts: {},
|
||||
mounts: [{ mount_path: 'path-1', counts: {} }],
|
||||
},
|
||||
],
|
||||
** months organized in ascending order of timestamps, oldest to most recent
|
||||
months: [
|
||||
{
|
||||
timestamp: '2022-03-01T00:00:00Z',
|
||||
counts: {},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {},
|
||||
mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
counts: {},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {},
|
||||
mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamp: '2022-04-01T00:00:00Z',
|
||||
counts: {},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {},
|
||||
mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
counts: {},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {},
|
||||
mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
start_time: 'start timestamp string',
|
||||
end_time: 'end timestamp string',
|
||||
total: { clients: 300, non_entity_clients: 100, entity_clients: 400} ,
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -23,17 +23,6 @@ export const parseAPITimestamp = (timestamp, style) => {
|
|||
return format(date, style);
|
||||
};
|
||||
|
||||
// convert ISO timestamp '2021-03-21T00:00:00Z' to ['2021', 2]
|
||||
// (e.g. 2021 March, month is zero indexed) (used by calendar widget)
|
||||
export const parseRFC3339 = (timestamp) => {
|
||||
if (Array.isArray(timestamp)) {
|
||||
// return if already formatted correctly
|
||||
return timestamp;
|
||||
}
|
||||
const date = parseAPITimestamp(timestamp);
|
||||
return date ? [`${date.getFullYear()}`, date.getMonth()] : null;
|
||||
};
|
||||
|
||||
// convert M/yy (format of dates in charts) to 'Month yyyy' (format in tooltip)
|
||||
export function formatChartDate(date) {
|
||||
const array = date.split('/');
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,6 @@
|
|||
// add all handlers here
|
||||
// individual lookup done in mirage config
|
||||
import base from './base';
|
||||
import activity from './activity';
|
||||
import clients from './clients';
|
||||
import db from './db';
|
||||
import kms from './kms';
|
||||
|
@ -11,4 +10,4 @@ import oidcConfig from './oidc-config';
|
|||
import hcpLink from './hcp-link';
|
||||
import kubernetes from './kubernetes';
|
||||
|
||||
export { base, activity, clients, db, kms, mfaConfig, mfaLogin, oidcConfig, hcpLink, kubernetes };
|
||||
export { base, clients, db, kms, mfaConfig, mfaLogin, oidcConfig, hcpLink, kubernetes };
|
||||
|
|
|
@ -147,7 +147,7 @@
|
|||
"ember-inflector": "4.0.2",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-maybe-in-element": "^2.0.3",
|
||||
"ember-modal-dialog": "4.0.0",
|
||||
"ember-modal-dialog": "^4.0.1",
|
||||
"ember-modifier": "^3.1.0",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-power-select": "6.0.1",
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, settled, click } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import ENV from 'vault/config/environment';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import ss from 'vault/tests/pages/components/search-select';
|
||||
import { overrideResponse, SELECTORS } from '../helpers/clients';
|
||||
|
||||
const searchSelect = create(ss);
|
||||
|
||||
module('Acceptance | clients current tab', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.before(function () {
|
||||
ENV['ember-cli-mirage'].handler = 'clients';
|
||||
});
|
||||
|
||||
hooks.after(function () {
|
||||
ENV['ember-cli-mirage'].handler = null;
|
||||
});
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
return authPage.login();
|
||||
});
|
||||
|
||||
test('shows empty state when config disabled, no data', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.get('sys/internal/counters/config', () => {
|
||||
return {
|
||||
request_id: 'some-config-id',
|
||||
data: {
|
||||
default_report_months: 12,
|
||||
enabled: 'default-disable',
|
||||
retention_months: 24,
|
||||
},
|
||||
};
|
||||
});
|
||||
this.server.get('sys/internal/counters/activity/monthly', () => overrideResponse(204));
|
||||
await visit('/vault/clients/current');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/current');
|
||||
assert.dom(SELECTORS.currentMonthActiveTab).hasText('Current month', 'current month tab is active');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('Tracking is disabled');
|
||||
});
|
||||
|
||||
test('shows empty state when config enabled, no data', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.get('sys/internal/counters/activity/monthly', () => {
|
||||
return {
|
||||
request_id: 'some-monthly-id',
|
||||
data: {
|
||||
by_namespace: [],
|
||||
clients: 0,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
months: [],
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
await visit('/vault/clients/current');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/current');
|
||||
assert.dom(SELECTORS.currentMonthActiveTab).hasText('Current month', 'current month tab is active');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('No data received');
|
||||
});
|
||||
|
||||
test('filters correctly on current with full data', async function (assert) {
|
||||
assert.expect(27);
|
||||
await visit('/vault/clients/current');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/current');
|
||||
assert.dom(SELECTORS.currentMonthActiveTab).hasText('Current month', 'current month tab is active');
|
||||
assert.dom(SELECTORS.usageStats).exists('usage stats block exists');
|
||||
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
|
||||
|
||||
// TODO update with dynamic counts
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('175');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('132');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('43');
|
||||
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
|
||||
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
|
||||
await settled();
|
||||
|
||||
// FILTER BY NAMESPACE
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('100');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('85');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('15');
|
||||
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
|
||||
await settled();
|
||||
|
||||
// FILTER BY AUTH METHOD
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('35');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('20');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('15');
|
||||
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution block');
|
||||
|
||||
// Delete auth filter goes back to filtered only on namespace
|
||||
await click('#auth-method-search-select [data-test-selected-list-button="delete"]');
|
||||
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('100');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('85');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('15');
|
||||
await settled();
|
||||
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
|
||||
assert.dom(SELECTORS.attributionBlock).exists('Still shows attribution block');
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
// Delete namespace filter with auth filter on
|
||||
await click('#namespace-search-select-monthly [data-test-selected-list-button="delete"]');
|
||||
// Goes back to no filters
|
||||
// TODO update with dynamic counts
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('175');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('132');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('43');
|
||||
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
|
||||
assert.dom('[data-test-chart-container="new-clients"] [data-test-empty-state-subtext]').doesNotExist();
|
||||
});
|
||||
|
||||
test('filters correctly on current with no auth mounts', async function (assert) {
|
||||
assert.expect(16);
|
||||
await visit('/vault/clients/current');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/current');
|
||||
assert.dom(SELECTORS.currentMonthActiveTab).hasText('Current month', 'current month tab is active');
|
||||
assert.dom(SELECTORS.usageStats).exists('usage stats block exists');
|
||||
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
|
||||
// TODO CMB update with dynamic data
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('175');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('132');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('43');
|
||||
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
|
||||
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
|
||||
|
||||
// Filter by namespace
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('100');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('85');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('15');
|
||||
|
||||
// TODO add month data without mounts
|
||||
// assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution');
|
||||
// assert.dom('#auth-method-search-select').doesNotExist('Auth method filter is not shown');
|
||||
|
||||
// Remove namespace filter
|
||||
await click('#namespace-search-select-monthly [data-test-selected-list-button="delete"]');
|
||||
|
||||
// Goes back to no filters
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('175');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('132');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('43');
|
||||
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
|
||||
});
|
||||
|
||||
test('shows correct empty state when config off but no read on config', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.get('sys/internal/counters/activity/monthly', () => overrideResponse(204));
|
||||
this.server.get('sys/internal/counters/config', () => overrideResponse(403));
|
||||
await visit('/vault/clients/current');
|
||||
assert.dom(SELECTORS.filterBar).doesNotExist('Filter bar is not shown');
|
||||
assert.dom(SELECTORS.emptyStateTitle).containsText('No data available', 'Shows no data empty state');
|
||||
});
|
||||
});
|
|
@ -10,20 +10,20 @@ import { create } from 'ember-cli-page-object';
|
|||
import ss from 'vault/tests/pages/components/search-select';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
|
||||
import { formatNumber } from 'core/helpers/format-number';
|
||||
|
||||
const searchSelect = create(ss);
|
||||
|
||||
const NEW_DATE = new Date();
|
||||
const LAST_MONTH = startOfMonth(subMonths(NEW_DATE, 1));
|
||||
const COUNTS_START = subMonths(NEW_DATE, 12); // pretend vault user started cluster 1 year ago
|
||||
const CURRENT_DATE = new Date();
|
||||
const LAST_MONTH = startOfMonth(subMonths(CURRENT_DATE, 1));
|
||||
const COUNTS_START = subMonths(CURRENT_DATE, 12); // pretend vault user started cluster 1 year ago
|
||||
|
||||
// for testing, we're in the middle of a license/billing period
|
||||
const LICENSE_START = startOfMonth(subMonths(NEW_DATE, 6));
|
||||
|
||||
const LICENSE_START = startOfMonth(subMonths(CURRENT_DATE, 6));
|
||||
// upgrade happened 1 month after license start
|
||||
const UPGRADE_DATE = addMonths(LICENSE_START, 1);
|
||||
|
||||
module('Acceptance | clients history tab', function (hooks) {
|
||||
module('Acceptance | client counts dashboard tab', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
|
@ -31,6 +31,10 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
ENV['ember-cli-mirage'].handler = 'clients';
|
||||
});
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
});
|
||||
|
||||
hooks.after(function () {
|
||||
ENV['ember-cli-mirage'].handler = null;
|
||||
});
|
||||
|
@ -39,30 +43,7 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
return authPage.login();
|
||||
});
|
||||
|
||||
test('shows warning when config off, no data, queries_available=true', async function (assert) {
|
||||
assert.expect(6);
|
||||
this.server.get('sys/internal/counters/activity', () => overrideResponse(204));
|
||||
this.server.get('sys/internal/counters/config', () => {
|
||||
return {
|
||||
request_id: 'some-config-id',
|
||||
data: {
|
||||
default_report_months: 12,
|
||||
enabled: 'default-disable',
|
||||
queries_available: true,
|
||||
retention_months: 24,
|
||||
},
|
||||
};
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history');
|
||||
assert.dom(SELECTORS.historyActiveTab).hasText('History', 'history tab is active');
|
||||
assert.dom('[data-test-tracking-disabled] .message-title').hasText('Tracking is disabled');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('No data received');
|
||||
assert.dom(SELECTORS.filterBar).doesNotExist('Shows filter bar to search previous dates');
|
||||
assert.dom(SELECTORS.usageStats).doesNotExist('No usage stats');
|
||||
});
|
||||
|
||||
test('shows warning when config off, no data, queries_available=false', async function (assert) {
|
||||
test('shows warning when config off, no data', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.server.get('sys/internal/counters/activity', () => overrideResponse(204));
|
||||
this.server.get('sys/internal/counters/config', () => {
|
||||
|
@ -76,14 +57,14 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
},
|
||||
};
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history');
|
||||
assert.dom(SELECTORS.historyActiveTab).hasText('History', 'history tab is active');
|
||||
await visit('/vault/clients/dashboard');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/dashboard');
|
||||
assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('Data tracking is disabled');
|
||||
assert.dom(SELECTORS.filterBar).doesNotExist('Filter bar is hidden when no data available');
|
||||
});
|
||||
|
||||
test('shows empty state when config enabled and queries_available=false', async function (assert) {
|
||||
test('shows empty state when config enabled and no data', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.server.get('sys/internal/counters/activity', () => overrideResponse(204));
|
||||
this.server.get('sys/internal/counters/config', () => {
|
||||
|
@ -92,31 +73,28 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
data: {
|
||||
default_report_months: 12,
|
||||
enabled: 'default-enable',
|
||||
queries_available: false,
|
||||
retention_months: 24,
|
||||
},
|
||||
};
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history');
|
||||
assert.dom(SELECTORS.historyActiveTab).hasText('History', 'history tab is active');
|
||||
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('No monthly history');
|
||||
await visit('/vault/clients/dashboard');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/dashboard');
|
||||
assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasTextContaining('No data received');
|
||||
assert.dom(SELECTORS.filterBar).doesNotExist('Does not show filter bar');
|
||||
});
|
||||
|
||||
test('visiting history tab config on and data with mounts', async function (assert) {
|
||||
test('visiting dashboard tab config on and data with mounts', async function (assert) {
|
||||
assert.expect(8);
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history');
|
||||
|
||||
await visit('/vault/clients/dashboard');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/dashboard');
|
||||
assert
|
||||
.dom(SELECTORS.dateDisplay)
|
||||
.hasText(format(LICENSE_START, 'MMMM yyyy'), 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(SELECTORS.rangeDropdown)
|
||||
.hasText(
|
||||
`${format(LICENSE_START, 'MMMM yyyy')} - ${format(LAST_MONTH, 'MMMM yyyy')}`,
|
||||
`${format(LICENSE_START, 'MMM yyyy')} - ${format(CURRENT_DATE, 'MMM yyyy')}`,
|
||||
'Date range shows dates correctly parsed activity response'
|
||||
);
|
||||
assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area');
|
||||
|
@ -129,18 +107,15 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
.hasText(`${format(LICENSE_START, 'M/yy')}`, 'x-axis labels start with billing start date');
|
||||
assert.strictEqual(
|
||||
findAll('[data-test-line-chart="plot-point"]').length,
|
||||
5,
|
||||
`line chart plots 5 points to match query`
|
||||
6,
|
||||
`line chart plots 6 points to match query`
|
||||
);
|
||||
});
|
||||
|
||||
test('updates correctly when querying date ranges', async function (assert) {
|
||||
assert.expect(26);
|
||||
// TODO CMB: wire up dynamically generated activity to mirage clients handler
|
||||
// const activity = generateActivityResponse(5, LICENSE_START, LAST_MONTH);
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history');
|
||||
|
||||
await visit('/vault/clients/dashboard');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/dashboard');
|
||||
// query for single, historical month with no new counts
|
||||
await click(SELECTORS.rangeDropdown);
|
||||
await click('[data-test-show-calendar]');
|
||||
|
@ -148,7 +123,6 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
await click('[data-test-previous-year]');
|
||||
}
|
||||
await click(find(`[data-test-calendar-month=${ARRAY_OF_MONTHS[LICENSE_START.getMonth()]}]`));
|
||||
|
||||
assert.dom('[data-test-usage-stats]').exists('total usage stats show');
|
||||
assert
|
||||
.dom(SELECTORS.runningTotalMonthStats)
|
||||
|
@ -173,11 +147,10 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
// change billing start to month/year of first upgrade
|
||||
await click('[data-test-start-date-editor] button');
|
||||
await click(SELECTORS.monthDropdown);
|
||||
await click(find(`[data-test-date-modal-month="${ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()]}"]`));
|
||||
await click(`[data-test-dropdown-month="${ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()]}"]`);
|
||||
await click(SELECTORS.yearDropdown);
|
||||
await click(find(`[data-test-date-modal-year="${UPGRADE_DATE.getFullYear()}`));
|
||||
await click('[data-test-modal-save]');
|
||||
|
||||
await click(`[data-test-dropdown-year="${UPGRADE_DATE.getFullYear()}"]`);
|
||||
await click('[data-test-date-dropdown-submit]');
|
||||
assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area');
|
||||
assert.dom(SELECTORS.monthlyUsageBlock).exists('Shows monthly usage block');
|
||||
assert
|
||||
|
@ -188,12 +161,12 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
.hasText(`${format(UPGRADE_DATE, 'M/yy')}`, 'x-axis labels start with updated billing start month');
|
||||
assert.strictEqual(
|
||||
findAll('[data-test-line-chart="plot-point"]').length,
|
||||
5,
|
||||
`line chart plots 5 points to match query`
|
||||
6,
|
||||
`line chart plots 6 points to match query`
|
||||
);
|
||||
|
||||
// query custom end month
|
||||
const customEndDate = subMonths(NEW_DATE, 3);
|
||||
// query three months ago
|
||||
const customEndDate = subMonths(CURRENT_DATE, 3);
|
||||
await click(SELECTORS.rangeDropdown);
|
||||
await click('[data-test-show-calendar]');
|
||||
if (parseInt(find('[data-test-display-year]').innerText) !== customEndDate.getFullYear()) {
|
||||
|
@ -216,7 +189,7 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
.dom(xAxisLabels[xAxisLabels.length - 1])
|
||||
.hasText(`${format(subMonths(LAST_MONTH, 2), 'M/yy')}`, 'x-axis labels end with queried end month');
|
||||
|
||||
// query for single, historical month
|
||||
// query for single, historical month (upgrade month)
|
||||
await click(SELECTORS.rangeDropdown);
|
||||
await click('[data-test-show-calendar]');
|
||||
if (parseInt(find('[data-test-display-year]').innerText) !== UPGRADE_DATE.getFullYear()) {
|
||||
|
@ -236,13 +209,13 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
// reset to billing period
|
||||
await click('[data-test-popup-menu-trigger]');
|
||||
await click('[data-test-current-billing-period]');
|
||||
|
||||
// query month older than count start date
|
||||
await click('[data-test-start-date-editor] button');
|
||||
await click(SELECTORS.monthDropdown);
|
||||
await click(`[data-test-dropdown-month="${ARRAY_OF_MONTHS[LICENSE_START.getMonth()]}"]`);
|
||||
await click(SELECTORS.yearDropdown);
|
||||
await click(find(`[data-test-date-modal-year="${LICENSE_START.getFullYear() - 3}`));
|
||||
await click('[data-test-modal-save]');
|
||||
|
||||
await click(`[data-test-dropdown-year="${LICENSE_START.getFullYear() - 3}"]`);
|
||||
await click('[data-test-date-dropdown-submit]');
|
||||
assert
|
||||
.dom('[data-test-alert-banner="alert"]')
|
||||
.hasTextContaining(
|
||||
|
@ -251,53 +224,83 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test('filters correctly on history with full data', async function (assert) {
|
||||
assert.expect(19);
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
|
||||
assert.dom(SELECTORS.historyActiveTab).hasText('History', 'history tab is active');
|
||||
test('dashboard filters correctly with full data', async function (assert) {
|
||||
assert.expect(21);
|
||||
await visit('/vault/clients/dashboard');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct');
|
||||
assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active');
|
||||
assert
|
||||
.dom(SELECTORS.runningTotalMonthlyCharts)
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area');
|
||||
assert.dom(SELECTORS.monthlyUsageBlock).exists('Shows monthly usage block');
|
||||
const response = await this.store.peekRecord('clients/activity', 'some-activity-id');
|
||||
|
||||
// FILTER BY NAMESPACE
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
|
||||
await settled();
|
||||
const topNamespace = response.byNamespace[0];
|
||||
const topMount = topNamespace.mounts[0];
|
||||
assert.ok(true, 'Filter by first namespace');
|
||||
assert.strictEqual(
|
||||
find(SELECTORS.selectedNs).innerText.toLowerCase(),
|
||||
topNamespace.label,
|
||||
'selects top namespace'
|
||||
);
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top auth method');
|
||||
assert.dom('[data-test-running-total-entity]').includesText('23,326', 'total entity clients is accurate');
|
||||
assert
|
||||
.dom('[data-test-running-total-nonentity]')
|
||||
.includesText('17,826', 'total non-entity clients is accurate');
|
||||
assert.dom('[data-test-attribution-clients]').includesText('10,142', 'top attribution clients accurate');
|
||||
.dom('[data-test-running-total-entity] p')
|
||||
.includesText(`${formatNumber([topNamespace.entity_clients])}`, 'total entity clients is accurate');
|
||||
assert
|
||||
.dom('[data-test-running-total-nonentity] p')
|
||||
.includesText(
|
||||
`${formatNumber([topNamespace.non_entity_clients])}`,
|
||||
'total non-entity clients is accurate'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-attribution-clients] p')
|
||||
.includesText(`${formatNumber([topMount.clients])}`, 'top attribution clients accurate');
|
||||
|
||||
// FILTER BY AUTH METHOD
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
assert.ok(true, 'Filter by first auth method');
|
||||
assert.dom('[data-test-running-total-entity]').includesText('6,508', 'total entity clients is accurate');
|
||||
assert.strictEqual(
|
||||
find(SELECTORS.selectedAuthMount).innerText.toLowerCase(),
|
||||
topMount.label,
|
||||
'selects top mount'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-running-total-nonentity]')
|
||||
.includesText('3,634', 'total non-entity clients is accurate');
|
||||
.dom('[data-test-running-total-entity] p')
|
||||
.includesText(`${formatNumber([topMount.entity_clients])}`, 'total entity clients is accurate');
|
||||
assert
|
||||
.dom('[data-test-running-total-nonentity] p')
|
||||
.includesText(`${formatNumber([topMount.non_entity_clients])}`, 'total non-entity clients is accurate');
|
||||
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution block');
|
||||
|
||||
await click('#namespace-search-select [data-test-selected-list-button="delete"]');
|
||||
assert.ok(true, 'Remove namespace filter without first removing auth method filter');
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
|
||||
assert
|
||||
.dom('[data-test-attribution-clients]')
|
||||
.hasTextContaining('41,152', 'top attribution clients back to unfiltered value');
|
||||
assert
|
||||
.dom('[data-test-running-total-entity]')
|
||||
.hasTextContaining('98,289', 'total entity clients is back to unfiltered value');
|
||||
.hasTextContaining(
|
||||
`${formatNumber([response.total.entity_clients])}`,
|
||||
'total entity clients is back to unfiltered value'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-running-total-nonentity]')
|
||||
.hasTextContaining('96,412', 'total non-entity clients is back to unfiltered value');
|
||||
.hasTextContaining(
|
||||
`${formatNumber([formatNumber([response.total.non_entity_clients])])}`,
|
||||
'total non-entity clients is back to unfiltered value'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-attribution-clients]')
|
||||
.hasTextContaining(
|
||||
`${formatNumber([topNamespace.clients])}`,
|
||||
'top attribution clients back to unfiltered value'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows warning if upgrade happened within license period', async function (assert) {
|
||||
|
@ -327,22 +330,23 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
},
|
||||
};
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
|
||||
assert.dom(SELECTORS.historyActiveTab).hasText('History', 'history tab is active');
|
||||
await visit('/vault/clients/dashboard');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct');
|
||||
assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active');
|
||||
assert
|
||||
.dom('[data-test-alert-banner="alert"]')
|
||||
.hasTextContaining(
|
||||
`Warning Vault was upgraded to 1.10.1 on ${format(
|
||||
UPGRADE_DATE,
|
||||
'MMM d, yyyy'
|
||||
)}. We added monthly breakdowns and mount level attribution starting in 1.10, so keep that in mind when looking at the data below. Learn more here.`
|
||||
)}. We added monthly breakdowns and mount level attribution starting in 1.10, so keep that in mind when looking at the data. Learn more here.`
|
||||
);
|
||||
});
|
||||
|
||||
test('Shows empty if license start date is current month', async function (assert) {
|
||||
const licenseStart = NEW_DATE;
|
||||
const licenseEnd = addMonths(NEW_DATE, 12);
|
||||
// TODO cmb update to reflect new behavior
|
||||
const licenseStart = CURRENT_DATE;
|
||||
const licenseEnd = addMonths(CURRENT_DATE, 12);
|
||||
this.server.get('sys/license/status', function () {
|
||||
return {
|
||||
request_id: 'my-license-request-id',
|
||||
|
@ -355,21 +359,16 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
},
|
||||
};
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('No data for this billing period');
|
||||
assert
|
||||
.dom(SELECTORS.dateDisplay)
|
||||
.hasText(format(licenseStart, 'MMMM yyyy'), 'Shows license date, gives ability to edit');
|
||||
assert.dom(SELECTORS.monthDropdown).exists('Dropdown exists to select month');
|
||||
assert.dom(SELECTORS.yearDropdown).exists('Dropdown exists to select year');
|
||||
await visit('/vault/clients/dashboard');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct');
|
||||
assert.dom(SELECTORS.emptyStateTitle).doesNotExist('No data for this billing period');
|
||||
});
|
||||
|
||||
test('shows correct interface if no permissions on license', async function (assert) {
|
||||
this.server.get('/sys/license/status', () => overrideResponse(403));
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
|
||||
assert.dom(SELECTORS.historyActiveTab).hasText('History', 'history tab is active');
|
||||
await visit('/vault/clients/dashboard');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct');
|
||||
assert.dom(SELECTORS.dashboardActiveTab).hasText('Dashboard', 'dashboard tab is active');
|
||||
// Message changes depending on ent or OSS
|
||||
assert.dom(SELECTORS.emptyStateTitle).exists('Empty state exists');
|
||||
assert.dom(SELECTORS.monthDropdown).exists('Dropdown exists to select month');
|
||||
|
@ -382,8 +381,8 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||
this.server.get('sys/internal/counters/config', () => overrideResponse(403));
|
||||
this.server.get('sys/internal/counters/activity', () => overrideResponse(403));
|
||||
|
||||
await visit('/vault/clients/history');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
|
||||
await visit('/vault/clients/dashboard');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/dashboard', 'clients/dashboard URL is correct');
|
||||
assert
|
||||
.dom(SELECTORS.emptyStateTitle)
|
||||
.includesText('start date found', 'Empty state shows no billing start date');
|
|
@ -1,4 +1,3 @@
|
|||
import { addMonths, differenceInCalendarMonths, formatRFC3339, startOfMonth } from 'date-fns';
|
||||
import { Response } from 'miragejs';
|
||||
|
||||
/** Scenarios
|
||||
|
@ -18,8 +17,7 @@ import { Response } from 'miragejs';
|
|||
License start date this month
|
||||
*/
|
||||
export const SELECTORS = {
|
||||
currentMonthActiveTab: '.active[data-test-current-month]',
|
||||
historyActiveTab: '.active[data-test-history]',
|
||||
dashboardActiveTab: '.active[data-test-dashboard]',
|
||||
emptyStateTitle: '[data-test-empty-state-title]',
|
||||
usageStats: '[data-test-usage-stats]',
|
||||
dateDisplay: '[data-test-date-display]',
|
||||
|
@ -32,6 +30,8 @@ export const SELECTORS = {
|
|||
runningTotalMonthStats: '[data-test-running-total="single-month-stats"]',
|
||||
runningTotalMonthlyCharts: '[data-test-running-total="monthly-charts"]',
|
||||
monthlyUsageBlock: '[data-test-monthly-usage]',
|
||||
selectedAuthMount: 'div#auth-method-search-select [data-test-selected-option] div',
|
||||
selectedNs: 'div#namespace-search-select [data-test-selected-option] div',
|
||||
};
|
||||
|
||||
export const CHART_ELEMENTS = {
|
||||
|
@ -74,187 +74,3 @@ export function overrideResponse(httpStatus, data) {
|
|||
}
|
||||
return new Response(200, { 'Content-Type': 'application/json' }, JSON.stringify(data));
|
||||
}
|
||||
|
||||
function generateNamespaceBlock(idx = 0, skipMounts = false) {
|
||||
let mountCount = 1;
|
||||
const nsBlock = {
|
||||
namespace_id: `${idx}UUID`,
|
||||
namespace_path: `${idx}/namespace`,
|
||||
counts: {
|
||||
clients: mountCount * 15,
|
||||
entity_clients: mountCount * 5,
|
||||
non_entity_clients: mountCount * 10,
|
||||
distinct_entities: mountCount * 5,
|
||||
non_entity_tokens: mountCount * 10,
|
||||
},
|
||||
};
|
||||
if (!skipMounts) {
|
||||
mountCount = Math.floor((Math.random() + idx) * 20);
|
||||
const mounts = [];
|
||||
Array.from(Array(mountCount)).forEach((v, index) => {
|
||||
mounts.push({
|
||||
mount_path: `auth/authid${index}`,
|
||||
counts: {
|
||||
clients: 5,
|
||||
entity_clients: 3,
|
||||
non_entity_clients: 2,
|
||||
distinct_entities: 3,
|
||||
non_entity_tokens: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
nsBlock.mounts = mounts;
|
||||
}
|
||||
return nsBlock;
|
||||
}
|
||||
|
||||
function generateCounts(max, arrayLength) {
|
||||
function randomBetween(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
var result = [];
|
||||
var sum = 0;
|
||||
for (var i = 0; i < arrayLength - 1; i++) {
|
||||
result[i] = randomBetween(1, max - (arrayLength - i - 1) - sum);
|
||||
sum += result[i];
|
||||
}
|
||||
result[arrayLength - 1] = max - sum;
|
||||
return result.sort((a, b) => b - a);
|
||||
}
|
||||
|
||||
function generateMonths(startDate, endDate, hasNoData = false) {
|
||||
const numberOfMonths = differenceInCalendarMonths(endDate, startDate) + 1;
|
||||
const months = [];
|
||||
|
||||
for (let i = 0; i < numberOfMonths; i++) {
|
||||
if (hasNoData) {
|
||||
months.push({
|
||||
timestamp: formatRFC3339(startOfMonth(addMonths(startDate, i))),
|
||||
counts: null,
|
||||
namespace: null,
|
||||
new_clients: null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const namespaces = Array.from(Array(5)).map((v, idx) => {
|
||||
return generateNamespaceBlock(idx);
|
||||
});
|
||||
const clients = numberOfMonths * 5 + i * 5;
|
||||
const [entity_clients, non_entity_clients] = generateCounts(clients, 2);
|
||||
const counts = {
|
||||
clients,
|
||||
entity_clients,
|
||||
non_entity_clients,
|
||||
distinct_entities: entity_clients,
|
||||
non_entity_tokens: non_entity_clients,
|
||||
};
|
||||
const new_counts = 5 + i;
|
||||
const [new_entity, new_non_entity] = generateCounts(new_counts, 2);
|
||||
months.push({
|
||||
timestamp: formatRFC3339(startOfMonth(addMonths(startDate, i))),
|
||||
counts,
|
||||
namespaces,
|
||||
new_clients: {
|
||||
counts: {
|
||||
distinct_entities: new_entity,
|
||||
entity_clients: new_entity,
|
||||
non_entity_tokens: new_non_entity,
|
||||
non_entity_clients: new_non_entity,
|
||||
clients: new_counts,
|
||||
},
|
||||
namespaces,
|
||||
},
|
||||
});
|
||||
}
|
||||
return months;
|
||||
}
|
||||
|
||||
export function generateActivityResponse(nsCount = 1, startDate, endDate) {
|
||||
if (nsCount === 0) {
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: {
|
||||
start_time: formatRFC3339(startDate),
|
||||
end_time: formatRFC3339(endDate),
|
||||
total: {
|
||||
clients: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
},
|
||||
by_namespace: [
|
||||
{
|
||||
namespace_id: `root`,
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
months: generateMonths(startDate, endDate, false),
|
||||
},
|
||||
};
|
||||
}
|
||||
const namespaces = Array.from(Array(nsCount)).map((v, idx) => {
|
||||
return generateNamespaceBlock(idx);
|
||||
});
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: {
|
||||
start_time: formatRFC3339(startDate),
|
||||
end_time: formatRFC3339(endDate),
|
||||
total: {
|
||||
clients: 999,
|
||||
entity_clients: 666,
|
||||
non_entity_clients: 333,
|
||||
},
|
||||
by_namespace: namespaces,
|
||||
months: generateMonths(startDate, endDate),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function generateCurrentMonthResponse(namespaceCount, skipMounts = false, configEnabled = true) {
|
||||
if (!configEnabled) {
|
||||
return {
|
||||
data: { id: 'no-data' },
|
||||
};
|
||||
}
|
||||
if (!namespaceCount) {
|
||||
return {
|
||||
request_id: 'monthly-response-id',
|
||||
data: {
|
||||
by_namespace: [],
|
||||
clients: 0,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
months: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
// generate by_namespace data
|
||||
const by_namespace = Array.from(Array(namespaceCount)).map((ns, idx) =>
|
||||
generateNamespaceBlock(idx, skipMounts)
|
||||
);
|
||||
const counts = by_namespace.reduce(
|
||||
(prev, curr) => {
|
||||
return {
|
||||
clients: prev.clients + curr.counts.clients,
|
||||
entity_clients: prev.entity_clients + curr.counts.entity_clients,
|
||||
non_entity_clients: prev.non_entity_clients + curr.counts.non_entity_clients,
|
||||
};
|
||||
},
|
||||
{ clients: 0, entity_clients: 0, non_entity_clients: 0 }
|
||||
);
|
||||
return {
|
||||
request_id: 'monthly-response-id',
|
||||
data: {
|
||||
by_namespace,
|
||||
...counts,
|
||||
months: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,140 +1,321 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click } from '@ember/test-helpers';
|
||||
import { render, click, findAll, find } from '@ember/test-helpers';
|
||||
import sinon from 'sinon';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import calendarDropdown from 'vault/tests/pages/components/calendar-widget';
|
||||
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
|
||||
import { subYears } from 'date-fns';
|
||||
import { subMonths, subYears } from 'date-fns';
|
||||
import format from 'date-fns/format';
|
||||
|
||||
module('Integration | Component | calendar-widget', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
const isDisplayingSameYear = (comparisonDate, calendarYear) => {
|
||||
return comparisonDate.getFullYear() === parseInt(calendarYear);
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
const CURRENT_YEAR = new Date().getFullYear();
|
||||
const PREVIOUS_YEAR = subYears(new Date(), 1).getFullYear();
|
||||
this.set('currentYear', CURRENT_YEAR);
|
||||
this.set('previousYear', PREVIOUS_YEAR);
|
||||
const CURRENT_DATE = new Date();
|
||||
this.set('currentDate', CURRENT_DATE);
|
||||
this.set('calendarStartDate', subMonths(CURRENT_DATE, 12));
|
||||
this.set('calendarEndDate', CURRENT_DATE);
|
||||
this.set('startTimestamp', subMonths(CURRENT_DATE, 12).toISOString());
|
||||
this.set('endTimestamp', CURRENT_DATE.toISOString());
|
||||
this.set('handleClientActivityQuery', sinon.spy());
|
||||
this.set('handleCurrentBillingPeriod', sinon.spy());
|
||||
this.set('arrayOfMonths', ARRAY_OF_MONTHS);
|
||||
this.set('endTimeFromResponse', [CURRENT_YEAR, 0]);
|
||||
});
|
||||
|
||||
test('it renders and can open the calendar view', async function (assert) {
|
||||
test('it renders and disables correct months when start date is 12 months ago', async function (assert) {
|
||||
assert.expect(14);
|
||||
await render(hbs`
|
||||
<CalendarWidget
|
||||
@arrayOfMonths={{this.arrayOfMonths}}
|
||||
@endTimeDisplay={{concat "January " this.currentYear}}
|
||||
@endTimeFromResponse={{this.endTimeFromResponse}}
|
||||
@handleClientActivityQuery={{this.handleClientActivityQuery}}
|
||||
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
|
||||
@startTimeDisplay={{concat "February " this.previousYear}}
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.endTimestamp}}
|
||||
@selectMonth={{this.handleClientActivityQuery}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(calendarDropdown.dateRangeTrigger).hasText(
|
||||
`${format(this.calendarStartDate, 'MMM yyyy')} -
|
||||
${format(this.calendarEndDate, 'MMM yyyy')}`,
|
||||
'renders and formats start and end dates'
|
||||
);
|
||||
await calendarDropdown.openCalendar();
|
||||
assert.ok(calendarDropdown.showsCalendar, 'renders the calendar component');
|
||||
|
||||
// assert months in current year are disabled/enabled correctly
|
||||
const monthButtons = findAll('[data-test-calendar-month]');
|
||||
const enabledMonths = [],
|
||||
disabledMonths = [];
|
||||
for (let monthIdx = 0; monthIdx < 12; monthIdx++) {
|
||||
if (monthIdx > this.calendarEndDate.getMonth()) {
|
||||
disabledMonths.push(monthButtons[monthIdx]);
|
||||
} else {
|
||||
enabledMonths.push(monthButtons[monthIdx]);
|
||||
}
|
||||
}
|
||||
enabledMonths.forEach((btn) => {
|
||||
assert
|
||||
.dom(btn)
|
||||
.doesNotHaveClass(
|
||||
'is-readOnly',
|
||||
`${ARRAY_OF_MONTHS[btn.id] + this.calendarEndDate.getFullYear()} is enabled`
|
||||
);
|
||||
});
|
||||
disabledMonths.forEach((btn) => {
|
||||
assert
|
||||
.dom(btn)
|
||||
.hasClass(
|
||||
'is-readOnly',
|
||||
`${ARRAY_OF_MONTHS[btn.id] + this.calendarEndDate.getFullYear()} is read only`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('it does not allow a user to click to a future year but does allow a user to click to previous year', async function (assert) {
|
||||
test('it renders and disables months before start timestamp', async function (assert) {
|
||||
await render(hbs`
|
||||
<CalendarWidget
|
||||
@arrayOfMonths={{this.arrayOfMonths}}
|
||||
@endTimeDisplay={{concat "March " this.currentYear}}
|
||||
@endTimeFromResponse={{this.endTimeFromResponse}}
|
||||
@handleClientActivityQuery={{this.handleClientActivityQuery}}
|
||||
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
|
||||
@startTimeDisplay={{concat "February " this.previousYear}}
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.endTimestamp}}
|
||||
@selectMonth={{this.handleClientActivityQuery}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await calendarDropdown.openCalendar();
|
||||
assert.dom('[data-test-future-year]').isDisabled('Future year is disabled');
|
||||
|
||||
assert.dom('[data-test-next-year]').isDisabled('Future year is disabled');
|
||||
await calendarDropdown.clickPreviousYear();
|
||||
assert.dom('[data-test-display-year]').hasText(this.previousYear.toString(), 'shows the previous year');
|
||||
assert
|
||||
.dom('[data-test-calendar-month="January"]')
|
||||
.hasClass(
|
||||
'is-readOnly',
|
||||
`January ${this.previousYear} is disabled because it comes before startTimeDisplay`
|
||||
);
|
||||
.dom('[data-test-display-year]')
|
||||
.hasText(`${subYears(this.currentDate, 1).getFullYear()}`, 'shows the previous year');
|
||||
assert.dom('[data-test-previous-year]').isDisabled('disables previous year');
|
||||
|
||||
// assert months in previous year are disabled/enabled correctly
|
||||
const monthButtons = findAll('[data-test-calendar-month]');
|
||||
const enabledMonths = [],
|
||||
disabledMonths = [];
|
||||
for (let monthIdx = 0; monthIdx < 12; monthIdx++) {
|
||||
if (monthIdx < this.calendarStartDate.getMonth()) {
|
||||
disabledMonths.push(monthButtons[monthIdx]);
|
||||
} else {
|
||||
enabledMonths.push(monthButtons[monthIdx]);
|
||||
}
|
||||
}
|
||||
disabledMonths.forEach((btn) => {
|
||||
assert
|
||||
.dom(btn)
|
||||
.hasClass(
|
||||
'is-readOnly',
|
||||
`${ARRAY_OF_MONTHS[btn.id] + this.calendarEndDate.getFullYear()} is read only`
|
||||
);
|
||||
});
|
||||
enabledMonths.forEach((btn) => {
|
||||
assert
|
||||
.dom(btn)
|
||||
.doesNotHaveClass(
|
||||
'is-readOnly',
|
||||
`${ARRAY_OF_MONTHS[btn.id] + this.calendarEndDate.getFullYear()} is enabled`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('it disables the current month', async function (assert) {
|
||||
test('it calls parent callback with correct arg when clicking "Current billing period"', async function (assert) {
|
||||
await render(hbs`
|
||||
<CalendarWidget
|
||||
@arrayOfMonths={{this.arrayOfMonths}}
|
||||
@endTimeDisplay={{concat "January " this.currentYear}}
|
||||
@endTimeFromResponse={{this.endTimeFromResponse}}
|
||||
@handleClientActivityQuery={{this.handleClientActivityQuery}}
|
||||
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
|
||||
@startTimeDisplay={{concat "February " this.previousYear}}
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.endTimestamp}}
|
||||
@selectMonth={{this.handleClientActivityQuery}}
|
||||
/>
|
||||
`);
|
||||
await calendarDropdown.openCalendar();
|
||||
const month = this.arrayOfMonths[new Date().getMonth()];
|
||||
assert
|
||||
.dom(`[data-test-calendar-month="${month}"]`)
|
||||
.hasClass('is-readOnly', `${month} ${this.currentYear} is disabled`);
|
||||
// The component also disables all months after the current one, but this
|
||||
// is tricky to test since it's based on browser time, so the behavior
|
||||
// would be different in december than other months
|
||||
});
|
||||
|
||||
test('it allows you to reset the billing period', async function (assert) {
|
||||
await render(hbs`
|
||||
<CalendarWidget
|
||||
@arrayOfMonths={{this.arrayOfMonths}}
|
||||
@endTimeDisplay={{concat "January " this.currentYear}}
|
||||
@endTimeFromResponse={{this.endTimeFromResponse}}
|
||||
@handleClientActivityQuery={{this.handleClientActivityQuery}}
|
||||
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
|
||||
@startTimeDisplay={{concat "February " this.previousYear}}
|
||||
/>
|
||||
`);
|
||||
await calendarDropdown.menuToggle();
|
||||
await calendarDropdown.clickCurrentBillingPeriod();
|
||||
assert.ok(this.handleCurrentBillingPeriod.calledOnce, 'it calls the parents handleCurrentBillingPeriod');
|
||||
});
|
||||
|
||||
test('it passes the appropriate data to the handleCurrentBillingPeriod when a date is selected', async function (assert) {
|
||||
await render(hbs`
|
||||
<CalendarWidget
|
||||
@arrayOfMonths={{this.arrayOfMonths}}
|
||||
@endTimeDisplay={{concat "January " this.currentYear}}
|
||||
@endTimeFromResponse={{this.endTimeFromResponse}}
|
||||
@handleClientActivityQuery={{this.handleClientActivityQuery}}
|
||||
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
|
||||
@startTimeDisplay={{concat "February " this.previousYear}}
|
||||
/>
|
||||
`);
|
||||
await calendarDropdown.openCalendar();
|
||||
await calendarDropdown.clickPreviousYear();
|
||||
await click('[data-test-calendar-month="October"]'); // select endTime of October 2021
|
||||
assert.ok(this.handleClientActivityQuery.calledOnce, 'it calls the parents handleClientActivityQuery');
|
||||
assert.ok(
|
||||
this.handleClientActivityQuery.calledWith(9, this.previousYear, 'endTime'),
|
||||
'Passes the month as an index, year and date type to the parent'
|
||||
assert.propEqual(
|
||||
this.handleClientActivityQuery.args[0][0],
|
||||
{ dateType: 'reset' },
|
||||
'it calls parent function with reset dateType'
|
||||
);
|
||||
});
|
||||
|
||||
test('it displays the year from endTimeDisplay when opened', async function (assert) {
|
||||
this.set('endTimeFromResponse', [this.previousYear, 11]);
|
||||
test('it calls parent callback with correct arg when clicking "Current month"', async function (assert) {
|
||||
await render(hbs`
|
||||
<CalendarWidget
|
||||
@arrayOfMonths={{this.arrayOfMonths}}
|
||||
@endTimeDisplay={{concat "December " this.previousYear}}
|
||||
@endTimeFromResponse={{this.endTimeFromResponse}}
|
||||
@handleClientActivityQuery={{this.handleClientActivityQuery}}
|
||||
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
|
||||
@startTimeDisplay="March 2020"
|
||||
/>
|
||||
`);
|
||||
<CalendarWidget
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.endTimestamp}}
|
||||
@selectMonth={{this.handleClientActivityQuery}}
|
||||
/>
|
||||
`);
|
||||
await calendarDropdown.menuToggle();
|
||||
await calendarDropdown.clickCurrentMonth();
|
||||
assert.propEqual(
|
||||
this.handleClientActivityQuery.args[0][0],
|
||||
{ dateType: 'currentMonth' },
|
||||
'it calls parent function with currentMoth dateType'
|
||||
);
|
||||
});
|
||||
|
||||
test('it calls parent callback with correct arg when selecting a month', async function (assert) {
|
||||
await render(hbs`
|
||||
<CalendarWidget
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.endTimestamp}}
|
||||
@selectMonth={{this.handleClientActivityQuery}}
|
||||
/>
|
||||
`);
|
||||
await calendarDropdown.openCalendar();
|
||||
assert
|
||||
.dom('[data-test-display-year]')
|
||||
.hasText(this.previousYear.toString(), 'Shows year from the end response');
|
||||
await click(`[data-test-calendar-month="${ARRAY_OF_MONTHS[this.calendarEndDate.getMonth()]}"]`);
|
||||
assert.propEqual(
|
||||
this.handleClientActivityQuery.lastCall.lastArg,
|
||||
{
|
||||
dateType: 'endDate',
|
||||
monthIdx: this.currentDate.getMonth(),
|
||||
monthName: ARRAY_OF_MONTHS[this.currentDate.getMonth()],
|
||||
year: this.currentDate.getFullYear(),
|
||||
},
|
||||
'it calls parent function with end date (current) month/year'
|
||||
);
|
||||
|
||||
await calendarDropdown.openCalendar();
|
||||
await calendarDropdown.clickPreviousYear();
|
||||
await click(`[data-test-calendar-month="${ARRAY_OF_MONTHS[this.calendarStartDate.getMonth()]}"]`);
|
||||
assert.propEqual(
|
||||
this.handleClientActivityQuery.lastCall.lastArg,
|
||||
{
|
||||
dateType: 'endDate',
|
||||
monthIdx: this.currentDate.getMonth(),
|
||||
monthName: ARRAY_OF_MONTHS[this.currentDate.getMonth()],
|
||||
year: this.currentDate.getFullYear() - 1,
|
||||
},
|
||||
'it calls parent function with start date month/year'
|
||||
);
|
||||
});
|
||||
|
||||
test('it disables correct months when start date 6 months ago', async function (assert) {
|
||||
this.set('calendarStartDate', subMonths(this.currentDate, 6));
|
||||
this.set('startTimestamp', subMonths(this.currentDate, 6).toISOString());
|
||||
await render(hbs`
|
||||
<CalendarWidget
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.endTimestamp}}
|
||||
@selectMonth={{this.handleClientActivityQuery}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await calendarDropdown.openCalendar();
|
||||
assert.dom('[data-test-next-year]').isDisabled('Future year is disabled');
|
||||
|
||||
const displayYear = find('[data-test-display-year]').innerText;
|
||||
const isRangeSameYear = isDisplayingSameYear(this.calendarStartDate, displayYear);
|
||||
|
||||
// only click previous year if 6 months ago was last year
|
||||
if (!isRangeSameYear) {
|
||||
await calendarDropdown.clickPreviousYear();
|
||||
}
|
||||
assert.dom('[data-test-previous-year]').isDisabled('previous year is disabled');
|
||||
|
||||
// DOM calendar is viewing start date year
|
||||
findAll('[data-test-calendar-month]').forEach((m) => {
|
||||
// months before start month should always be disabled
|
||||
if (m.id < this.calendarStartDate.getMonth()) {
|
||||
assert.dom(m).hasClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is read only`);
|
||||
}
|
||||
// if start/end dates are in the same year, DOM is also showing end date
|
||||
if (isRangeSameYear) {
|
||||
// months after end date should be disabled
|
||||
if (m.id > this.calendarEndDate.getMonth()) {
|
||||
assert.dom(m).hasClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is read only`);
|
||||
}
|
||||
// months between including start/end month should be enabled
|
||||
if (m.id >= this.calendarStartDate.getMonth() && m.id <= this.calendarEndDate.getMonth()) {
|
||||
assert.dom(m).doesNotHaveClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is enabled`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// click back to current year if duration spans multiple years
|
||||
if (!isRangeSameYear) {
|
||||
await click('[data-test-next-year]');
|
||||
findAll('[data-test-calendar-month]').forEach((m) => {
|
||||
// DOM is no longer showing start month, all months before current date should be enabled
|
||||
if (m.id <= this.currentDate.getMonth()) {
|
||||
assert.dom(m).doesNotHaveClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is enabled`);
|
||||
}
|
||||
// future months should be disabled
|
||||
if (m.id > this.currentDate.getMonth()) {
|
||||
assert.dom(m).hasClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is read only`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('it disables correct months when start date 36 months ago', async function (assert) {
|
||||
this.set('calendarStartDate', subMonths(this.currentDate, 36));
|
||||
this.set('startTimestamp', subMonths(this.currentDate, 36).toISOString());
|
||||
await render(hbs`
|
||||
<CalendarWidget
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.endTimestamp}}
|
||||
@selectMonth={{this.handleClientActivityQuery}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await calendarDropdown.openCalendar();
|
||||
assert.dom('[data-test-next-year]').isDisabled('Future year is disabled');
|
||||
|
||||
let displayYear = find('[data-test-display-year]').innerText;
|
||||
|
||||
while (!isDisplayingSameYear(this.calendarStartDate, displayYear)) {
|
||||
await calendarDropdown.clickPreviousYear();
|
||||
displayYear = find('[data-test-display-year]').innerText;
|
||||
}
|
||||
|
||||
assert.dom('[data-test-previous-year]').isDisabled('previous year is disabled');
|
||||
assert.dom('[data-test-next-year]').isEnabled('next year is enabled');
|
||||
|
||||
// DOM calendar is viewing start date year (3 years ago)
|
||||
findAll('[data-test-calendar-month]').forEach((m) => {
|
||||
// months before start month should always be disabled
|
||||
if (m.id < this.calendarStartDate.getMonth()) {
|
||||
assert.dom(m).hasClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is read only`);
|
||||
}
|
||||
if (m.id >= this.calendarStartDate.getMonth()) {
|
||||
assert.dom(m).doesNotHaveClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is enabled`);
|
||||
}
|
||||
});
|
||||
|
||||
await click('[data-test-next-year]');
|
||||
displayYear = await find('[data-test-display-year]').innerText;
|
||||
|
||||
if (!isDisplayingSameYear(this.currentDate, displayYear)) {
|
||||
await findAll('[data-test-calendar-month]').forEach((m) => {
|
||||
// years between should have all months enabled
|
||||
assert.dom(m).doesNotHaveClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is enabled`);
|
||||
});
|
||||
}
|
||||
|
||||
await click('[data-test-next-year]');
|
||||
displayYear = await find('[data-test-display-year]').innerText;
|
||||
|
||||
if (!isDisplayingSameYear(this.currentDate, displayYear)) {
|
||||
await findAll('[data-test-calendar-month]').forEach((m) => {
|
||||
// years between should have all months enabled
|
||||
assert.dom(m).doesNotHaveClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is enabled`);
|
||||
});
|
||||
}
|
||||
|
||||
await click('[data-test-next-year]');
|
||||
displayYear = await find('[data-test-display-year]').innerText;
|
||||
// now DOM is showing current year
|
||||
assert.dom('[data-test-next-year]').isDisabled('Future year is disabled');
|
||||
if (isDisplayingSameYear(this.currentDate, displayYear)) {
|
||||
findAll('[data-test-calendar-month]').forEach((m) => {
|
||||
// all months before current month should be enabled
|
||||
if (m.id <= this.currentDate.getMonth()) {
|
||||
assert.dom(m).doesNotHaveClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is enabled`);
|
||||
}
|
||||
// future months should be disabled
|
||||
if (m.id > this.currentDate.getMonth()) {
|
||||
assert.dom(m).hasClass('is-readOnly', `${ARRAY_OF_MONTHS[m.id] + displayYear} is read only`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,16 +2,17 @@ import { module, test } from 'qunit';
|
|||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { formatRFC3339 } from 'date-fns';
|
||||
import { endOfMonth, formatRFC3339 } from 'date-fns';
|
||||
import { click } from '@ember/test-helpers';
|
||||
import subMonths from 'date-fns/subMonths';
|
||||
|
||||
module('Integration | Component | clients/attribution', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.set('startTimestamp', formatRFC3339(subMonths(new Date(), 6)));
|
||||
this.set('timestamp', formatRFC3339(new Date()));
|
||||
this.set('selectedNamespace', null);
|
||||
this.set('isDateRange', true);
|
||||
this.set('chartLegend', [
|
||||
{ label: 'entity clients', key: 'entity_clients' },
|
||||
{ label: 'non-entity clients', key: 'non_entity_clients' },
|
||||
|
@ -48,9 +49,11 @@ module('Integration | Component | clients/attribution', function (hooks) {
|
|||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientAttribution={{this.totalClientAttribution}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@timestamp={{this.timestamp}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.timestamp}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@isDateRange={{this.isDateRange}}
|
||||
@isHistoricalMonth={{false}}
|
||||
/>
|
||||
`);
|
||||
|
||||
|
@ -71,17 +74,20 @@ module('Integration | Component | clients/attribution', function (hooks) {
|
|||
assert.dom('[data-test-attribution-clients]').includesText('namespace').includesText('10');
|
||||
});
|
||||
|
||||
test('it renders correct text for a single month', async function (assert) {
|
||||
this.set('isDateRange', false);
|
||||
test('it renders two charts and correct text for single, historical month', async function (assert) {
|
||||
this.start = formatRFC3339(subMonths(new Date(), 1));
|
||||
this.end = formatRFC3339(subMonths(endOfMonth(new Date()), 1));
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientAttribution={{this.totalClientAttribution}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@timestamp={{this.timestamp}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
@startTimestamp={{this.start}}
|
||||
@endTimestamp={{this.end}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@isDateRange={{this.isDateRange}}
|
||||
@isHistoricalMonth={{true}}
|
||||
/>
|
||||
`);
|
||||
assert
|
||||
|
@ -124,6 +130,51 @@ module('Integration | Component | clients/attribution', function (hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test('it renders single chart for current month', async function (assert) {
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientAttribution={{this.totalClientAttribution}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
@startTimestamp={{this.timestamp}}
|
||||
@endTimestamp={{this.timestamp}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@isHistoricalMonth={{false}}
|
||||
/>
|
||||
`);
|
||||
assert
|
||||
.dom('[data-test-chart-container="single-chart"]')
|
||||
.exists('renders single chart with total clients');
|
||||
assert
|
||||
.dom('[data-test-attribution-subtext]')
|
||||
.hasTextContaining('this month', 'renders total monthly namespace text');
|
||||
});
|
||||
|
||||
test('it renders single chart and correct text for for date range', async function (assert) {
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientAttribution={{this.totalClientAttribution}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.timestamp}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@isHistoricalMonth={{false}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert
|
||||
.dom('[data-test-chart-container="single-chart"]')
|
||||
.exists('renders single chart with total clients');
|
||||
assert
|
||||
.dom('[data-test-attribution-subtext]')
|
||||
.hasTextContaining('date range', 'renders total monthly namespace text');
|
||||
});
|
||||
|
||||
test('it renders with data for selected namespace auth methods for a date range', async function (assert) {
|
||||
this.set('selectedNamespace', 'second');
|
||||
await render(hbs`
|
||||
|
@ -132,9 +183,11 @@ module('Integration | Component | clients/attribution', function (hooks) {
|
|||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientAttribution={{this.namespaceMountsData}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@timestamp={{this.timestamp}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.timestamp}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@isDateRange={{this.isDateRange}}
|
||||
@isHistoricalMonth={{this.isHistoricalMonth}}
|
||||
/>
|
||||
`);
|
||||
|
||||
|
@ -161,13 +214,13 @@ module('Integration | Component | clients/attribution', function (hooks) {
|
|||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalClientAttribution={{this.namespaceMountsData}}
|
||||
@timestamp={{this.timestamp}}
|
||||
@startTimeDisplay={{"January 2022"}}
|
||||
@endTimeDisplay={{"February 2022"}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
@startTimestamp="2022-06-01T23:00:11.050Z"
|
||||
@endTimestamp="2022-12-01T23:00:11.050Z"
|
||||
/>
|
||||
`);
|
||||
await click('[data-test-attribution-export-button]');
|
||||
assert.dom('.modal.is-active .title').hasText('Export attribution data', 'modal appears to export csv');
|
||||
assert.dom('.modal.is-active').includesText('January 2022 - February 2022');
|
||||
assert.dom('.modal.is-active').includesText('June 2022 - December 2022');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1421,7 +1421,7 @@ module('Integration | Component | clients/monthly-usage', function (hooks) {
|
|||
test('it renders empty state with no data', async function (assert) {
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::MonthlyUsage @chartLegend={{this.chartLegend}} @timestamp={{this.timestamp}}/>
|
||||
<Clients::MonthlyUsage @chartLegend={{this.chartLegend}} @responseTimestamp={{this.timestamp}}/>
|
||||
`);
|
||||
assert.dom('[data-test-monthly-usage]').exists('monthly usage component renders');
|
||||
assert.dom('[data-test-component="empty-state"]').exists();
|
||||
|
@ -1446,7 +1446,7 @@ module('Integration | Component | clients/monthly-usage', function (hooks) {
|
|||
<Clients::MonthlyUsage
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@verticalBarChartData={{this.byMonthActivityData}}
|
||||
@timestamp={{this.timestamp}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
/>
|
||||
`);
|
||||
assert.dom('[data-test-monthly-usage]').exists('monthly usage component renders');
|
||||
|
|
|
@ -1424,7 +1424,6 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
|||
|
||||
test('it renders with full monthly activity data', async function (assert) {
|
||||
this.set('byMonthActivityData', MONTHLY_ACTIVITY);
|
||||
this.set('byMonthNewClients', NEW_ACTIVITY);
|
||||
this.set('totalUsageCounts', TOTAL_USAGE_COUNTS);
|
||||
const expectedTotalEntity = formatNumber([TOTAL_USAGE_COUNTS.entity_clients]);
|
||||
const expectedTotalNonEntity = formatNumber([TOTAL_USAGE_COUNTS.non_entity_clients]);
|
||||
|
@ -1436,11 +1435,11 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
|||
<Clients::RunningTotal
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@selectedAuthMethod={{this.selectedAuthMethod}}
|
||||
// @lineChartData={{this.byMonthActivityData}}
|
||||
// @barChartData={{this.byMonthNewClients}}
|
||||
@byMonthActivityData={{this.byMonthActivityData}}
|
||||
@runningTotals={{this.totalUsageCounts}}
|
||||
@upgradeData={{this.upgradeDuringActivity}}
|
||||
@timestamp={{this.timestamp}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
@isHistoricalMonth={{false}}
|
||||
/>
|
||||
`);
|
||||
|
||||
|
@ -1496,8 +1495,8 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
|||
});
|
||||
|
||||
test('it renders with no new monthly data', async function (assert) {
|
||||
this.set('byMonthActivityData', MONTHLY_ACTIVITY);
|
||||
this.set('byMonthNewClients', NEW_ACTIVITY);
|
||||
const monthlyWithoutNew = MONTHLY_ACTIVITY.map((d) => ({ ...d, new_clients: { month: d.month } }));
|
||||
this.set('byMonthActivityData', monthlyWithoutNew);
|
||||
this.set('totalUsageCounts', TOTAL_USAGE_COUNTS);
|
||||
const expectedTotalEntity = formatNumber([TOTAL_USAGE_COUNTS.entity_clients]);
|
||||
const expectedTotalNonEntity = formatNumber([TOTAL_USAGE_COUNTS.non_entity_clients]);
|
||||
|
@ -1507,12 +1506,12 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
|||
<Clients::RunningTotal
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@selectedAuthMethod={{this.selectedAuthMethod}}
|
||||
@lineChartData={{this.byMonthActivityData}}
|
||||
@byMonthActivityData={{this.byMonthActivityData}}
|
||||
@runningTotals={{this.totalUsageCounts}}
|
||||
@timestamp={{this.timestamp}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
@isHistoricalMonth={{false}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-running-total]').exists('running total component renders');
|
||||
assert.dom('[data-test-line-chart]').exists('line chart renders');
|
||||
assert.dom('[data-test-vertical-bar-chart]').doesNotExist('vertical bar chart does not render');
|
||||
|
@ -1528,17 +1527,16 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
|||
.hasText(`${expectedTotalNonEntity}`, `renders correct new average ${expectedTotalNonEntity}`);
|
||||
assert
|
||||
.dom('[data-test-running-new-entity] p.data-details')
|
||||
.hasText('0', 'renders 0 average new entity clients');
|
||||
.doesNotExist('new client counts does not exist');
|
||||
assert
|
||||
.dom('[data-test-running-new-nonentity] p.data-details')
|
||||
.hasText('0', 'renders 0 average entity clients');
|
||||
.doesNotExist('average new client counts does not exist');
|
||||
});
|
||||
|
||||
test('it renders with single historical month data', async function (assert) {
|
||||
const singleMonth = MONTHLY_ACTIVITY[MONTHLY_ACTIVITY.length - 1];
|
||||
const singleMonthNew = NEW_ACTIVITY[NEW_ACTIVITY.length - 1];
|
||||
this.set('singleMonth', [singleMonth]);
|
||||
this.set('singleMonthNew', [singleMonthNew]);
|
||||
const expectedTotalClients = formatNumber([singleMonth.clients]);
|
||||
const expectedTotalEntity = formatNumber([singleMonth.entity_clients]);
|
||||
const expectedTotalNonEntity = formatNumber([singleMonth.non_entity_clients]);
|
||||
|
@ -1551,10 +1549,10 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
|||
<Clients::RunningTotal
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@selectedAuthMethod={{this.selectedAuthMethod}}
|
||||
@lineChartData={{this.singleMonth}}
|
||||
@barChartData={{this.singleMonthNew}}
|
||||
@byMonthActivityData={{this.singleMonth}}
|
||||
@runningTotals={{this.totalUsageCounts}}
|
||||
@timestamp={{this.timestamp}}
|
||||
@responseTimestamp={{this.timestamp}}
|
||||
@isHistoricalMonth={{true}}
|
||||
/>
|
||||
`);
|
||||
assert.dom('[data-test-running-total]').exists('running total component renders');
|
||||
|
|
|
@ -11,13 +11,15 @@ module('Integration | Component | clients/usage-stats', function (hooks) {
|
|||
|
||||
assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts even with no data passed');
|
||||
assert.dom('[data-test-stat-text="total-clients"]').exists('Total clients exists');
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('0', 'Value defaults to zero');
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('-', 'renders dash when no data');
|
||||
assert.dom('[data-test-stat-text="entity-clients"]').exists('Entity clients exists');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('0', 'Value defaults to zero');
|
||||
assert
|
||||
.dom('[data-test-stat-text="entity-clients"] .stat-value')
|
||||
.hasText('-', 'renders dash when no data');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"]').exists('Non entity clients exists');
|
||||
assert
|
||||
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
|
||||
.hasText('0', 'Value defaults to zero');
|
||||
.hasText('-', 'renders dash when no data');
|
||||
assert
|
||||
.dom('a')
|
||||
.hasAttribute('href', 'https://developer.hashicorp.com/vault/tutorials/monitoring/usage-metrics');
|
||||
|
|
|
@ -1,72 +1,79 @@
|
|||
/* eslint qunit/no-conditional-assertions: "warn" */
|
||||
import { module, skip, test } from 'qunit';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { click, render } from '@ember/test-helpers';
|
||||
import { click, find, findAll, render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
const ARRAY_OF_MONTHS = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
const CURRENT_DATE = new Date();
|
||||
const CURRENT_YEAR = CURRENT_DATE.getFullYear(); // integer of year
|
||||
const CURRENT_MONTH = CURRENT_DATE.getMonth(); // index of month
|
||||
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
|
||||
|
||||
module('Integration | Component | date-dropdown', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders dropdown', async function (assert) {
|
||||
this.set('text', 'Save');
|
||||
hooks.before(function () {
|
||||
const currentDate = new Date();
|
||||
this.currentYear = currentDate.getFullYear(); // integer of year
|
||||
this.currentMonth = currentDate.getMonth(); // index of month
|
||||
});
|
||||
|
||||
test('it renders dropdown', async function (assert) {
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown/>
|
||||
</div>
|
||||
`);
|
||||
assert.dom('[data-test-date-dropdown-submit]').hasText('Submit', 'button renders default text');
|
||||
assert
|
||||
.dom('[data-test-date-dropdown-cancel]')
|
||||
.doesNotExist('it does not render cancel button by default');
|
||||
});
|
||||
|
||||
test('it fires off cancel callback', async function (assert) {
|
||||
assert.expect(2);
|
||||
const onCancel = () => {
|
||||
assert.ok('fires onCancel callback');
|
||||
};
|
||||
this.set('onCancel', onCancel);
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown @submitText={{this.text}}/>
|
||||
<DateDropdown @handleCancel={{this.onCancel}} @submitText="Save"/>
|
||||
</div>
|
||||
`);
|
||||
assert.dom('[data-test-date-dropdown-submit]').hasText('Save', 'button renders passed in text');
|
||||
await click(find('[data-test-date-dropdown-cancel]'));
|
||||
});
|
||||
|
||||
// skip until https://github.com/hashicorp/vault/pull/17575 is merged which refactors these tests and fixes flakiness
|
||||
skip('it renders dropdown and selects month and year', async function (assert) {
|
||||
assert.expect(27);
|
||||
const parentAction = (month, year) => {
|
||||
assert.strictEqual(month, 'January', 'sends correct month to parent callback');
|
||||
assert.strictEqual(year, CURRENT_YEAR, 'sends correct year to parent callback');
|
||||
test('it renders dropdown and selects month and year', async function (assert) {
|
||||
assert.expect(26);
|
||||
const parentAction = (args) => {
|
||||
assert.propEqual(
|
||||
args,
|
||||
{
|
||||
dateType: 'start',
|
||||
monthIdx: 0,
|
||||
monthName: 'January',
|
||||
year: this.currentYear,
|
||||
},
|
||||
'sends correct args to parent'
|
||||
);
|
||||
};
|
||||
this.set('parentAction', parentAction);
|
||||
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown
|
||||
@handleDateSelection={{this.parentAction}} />
|
||||
</div>
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown
|
||||
@handleSubmit={{this.parentAction}}
|
||||
@dateType="start"
|
||||
/>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const monthDropdown = this.element.querySelector('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = this.element.querySelector('[data-test-popup-menu-trigger="year"]');
|
||||
const submitButton = this.element.querySelector('[data-test-date-dropdown-submit]');
|
||||
const monthDropdown = find('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = find('[data-test-popup-menu-trigger="year"]');
|
||||
const submitButton = find('[data-test-date-dropdown-submit]');
|
||||
|
||||
assert.true(submitButton.disabled, 'button is disabled when no month or year selected');
|
||||
|
||||
await click(monthDropdown);
|
||||
const dropdownListMonths = this.element.querySelectorAll('[data-test-month-list] button');
|
||||
const dropdownListMonths = findAll('[data-test-month-list] button');
|
||||
|
||||
assert.strictEqual(dropdownListMonths.length, 12, 'dropdown has 12 months');
|
||||
for (const [index, month] of ARRAY_OF_MONTHS.entries()) {
|
||||
assert.dom(dropdownListMonths[index]).hasText(`${month}`, `dropdown includes ${month}`);
|
||||
|
@ -77,98 +84,121 @@ module('Integration | Component | date-dropdown', function (hooks) {
|
|||
assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting month');
|
||||
|
||||
await click(yearDropdown);
|
||||
const dropdownListYears = this.element.querySelectorAll('[data-test-year-list] button');
|
||||
const dropdownListYears = findAll('[data-test-year-list] button');
|
||||
assert.strictEqual(dropdownListYears.length, 5, 'dropdown has 5 years');
|
||||
|
||||
for (const [index, year] of dropdownListYears.entries()) {
|
||||
const comparisonYear = CURRENT_YEAR - index;
|
||||
const comparisonYear = this.currentYear - index;
|
||||
assert.dom(year).hasText(`${comparisonYear}`, `dropdown includes ${comparisonYear}`);
|
||||
}
|
||||
|
||||
await click(dropdownListYears[0]);
|
||||
assert.dom(yearDropdown).hasText(`${CURRENT_YEAR}`, `dropdown selects ${CURRENT_YEAR}`);
|
||||
assert.dom(yearDropdown).hasText(`${this.currentYear}`, `dropdown selects ${this.currentYear}`);
|
||||
assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting year');
|
||||
assert.false(submitButton.disabled, 'button enabled when month and year selected');
|
||||
|
||||
await click(submitButton);
|
||||
});
|
||||
|
||||
skip('it disables correct years when selecting month first', async function (assert) {
|
||||
assert.expect(60);
|
||||
test('selecting month first: it enables current year when selecting valid months', async function (assert) {
|
||||
// the date dropdown displays 5 years, multiply by month to calculate how many assertions to expect
|
||||
const datesEnabled = (this.currentMonth + 1) * 5;
|
||||
assert.expect(datesEnabled);
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown/>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const monthDropdown = this.element.querySelector('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = this.element.querySelector('[data-test-popup-menu-trigger="year"]');
|
||||
const monthDropdown = find('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = find('[data-test-popup-menu-trigger="year"]');
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
// select months before or equal to current month and assert year is enabled
|
||||
for (let monthIdx = 0; monthIdx < this.currentMonth + 1; monthIdx++) {
|
||||
await click(monthDropdown);
|
||||
const dropdownListMonths = this.element.querySelectorAll('[data-test-month-list] button');
|
||||
await click(dropdownListMonths[i]);
|
||||
|
||||
const dropdownListMonths = findAll('[data-test-month-list] button');
|
||||
await click(dropdownListMonths[monthIdx]);
|
||||
await click(yearDropdown);
|
||||
const dropdownListYears = this.element.querySelectorAll('[data-test-year-list] button');
|
||||
|
||||
if (i < CURRENT_MONTH) {
|
||||
for (const year of dropdownListYears) {
|
||||
assert.false(year.disabled, `${ARRAY_OF_MONTHS[i]} ${year.innerText} valid`);
|
||||
}
|
||||
} else {
|
||||
for (const [yearIndex, year] of dropdownListYears.entries()) {
|
||||
if (yearIndex === 0) {
|
||||
assert.true(year.disabled, `${ARRAY_OF_MONTHS[i]} ${year.innerText} disabled`);
|
||||
} else {
|
||||
assert.false(year.disabled, `${ARRAY_OF_MONTHS[i]} ${year.innerText} valid`);
|
||||
}
|
||||
}
|
||||
const dropdownListYears = findAll('[data-test-year-list] button');
|
||||
for (const year of dropdownListYears) {
|
||||
assert.false(year.disabled, `${ARRAY_OF_MONTHS[monthIdx]} ${year.innerText} enabled`);
|
||||
}
|
||||
await click(yearDropdown);
|
||||
}
|
||||
});
|
||||
|
||||
skip('it disables correct months when selecting year first', async function (assert) {
|
||||
assert.expect(60);
|
||||
test('selecting month first: it disables current year when selecting future months', async function (assert) {
|
||||
// assertions only run for future months
|
||||
const yearsDisabled = 11 - this.currentMonth; // ex: in December, current year is enabled for all months, so 0 assertions will run
|
||||
assert.expect(yearsDisabled);
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown/>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const monthDropdown = this.element.querySelector('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = this.element.querySelector('[data-test-popup-menu-trigger="year"]');
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await click(yearDropdown);
|
||||
const dropdownListYears = this.element.querySelectorAll('[data-test-year-list] button');
|
||||
await click(dropdownListYears[i]);
|
||||
const monthDropdown = find('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = find('[data-test-popup-menu-trigger="year"]');
|
||||
|
||||
// select future months and assert current year is disabled
|
||||
for (let monthIdx = this.currentMonth + 1; monthIdx < 12; monthIdx++) {
|
||||
await click(monthDropdown);
|
||||
const dropdownListMonths = this.element.querySelectorAll('[data-test-month-list] button');
|
||||
const dropdownListMonths = findAll('[data-test-month-list] button');
|
||||
await click(dropdownListMonths[monthIdx]);
|
||||
await click(yearDropdown);
|
||||
const dropdownListYears = findAll('[data-test-year-list] button');
|
||||
const currentYear = dropdownListYears[0];
|
||||
assert.true(currentYear.disabled, `${ARRAY_OF_MONTHS[monthIdx]} ${currentYear.innerText} disabled`);
|
||||
await click(yearDropdown);
|
||||
}
|
||||
});
|
||||
|
||||
if (i === 0) {
|
||||
for (const [monthIndex, month] of dropdownListMonths.entries()) {
|
||||
if (monthIndex < CURRENT_MONTH) {
|
||||
assert.false(
|
||||
month.disabled,
|
||||
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[i].innerText.trim()} valid`
|
||||
);
|
||||
} else {
|
||||
assert.true(
|
||||
month.disabled,
|
||||
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[i].innerText.trim()} disabled`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [monthIndex, month] of dropdownListMonths.entries()) {
|
||||
assert.false(
|
||||
month.disabled,
|
||||
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[i].innerText.trim()} valid`
|
||||
);
|
||||
}
|
||||
test('selecting year first: it disables future months when current year selected', async function (assert) {
|
||||
assert.expect(12);
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown/>
|
||||
</div>
|
||||
`);
|
||||
const monthDropdown = find('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = find('[data-test-popup-menu-trigger="year"]');
|
||||
await click(yearDropdown);
|
||||
await click(`[data-test-dropdown-year="${this.currentYear}"]`);
|
||||
await click(monthDropdown);
|
||||
const dropdownListMonths = findAll('[data-test-month-list] button');
|
||||
const enabledMonths = dropdownListMonths.slice(0, this.currentMonth + 1);
|
||||
const disabledMonths = dropdownListMonths.slice(this.currentMonth + 1);
|
||||
for (const [monthIndex, month] of enabledMonths.entries()) {
|
||||
assert.false(month.disabled, `${ARRAY_OF_MONTHS[monthIndex]} ${this.currentYear} enabled`);
|
||||
}
|
||||
for (const [monthIndex, month] of disabledMonths.entries()) {
|
||||
assert.true(month.disabled, `${ARRAY_OF_MONTHS[monthIndex]} ${this.currentYear} disabled`);
|
||||
}
|
||||
});
|
||||
|
||||
test('selecting year first: it enables all months when past year is selected', async function (assert) {
|
||||
assert.expect(48);
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown/>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const monthDropdown = find('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = find('[data-test-popup-menu-trigger="year"]');
|
||||
|
||||
// start at 1 because current year (index=0) is accounted for in previous test
|
||||
for (let yearIdx = 1; yearIdx < 5; yearIdx++) {
|
||||
await click(yearDropdown);
|
||||
const dropdownListYears = findAll('[data-test-year-list] button');
|
||||
await click(dropdownListYears[yearIdx]);
|
||||
await click(monthDropdown);
|
||||
const dropdownListMonths = findAll('[data-test-month-list] button');
|
||||
for (const [monthIndex, month] of dropdownListMonths.entries()) {
|
||||
assert.false(
|
||||
month.disabled,
|
||||
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[yearIdx].innerText.trim()} enabled`
|
||||
);
|
||||
}
|
||||
await click(monthDropdown);
|
||||
}
|
||||
|
|
|
@ -11,18 +11,16 @@ module('Integration | Helper | date-format', function (hooks) {
|
|||
const today = new Date();
|
||||
this.set('today', today);
|
||||
|
||||
await render(hbs`<p data-test-date-format>Date: {{date-format this.today "yyyy"}}</p>`);
|
||||
assert
|
||||
.dom('[data-test-date-format]')
|
||||
.includesText(today.getFullYear(), 'it renders the date in the year format');
|
||||
await render(hbs`{{date-format this.today "yyyy"}}`);
|
||||
assert.dom(this.element).includesText(today.getFullYear(), 'it renders the date in the year format');
|
||||
});
|
||||
|
||||
test('it supports date timestamps', async function (assert) {
|
||||
const today = new Date().getTime();
|
||||
this.set('today', today);
|
||||
|
||||
await render(hbs`<p class="date-format">{{date-format this.today 'hh:mm:ss'}}</p>`);
|
||||
const formattedDate = document.querySelector('.date-format').innerText;
|
||||
await render(hbs`{{date-format this.today 'hh:mm:ss'}}`);
|
||||
const formattedDate = this.element.innerText;
|
||||
assert.ok(formattedDate.match(/^\d{2}:\d{2}:\d{2}$/));
|
||||
});
|
||||
|
||||
|
@ -30,37 +28,32 @@ module('Integration | Helper | date-format', function (hooks) {
|
|||
const todayString = new Date().getFullYear().toString();
|
||||
this.set('todayString', todayString);
|
||||
|
||||
await render(hbs`<p data-test-date-format>Date: {{date-format this.todayString "yyyy"}}</p>`);
|
||||
assert
|
||||
.dom('[data-test-date-format]')
|
||||
.includesText(todayString, 'it renders the a date if passed in as a string');
|
||||
await render(hbs`{{date-format this.todayString "yyyy"}}`);
|
||||
assert.dom(this.element).includesText(todayString, 'it renders the a date if passed in as a string');
|
||||
});
|
||||
|
||||
test('it supports ten digit dates', async function (assert) {
|
||||
const tenDigitDate = 1621785298;
|
||||
this.set('tenDigitDate', tenDigitDate);
|
||||
|
||||
await render(hbs`<p data-test-date-format>Date: {{date-format this.tenDigitDate "MM/dd/yyyy"}}</p>`);
|
||||
assert.dom('[data-test-date-format]').includesText('05/23/2021');
|
||||
await render(hbs`{{date-format this.tenDigitDate "MM/dd/yyyy"}}`);
|
||||
assert.dom(this.element).includesText('05/23/2021');
|
||||
});
|
||||
|
||||
test('it supports already formatted dates', async function (assert) {
|
||||
const formattedDate = new Date();
|
||||
this.set('formattedDate', formattedDate);
|
||||
|
||||
await render(
|
||||
hbs`<p data-test-date-format>Date: {{date-format this.formattedDate 'MMMM dd, yyyy hh:mm:ss a' isFormatted=true}}</p>`
|
||||
);
|
||||
assert.dom('[data-test-date-format]').includesText(format(formattedDate, 'MMMM dd, yyyy hh:mm:ss a'));
|
||||
await render(hbs`{{date-format this.formattedDate 'MMMM dd, yyyy hh:mm:ss a' isFormatted=true}}`);
|
||||
assert.dom(this.element).includesText(format(formattedDate, 'MMMM dd, yyyy hh:mm:ss a'));
|
||||
});
|
||||
|
||||
test('displays correct date when timestamp is in ISO 8601 format', async function (assert) {
|
||||
const timestampDate = '2021-09-01T00:00:00Z';
|
||||
test('displays time zone if withTimeZone=true', async function (assert) {
|
||||
const timestampDate = '2022-12-06T11:29:15-08:00';
|
||||
const zone = new Date().toLocaleTimeString(undefined, { timeZoneName: 'short' }).split(' ')[2];
|
||||
this.set('timestampDate', timestampDate);
|
||||
|
||||
await render(
|
||||
hbs`<p data-test-date-format>Date: {{date-format this.timestampDate 'MMM dd, yyyy' dateOnly=true}}</p>`
|
||||
);
|
||||
assert.dom('[data-test-date-format]').includesText('Date: Sep 01, 2021');
|
||||
await render(hbs`{{date-format this.timestampDate 'MMM d yyyy, h:mm:ss aaa' withTimeZone=true}}`);
|
||||
assert.dom(this.element).hasTextContaining(`${zone}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -663,6 +663,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
new_clients: {
|
||||
month: '6/21',
|
||||
namespaces: [],
|
||||
timestamp: '2021-06-01T00:00:00Z',
|
||||
},
|
||||
timestamp: '2021-06-01T00:00:00Z',
|
||||
},
|
||||
|
@ -674,6 +675,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
new_clients: {
|
||||
month: '7/21',
|
||||
namespaces: [],
|
||||
timestamp: '2021-07-01T00:00:00Z',
|
||||
},
|
||||
timestamp: '2021-07-01T00:00:00Z',
|
||||
},
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { format, formatRFC3339, isSameDay, isSameMonth, isSameYear } from 'date-fns';
|
||||
import {
|
||||
ARRAY_OF_MONTHS,
|
||||
parseAPITimestamp,
|
||||
parseRFC3339,
|
||||
formatChartDate,
|
||||
} from 'core/utils/date-formatters';
|
||||
import { parseAPITimestamp, formatChartDate } from 'core/utils/date-formatters';
|
||||
|
||||
module('Integration | Util | date formatters utils', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
@ -30,25 +25,6 @@ module('Integration | Util | date formatters utils', function (hooks) {
|
|||
assert.strictEqual(formattedTimestamp, format(DATE, 'MM yyyy'), 'it formats the date');
|
||||
});
|
||||
|
||||
test('parseRFC3339: convert timestamp to array for widget', async function (assert) {
|
||||
assert.expect(4);
|
||||
const arrayArg = ['2021', 2];
|
||||
assert.strictEqual(parseRFC3339(arrayArg), arrayArg, 'it returns arg if already an array');
|
||||
assert.strictEqual(
|
||||
parseRFC3339(UNIX_TIME),
|
||||
null,
|
||||
'it returns null parsing a timestamp of the wrong format'
|
||||
);
|
||||
|
||||
const parsedTimestamp = parseRFC3339(API_TIMESTAMP);
|
||||
assert.strictEqual(parsedTimestamp[0], format(DATE, 'yyyy'), 'first element is a string of the year');
|
||||
assert.strictEqual(
|
||||
ARRAY_OF_MONTHS[parsedTimestamp[1]],
|
||||
format(DATE, 'MMMM'),
|
||||
'second element is an integer of the month'
|
||||
);
|
||||
});
|
||||
|
||||
test('formatChartDate: expand chart date to full month and year', async function (assert) {
|
||||
assert.expect(1);
|
||||
const chartDate = '03/21';
|
||||
|
|
|
@ -2,11 +2,12 @@ import { clickable, create, isPresent } from 'ember-cli-page-object';
|
|||
|
||||
export default create({
|
||||
clickPreviousYear: clickable('[data-test-previous-year]'),
|
||||
clickCurrentMonth: clickable('[data-test-current-month]'),
|
||||
clickCurrentBillingPeriod: clickable('[data-test-current-billing-period]'),
|
||||
customEndMonthBtn: clickable('[data-test-show-calendar]'),
|
||||
menuToggle: clickable('[data-test-popup-menu-trigger="true"]'),
|
||||
showsCalendar: isPresent('[data-test-calendar-widget-container]'),
|
||||
|
||||
dateRangeTrigger: '[data-test-popup-menu-trigger="true"]',
|
||||
async openCalendar() {
|
||||
await this.menuToggle();
|
||||
await this.customEndMonthBtn();
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { lastDayOfMonth, subMonths, format, fromUnixTime, addMonths } from 'date-fns';
|
||||
import { parseAPITimestamp, ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
|
||||
|
||||
module('Unit | Adapter | clients activity', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.modelName = 'clients/activity';
|
||||
this.startDate = subMonths(new Date(), 6);
|
||||
this.endDate = new Date();
|
||||
this.readableUnix = (unix) => parseAPITimestamp(fromUnixTime(unix).toISOString(), 'MMMM dd yyyy');
|
||||
});
|
||||
|
||||
test('it does not format if both params are timestamp strings', async function (assert) {
|
||||
assert.expect(1);
|
||||
const queryParams = {
|
||||
start_time: { timestamp: this.startDate.toISOString() },
|
||||
end_time: { timestamp: this.endDate.toISOString() },
|
||||
};
|
||||
this.server.get('sys/internal/counters/activity', (schema, req) => {
|
||||
assert.propEqual(req.queryParams, {
|
||||
start_time: this.startDate.toISOString(),
|
||||
end_time: this.endDate.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
this.store.queryRecord(this.modelName, queryParams);
|
||||
});
|
||||
|
||||
test('it formats start_time if only end_time is a timestamp string', async function (assert) {
|
||||
assert.expect(2);
|
||||
const twoMonthsAhead = addMonths(this.startDate, 2);
|
||||
const month = twoMonthsAhead.getMonth();
|
||||
const year = twoMonthsAhead.getFullYear();
|
||||
const queryParams = {
|
||||
start_time: {
|
||||
monthIdx: month,
|
||||
year,
|
||||
},
|
||||
end_time: {
|
||||
timestamp: this.endDate.toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
this.server.get('sys/internal/counters/activity', (schema, req) => {
|
||||
const { start_time, end_time } = req.queryParams;
|
||||
const readableStart = this.readableUnix(start_time);
|
||||
assert.strictEqual(
|
||||
readableStart,
|
||||
`${ARRAY_OF_MONTHS[month]} 01 ${year}`,
|
||||
`formatted unix start time is the first of the month: ${readableStart}`
|
||||
);
|
||||
assert.strictEqual(end_time, this.endDate.toISOString(), 'end time is a timestamp string');
|
||||
});
|
||||
|
||||
this.store.queryRecord(this.modelName, queryParams);
|
||||
});
|
||||
|
||||
test('it formats end_time only if only start_time is a timestamp string', async function (assert) {
|
||||
assert.expect(2);
|
||||
const twoMothsAgo = subMonths(this.endDate, 2);
|
||||
const month = twoMothsAgo.getMonth() - 2;
|
||||
const year = twoMothsAgo.getFullYear();
|
||||
const dayOfMonth = format(lastDayOfMonth(new Date(year, month, 10)), 'dd');
|
||||
const queryParams = {
|
||||
start_time: {
|
||||
timestamp: this.startDate.toISOString(),
|
||||
},
|
||||
end_time: {
|
||||
monthIdx: month,
|
||||
year,
|
||||
},
|
||||
};
|
||||
|
||||
this.server.get('sys/internal/counters/activity', (schema, req) => {
|
||||
const { start_time, end_time } = req.queryParams;
|
||||
const readableEnd = this.readableUnix(end_time);
|
||||
assert.strictEqual(start_time, this.startDate.toISOString(), 'start time is a timestamp string');
|
||||
assert.strictEqual(
|
||||
readableEnd,
|
||||
`${ARRAY_OF_MONTHS[month]} ${dayOfMonth} ${year}`,
|
||||
`formatted unix end time is the last day of the month: ${readableEnd}`
|
||||
);
|
||||
});
|
||||
|
||||
this.store.queryRecord(this.modelName, queryParams);
|
||||
});
|
||||
|
||||
test('it formats both params if neither are a timestamp', async function (assert) {
|
||||
assert.expect(2);
|
||||
const startDate = subMonths(this.startDate, 2);
|
||||
const endDate = addMonths(this.endDate, 2);
|
||||
const startMonth = startDate.getMonth() + 2;
|
||||
const startYear = startDate.getFullYear();
|
||||
const endMonth = endDate.getMonth() - 2;
|
||||
const endYear = endDate.getFullYear();
|
||||
const endDay = format(lastDayOfMonth(new Date(endYear, endMonth, 10)), 'dd');
|
||||
const queryParams = {
|
||||
start_time: {
|
||||
monthIdx: startMonth,
|
||||
year: startYear,
|
||||
},
|
||||
end_time: {
|
||||
monthIdx: endMonth,
|
||||
year: endYear,
|
||||
},
|
||||
};
|
||||
|
||||
this.server.get('sys/internal/counters/activity', (schema, req) => {
|
||||
const { start_time, end_time } = req.queryParams;
|
||||
const readableEnd = this.readableUnix(end_time);
|
||||
const readableStart = this.readableUnix(start_time);
|
||||
assert.strictEqual(
|
||||
readableStart,
|
||||
`${ARRAY_OF_MONTHS[startMonth]} 01 ${startYear}`,
|
||||
`formatted unix start time is the first of the month: ${readableStart}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
readableEnd,
|
||||
`${ARRAY_OF_MONTHS[endMonth]} ${endDay} ${endYear}`,
|
||||
`formatted unix end time is the last day of the month: ${readableEnd}`
|
||||
);
|
||||
});
|
||||
|
||||
this.store.queryRecord(this.modelName, queryParams);
|
||||
});
|
||||
});
|
|
@ -38,24 +38,10 @@ module('Unit | Utility | chart-helpers', function () {
|
|||
{ label: 'foo', value: undefined },
|
||||
{ label: 'bar', value: 22 },
|
||||
];
|
||||
const testArray3 = [{ label: 'foo' }, { label: 'bar' }];
|
||||
const getAverage = (array) => array.reduce((a, b) => a + b, 0) / array.length;
|
||||
assert.strictEqual(calculateAverage(null), null, 'returns null if dataset it null');
|
||||
assert.strictEqual(calculateAverage([]), null, 'returns null if dataset it empty array');
|
||||
assert.strictEqual(
|
||||
calculateAverage([0]),
|
||||
getAverage([0]),
|
||||
`returns ${getAverage([0])} if array is just 0 0`
|
||||
);
|
||||
assert.strictEqual(
|
||||
calculateAverage([1]),
|
||||
getAverage([1]),
|
||||
`returns ${getAverage([1])} if array is just 1`
|
||||
);
|
||||
assert.strictEqual(
|
||||
calculateAverage([5, 1, 41, 5]),
|
||||
getAverage([5, 1, 41, 5]),
|
||||
`returns correct average for array of integers`
|
||||
);
|
||||
assert.strictEqual(
|
||||
calculateAverage(testArray1, 'value'),
|
||||
getAverage([10, 22]),
|
||||
|
@ -66,5 +52,10 @@ module('Unit | Utility | chart-helpers', function () {
|
|||
getAverage([0, 22]),
|
||||
`returns correct average for array of objects containing undefined values`
|
||||
);
|
||||
assert.strictEqual(
|
||||
calculateAverage(testArray3, 'value'),
|
||||
null,
|
||||
'returns null when object key does not exist at all'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10006,10 +10006,10 @@ ember-maybe-in-element@^2.0.3:
|
|||
ember-cli-version-checker "^5.1.1"
|
||||
ember-in-element-polyfill "^1.0.1"
|
||||
|
||||
ember-modal-dialog@4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-modal-dialog/-/ember-modal-dialog-4.0.0.tgz#ab599ad14055550706800bd9aac3a2cbb7799748"
|
||||
integrity sha512-ofh+xG8v6Lg60R3JXaG9pW683OWCexDQT/JT7S0TOgywSHzdC9WYE4/kIbvxZ6T9Ka87CosP+cz2A2iJ/jvZNQ==
|
||||
ember-modal-dialog@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-modal-dialog/-/ember-modal-dialog-4.0.1.tgz#093e0b5a32e2824b573bec9f0c935b4f990dbaff"
|
||||
integrity sha512-6RQjQClvufYB7Y/sOS90HF3tfmLe7LLDM3fgl8BsAEu9xSJTHZTz+xpBY467hxIkl2vAXH6rNLFCf7W/Rm9Jcw==
|
||||
dependencies:
|
||||
"@embroider/macros" "^1.0.0"
|
||||
"@embroider/util" "^1.0.0"
|
||||
|
|
Loading…
Reference in New Issue