diff --git a/ui/app/components/clients/attribution.js b/ui/app/components/clients/attribution.js index 2c29e12177..75bcde84dd 100644 --- a/ui/app/components/clients/attribution.js +++ b/ui/app/components/clients/attribution.js @@ -46,12 +46,12 @@ export default class Attribution extends Component { // truncate data before sending to chart component // move truncating to serializer when we add separate request to fetch and export ALL namespace data get barChartTotalClients() { - return this.args.totalClientsData.slice(0, 10); + return this.args.totalClientsData?.slice(0, 10); } get topClientCounts() { // get top namespace or auth method - return this.args.totalClientsData[0]; + return this.args.totalClientsData ? this.args.totalClientsData[0] : null; } get attributionBreakdown() { @@ -65,8 +65,8 @@ export default class Attribution extends Component { return { description: 'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.', - newCopy: `The new clients in the namespace for this ${dateText}. - This aids in understanding which namespaces create and use new clients + newCopy: `The new clients in the namespace for this ${dateText}. + This aids in understanding which namespaces create and use new clients ${dateText === 'date range' ? ' over time.' : '.'}`, totalCopy: `The total clients in the namespace for this ${dateText}. This number is useful for identifying overall usage volume.`, }; @@ -74,7 +74,7 @@ export default class Attribution extends Component { return { description: 'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.', - newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients + newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients ${dateText === 'date range' ? ' over time.' : '.'}`, totalCopy: `The total clients used by the auth method for this ${dateText}. This number is useful for identifying overall usage volume. `, }; diff --git a/ui/app/components/clients/config.js b/ui/app/components/clients/config.js index dca86dfbc5..6b74191447 100644 --- a/ui/app/components/clients/config.js +++ b/ui/app/components/clients/config.js @@ -57,7 +57,7 @@ export default class ConfigComponent extends Component { this.error = err.message; return; } - this.router.transitionTo('vault.cluster.clients.index'); + this.router.transitionTo('vault.cluster.clients.config'); }).drop()) save; diff --git a/ui/app/components/clients/current.js b/ui/app/components/clients/current.js index 98b558e2cb..090649ff3e 100644 --- a/ui/app/components/clients/current.js +++ b/ui/app/components/clients/current.js @@ -7,12 +7,17 @@ export default class Current extends Component { { key: 'entity_clients', label: 'entity clients' }, { key: 'non_entity_clients', label: 'non-entity clients' }, ]; - @tracked namespaceArray = this.args.model.monthly?.byNamespace.map((namespace) => { + @tracked selectedNamespace = null; + @tracked namespaceArray = this.byNamespaceCurrent.map((namespace) => { return { name: namespace['label'], id: namespace['label'] }; }); - @tracked selectedNamespace = null; - @tracked firstUpgradeVersion = this.args.model.versionHistory[0].id; // return 1.9.0 or earliest upgrade post 1.9.0 - @tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled; // returns RFC3339 timestamp + @tracked firstUpgradeVersion = this.args.model.versionHistory[0].id || null; // return 1.9.0 or earliest upgrade post 1.9.0 + @tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled || null; // returns RFC3339 timestamp + + // API client count data by namespace for current/partial month + get byNamespaceCurrent() { + return this.args.model.monthly?.byNamespace || []; + } get countsIncludeOlderData() { let firstUpgrade = this.args.model.versionHistory[0]; @@ -24,15 +29,6 @@ export default class Current extends Component { return isAfter(versionDate, startOfMonth(new Date())) ? versionDate : false; } - get licenseStartDate() { - return this.args.licenseStartDate || null; - } - - // API client count data by namespace for current/partial month - get byNamespaceCurrent() { - return this.args.model.monthly?.byNamespace || null; - } - // top level TOTAL client counts for current/partial month get totalUsageCounts() { return this.selectedNamespace diff --git a/ui/app/components/clients/dashboard.js b/ui/app/components/clients/dashboard.js deleted file mode 100644 index bbfbaad5e4..0000000000 --- a/ui/app/components/clients/dashboard.js +++ /dev/null @@ -1,209 +0,0 @@ -import Component from '@glimmer/component'; -import { action } from '@ember/object'; -import { inject as service } from '@ember/service'; -import { tracked } from '@glimmer/tracking'; -import { isSameMonth, isAfter } from 'date-fns'; - -export default class Dashboard extends Component { - arrayOfMonths = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - ]; - maxNamespaces = 10; - chartLegend = [ - { key: 'entity_clients', label: 'entity clients' }, - { key: 'non_entity_clients', label: 'non-entity clients' }, - ]; - - // needed for startTime modal picker - months = Array.from({ length: 12 }, (item, i) => { - return new Date(0, i).toLocaleString('en-US', { month: 'long' }); - }); - years = Array.from({ length: 5 }, (item, i) => { - return new Date().getFullYear() - i; - }); - - @service store; - - @tracked barChartSelection = false; - @tracked isEditStartMonthOpen = false; - @tracked responseRangeDiffMessage = null; - @tracked startTimeRequested = null; - @tracked startTimeFromResponse = this.args.model.startTimeFromLicense; // ex: ['2021', 3] is April 2021 (0 indexed) - @tracked endTimeFromResponse = this.args.model.endTimeFromResponse; - @tracked startMonth = null; - @tracked startYear = null; - @tracked selectedNamespace = null; - @tracked noActivityDate = ''; - @tracked namespaceArray = this.args.model.activity?.byNamespace.map((namespace) => { - return { name: namespace['label'], id: namespace['label'] }; - }); - @tracked firstUpgradeVersion = this.args.model.versionHistory[0].id; // return 1.9.0 or earliest upgrade post 1.9.0 - @tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled; // returns RFC3339 timestamp - - get startTimeDisplay() { - if (!this.startTimeFromResponse) { - // otherwise will return date of new Date(null) - return null; - } - let month = this.startTimeFromResponse[1]; - let year = this.startTimeFromResponse[0]; - return `${this.arrayOfMonths[month]} ${year}`; - } - - get endTimeDisplay() { - if (!this.endTimeFromResponse) { - // otherwise will return date of new Date(null) - return null; - } - let month = this.endTimeFromResponse[1]; - let year = this.endTimeFromResponse[0]; - return `${this.arrayOfMonths[month]} ${year}`; - } - - get isDateRange() { - return !isSameMonth( - new Date(this.args.model.activity.startTime), - new Date(this.args.model.activity.endTime) - ); - } - - // API client count data by namespace for date range - get byNamespaceActivity() { - return this.args.model.activity?.byNamespace || null; - } - - // top level TOTAL client counts for given date range - get totalUsageCounts() { - return this.selectedNamespace - ? this.filterByNamespace(this.selectedNamespace) - : this.args.model.activity?.total; - } - - // total client data for horizontal bar chart in attribution component - get totalClientsData() { - if (this.selectedNamespace) { - let filteredNamespace = this.filterByNamespace(this.selectedNamespace); - return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null; - } else { - return this.byNamespaceActivity; - } - } - - get responseTimestamp() { - return this.args.model.activity?.responseTimestamp; - } - - get countsIncludeOlderData() { - let firstUpgrade = this.args.model.versionHistory[0]; - if (!firstUpgrade) { - return false; - } - let versionDate = new Date(firstUpgrade.timestampInstalled); - // compare against this startTimeFromResponse to show message or not. - return isAfter(versionDate, new Date(this.startTimeFromResponse)) ? versionDate : false; - } - // HELPERS - areArraysTheSame(a1, a2) { - return ( - a1 === a2 || - (a1 !== null && - a2 !== null && - a1.length === a2.length && - a1 - .map(function (val, idx) { - return val === a2[idx]; - }) - .reduce(function (prev, cur) { - return prev && cur; - }, true)) - ); - } - - // ACTIONS - @action - async handleClientActivityQuery(month, year, dateType) { - if (dateType === 'cancel') { - return; - } - // clicked "Current Billing period" in the calendar widget - if (dateType === 'reset') { - this.startTimeRequested = this.args.model.startTimeFromLicense; - this.endTimeRequested = null; - } - // clicked "Edit" Billing start month in Dashboard which opens a modal. - if (dateType === 'startTime') { - let monthIndex = this.arrayOfMonths.indexOf(month); - this.startTimeRequested = [year.toString(), monthIndex]; // ['2021', 0] (e.g. January 2021) // TODO CHANGE TO ARRAY - this.endTimeRequested = null; - } - // clicked "Custom End Month" from the calendar-widget - if (dateType === 'endTime') { - // use the currently selected startTime for your startTimeRequested. - this.startTimeRequested = this.startTimeFromResponse; - this.endTimeRequested = [year.toString(), month]; // endTime comes in as a number/index whereas startTime comes in as a month name. Hence the difference between monthIndex and month. - } - - try { - let response = await this.store.queryRecord('clients/activity', { - start_time: this.startTimeRequested, - end_time: this.endTimeRequested, - }); - if (response.id === 'no-data') { - // empty response is the only time we want to update the displayed date with the requested time - this.startTimeFromResponse = this.startTimeRequested; - this.noActivityDate = this.startTimeDisplay; - } else { - // note: this.startTimeDisplay (getter) is updated by this.startTimeFromResponse - this.startTimeFromResponse = response.formattedStartTime; - this.endTimeFromResponse = response.formattedEndTime; - } - // compare if the response and what you requested are the same. If they are not throw a warning. - // this only gets triggered if the data was returned, which does not happen if the user selects a startTime after for which we have data. That's an adapter error and is captured differently. - if (!this.areArraysTheSame(this.startTimeFromResponse, this.startTimeRequested)) { - this.responseRangeDiffMessage = `You requested data from ${month} ${year}. We only have data from ${this.startTimeDisplay}, and that is what is being shown here.`; - } else { - this.responseRangeDiffMessage = null; - } - return response; - } catch (e) { - // ARG TODO handle error - } - } - - @action - handleCurrentBillingPeriod() { - this.handleClientActivityQuery(0, 0, 'reset'); - } - - @action - selectNamespace([value]) { - // value comes in as [namespace0] - this.selectedNamespace = value; - } - - @action - selectStartMonth(month) { - this.startMonth = month; - } - - @action - selectStartYear(year) { - this.startYear = year; - } - - // HELPERS - filterByNamespace(namespace) { - return this.byNamespaceActivity.find((ns) => ns.label === namespace); - } -} diff --git a/ui/app/components/clients/history-old.js b/ui/app/components/clients/history-old.js new file mode 100644 index 0000000000..0243574495 --- /dev/null +++ b/ui/app/components/clients/history-old.js @@ -0,0 +1,124 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { format } from 'date-fns'; + +export default class HistoryComponent extends Component { + max_namespaces = 10; + + @tracked selectedNamespace = null; + + @tracked barChartSelection = false; + + // Determine if we have client count data based on the current tab + get hasClientData() { + if (this.args.tab === 'current') { + // Show the current numbers as long as config is on + return this.args.model.config?.enabled !== 'Off'; + } + return this.args.model.activity && this.args.model.activity.total; + } + + // Show namespace graph only if we have more than 1 + get showGraphs() { + return ( + this.args.model.activity && + this.args.model.activity.byNamespace && + this.args.model.activity.byNamespace.length > 1 + ); + } + + // Construct the namespace model for the search select component + get searchDataset() { + if (!this.args.model.activity || !this.args.model.activity.byNamespace) { + return null; + } + let dataList = this.args.model.activity.byNamespace; + return dataList.map((d) => { + return { + name: d['namespace_id'], + id: d['namespace_path'] === '' ? 'root' : d['namespace_path'], + }; + }); + } + + // Construct the namespace model for the bar chart component + get barChartDataset() { + if (!this.args.model.activity || !this.args.model.activity.byNamespace) { + return null; + } + let dataset = this.args.model.activity.byNamespace.slice(0, this.max_namespaces); + return dataset.map((d) => { + return { + label: d['namespace_path'] === '' ? 'root' : d['namespace_path'], + non_entity_tokens: d['counts']['non_entity_tokens'], + distinct_entities: d['counts']['distinct_entities'], + total: d['counts']['clients'], + }; + }); + } + + // Create namespaces data for csv format + get getCsvData() { + if (!this.args.model.activity || !this.args.model.activity.byNamespace) { + return null; + } + let results = '', + namespaces = this.args.model.activity.byNamespace, + fields = ['Namespace path', 'Active clients', 'Unique entities', 'Non-entity tokens']; + + results = fields.join(',') + '\n'; + + namespaces.forEach(function (item) { + let path = item.namespace_path !== '' ? item.namespace_path : 'root', + total = item.counts.clients, + unique = item.counts.distinct_entities, + non_entity = item.counts.non_entity_tokens; + + results += path + ',' + total + ',' + unique + ',' + non_entity + '\n'; + }); + return results; + } + + // Return csv filename with start and end dates + get getCsvFileName() { + let defaultFileName = `clients-by-namespace`, + startDate = + this.args.model.queryStart || `${format(new Date(this.args.model.activity.startTime), 'MM-yyyy')}`, + endDate = + this.args.model.queryEnd || `${format(new Date(this.args.model.activity.endTime), 'MM-yyyy')}`; + if (startDate && endDate) { + defaultFileName += `-${startDate}-${endDate}`; + } + return defaultFileName; + } + + // Get the namespace by matching the path from the namespace list + getNamespace(path) { + return this.args.model.activity.byNamespace.find((ns) => { + if (path === 'root') { + return ns.namespace_path === ''; + } + return ns.namespace_path === path; + }); + } + + @action + selectNamespace(value) { + // In case of search select component, value returned is an array + if (Array.isArray(value)) { + this.selectedNamespace = this.getNamespace(value[0]); + this.barChartSelection = false; + } else if (typeof value === 'object') { + // While D3 bar selection returns an object + this.selectedNamespace = this.getNamespace(value.label); + this.barChartSelection = true; + } + } + + @action + resetData() { + this.barChartSelection = false; + this.selectedNamespace = null; + } +} diff --git a/ui/app/components/clients/history.js b/ui/app/components/clients/history.js index 0243574495..478a3dc4c6 100644 --- a/ui/app/components/clients/history.js +++ b/ui/app/components/clients/history.js @@ -1,124 +1,212 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; -import { format } from 'date-fns'; +import { isSameMonth, isAfter } from 'date-fns'; -export default class HistoryComponent extends Component { - max_namespaces = 10; +export default class History extends Component { + // TODO CMB alphabetize and delete unused vars (particularly @tracked) + arrayOfMonths = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; - @tracked selectedNamespace = null; + chartLegend = [ + { key: 'entity_clients', label: 'entity clients' }, + { key: 'non_entity_clients', label: 'non-entity clients' }, + ]; + // needed for startTime modal picker + months = Array.from({ length: 12 }, (item, i) => { + return new Date(0, i).toLocaleString('en-US', { month: 'long' }); + }); + years = Array.from({ length: 5 }, (item, i) => { + return new Date().getFullYear() - i; + }); + + @service store; + + @tracked queriedActivityResponse = null; @tracked barChartSelection = false; + @tracked isEditStartMonthOpen = false; + @tracked responseRangeDiffMessage = null; + @tracked startTimeRequested = null; + @tracked startTimeFromResponse = this.args.model.startTimeFromLicense; // ex: ['2021', 3] is April 2021 (0 indexed) + @tracked endTimeFromResponse = this.args.model.endTimeFromResponse; + @tracked startMonth = null; + @tracked startYear = null; + @tracked selectedNamespace = null; + @tracked noActivityDate = ''; + @tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => { + return { name: namespace['label'], id: namespace['label'] }; + }); + @tracked firstUpgradeVersion = this.args.model.versionHistory[0].id || null; // return 1.9.0 or earliest upgrade post 1.9.0 + @tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled || null; // returns RFC3339 timestamp - // Determine if we have client count data based on the current tab - get hasClientData() { - if (this.args.tab === 'current') { - // Show the current numbers as long as config is on - return this.args.model.config?.enabled !== 'Off'; - } - return this.args.model.activity && this.args.model.activity.total; + // on init API response uses license start_date, getter updates when user queries dates + get getActivityResponse() { + return this.queriedActivityResponse || this.args.model.activity; } - // Show namespace graph only if we have more than 1 - get showGraphs() { - return ( - this.args.model.activity && - this.args.model.activity.byNamespace && - this.args.model.activity.byNamespace.length > 1 + get startTimeDisplay() { + if (!this.startTimeFromResponse) { + // otherwise will return date of new Date(null) + return null; + } + let month = this.startTimeFromResponse[1]; + let year = this.startTimeFromResponse[0]; + return `${this.arrayOfMonths[month]} ${year}`; + } + + get endTimeDisplay() { + if (!this.endTimeFromResponse) { + // otherwise will return date of new Date(null) + return null; + } + let month = this.endTimeFromResponse[1]; + let year = this.endTimeFromResponse[0]; + return `${this.arrayOfMonths[month]} ${year}`; + } + + get isDateRange() { + return !isSameMonth( + new Date(this.getActivityResponse.startTime), + new Date(this.getActivityResponse.endTime) ); } - // Construct the namespace model for the search select component - get searchDataset() { - if (!this.args.model.activity || !this.args.model.activity.byNamespace) { - return null; - } - let dataList = this.args.model.activity.byNamespace; - return dataList.map((d) => { - return { - name: d['namespace_id'], - id: d['namespace_path'] === '' ? 'root' : d['namespace_path'], - }; - }); + // top level TOTAL client counts for given date range + get totalUsageCounts() { + return this.selectedNamespace + ? this.filterByNamespace(this.selectedNamespace) + : this.getActivityResponse.total; } - // Construct the namespace model for the bar chart component - get barChartDataset() { - if (!this.args.model.activity || !this.args.model.activity.byNamespace) { - return null; + // total client data for horizontal bar chart in attribution component + get totalClientsData() { + if (this.selectedNamespace) { + let filteredNamespace = this.filterByNamespace(this.selectedNamespace); + return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null; + } else { + return this.getActivityResponse?.byNamespace; } - let dataset = this.args.model.activity.byNamespace.slice(0, this.max_namespaces); - return dataset.map((d) => { - return { - label: d['namespace_path'] === '' ? 'root' : d['namespace_path'], - non_entity_tokens: d['counts']['non_entity_tokens'], - distinct_entities: d['counts']['distinct_entities'], - total: d['counts']['clients'], - }; - }); } - // Create namespaces data for csv format - get getCsvData() { - if (!this.args.model.activity || !this.args.model.activity.byNamespace) { - return null; - } - let results = '', - namespaces = this.args.model.activity.byNamespace, - fields = ['Namespace path', 'Active clients', 'Unique entities', 'Non-entity tokens']; - - results = fields.join(',') + '\n'; - - namespaces.forEach(function (item) { - let path = item.namespace_path !== '' ? item.namespace_path : 'root', - total = item.counts.clients, - unique = item.counts.distinct_entities, - non_entity = item.counts.non_entity_tokens; - - results += path + ',' + total + ',' + unique + ',' + non_entity + '\n'; - }); - return results; + get responseTimestamp() { + return this.getActivityResponse.responseTimestamp; } - // Return csv filename with start and end dates - get getCsvFileName() { - let defaultFileName = `clients-by-namespace`, - startDate = - this.args.model.queryStart || `${format(new Date(this.args.model.activity.startTime), 'MM-yyyy')}`, - endDate = - this.args.model.queryEnd || `${format(new Date(this.args.model.activity.endTime), 'MM-yyyy')}`; - if (startDate && endDate) { - defaultFileName += `-${startDate}-${endDate}`; + get countsIncludeOlderData() { + let firstUpgrade = this.args.model.versionHistory[0]; + if (!firstUpgrade) { + return false; } - return defaultFileName; + let versionDate = new Date(firstUpgrade.timestampInstalled); + // compare against this startTimeFromResponse to show message or not. + return isAfter(versionDate, new Date(this.startTimeFromResponse)) ? versionDate : false; } - // Get the namespace by matching the path from the namespace list - getNamespace(path) { - return this.args.model.activity.byNamespace.find((ns) => { - if (path === 'root') { - return ns.namespace_path === ''; + // HELPERS + areArraysTheSame(a1, a2) { + return ( + a1 === a2 || + (a1 !== null && + a2 !== null && + a1.length === a2.length && + a1 + .map(function (val, idx) { + return val === a2[idx]; + }) + .reduce(function (prev, cur) { + return prev && cur; + }, true)) + ); + } + + // ACTIONS + @action + async handleClientActivityQuery(month, year, dateType) { + if (dateType === 'cancel') { + return; + } + // clicked "Current Billing period" in the calendar widget + if (dateType === 'reset') { + this.startTimeRequested = this.args.model.startTimeFromLicense; + this.endTimeRequested = null; + } + // clicked "Edit" Billing start month in Dashboard which opens a modal. + if (dateType === 'startTime') { + let monthIndex = this.arrayOfMonths.indexOf(month); + this.startTimeRequested = [year.toString(), monthIndex]; // ['2021', 0] (e.g. January 2021) // TODO CHANGE TO ARRAY + this.endTimeRequested = null; + } + // clicked "Custom End Month" from the calendar-widget + if (dateType === 'endTime') { + // use the currently selected startTime for your startTimeRequested. + this.startTimeRequested = this.startTimeFromResponse; + this.endTimeRequested = [year.toString(), month]; // endTime comes in as a number/index whereas startTime comes in as a month name. Hence the difference between monthIndex and month. + } + + try { + let response = await this.store.queryRecord('clients/activity', { + start_time: this.startTimeRequested, + end_time: this.endTimeRequested, + }); + if (response.id === 'no-data') { + // empty response is the only time we want to update the displayed date with the requested time + this.startTimeFromResponse = this.startTimeRequested; + this.noActivityDate = this.startTimeDisplay; + } else { + // note: this.startTimeDisplay (getter) is updated by this.startTimeFromResponse + this.startTimeFromResponse = response.formattedStartTime; + this.endTimeFromResponse = response.formattedEndTime; } - return ns.namespace_path === path; - }); - } - - @action - selectNamespace(value) { - // In case of search select component, value returned is an array - if (Array.isArray(value)) { - this.selectedNamespace = this.getNamespace(value[0]); - this.barChartSelection = false; - } else if (typeof value === 'object') { - // While D3 bar selection returns an object - this.selectedNamespace = this.getNamespace(value.label); - this.barChartSelection = true; + // compare if the response and what you requested are the same. If they are not throw a warning. + // this only gets triggered if the data was returned, which does not happen if the user selects a startTime after for which we have data. That's an adapter error and is captured differently. + if (!this.areArraysTheSame(this.startTimeFromResponse, this.startTimeRequested)) { + this.responseRangeDiffMessage = `You requested data from ${month} ${year}. We only have data from ${this.startTimeDisplay}, and that is what is being shown here.`; + } else { + this.responseRangeDiffMessage = null; + } + this.queriedActivityResponse = response; + } catch (e) { + // ARG TODO handle error } } @action - resetData() { - this.barChartSelection = false; - this.selectedNamespace = null; + handleCurrentBillingPeriod() { + this.handleClientActivityQuery(0, 0, 'reset'); + } + + @action + selectNamespace([value]) { + // value comes in as [namespace0] + this.selectedNamespace = value; + } + + @action + selectStartMonth(month) { + this.startMonth = month; + } + + @action + selectStartYear(year) { + this.startYear = year; + } + + // HELPERS + filterByNamespace(namespace) { + return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace); } } diff --git a/ui/app/controllers/vault/cluster/clients/config.js b/ui/app/controllers/vault/cluster/clients/config.js new file mode 100644 index 0000000000..2d566a95a3 --- /dev/null +++ b/ui/app/controllers/vault/cluster/clients/config.js @@ -0,0 +1,3 @@ +import Controller from '@ember/controller'; + +export default class ConfigController extends Controller {} diff --git a/ui/app/controllers/vault/cluster/clients/history.js b/ui/app/controllers/vault/cluster/clients/history.js new file mode 100644 index 0000000000..cfc2bfe9ee --- /dev/null +++ b/ui/app/controllers/vault/cluster/clients/history.js @@ -0,0 +1,3 @@ +import Controller from '@ember/controller'; + +export default class HistoryController extends Controller {} diff --git a/ui/app/controllers/vault/cluster/clients/index.js b/ui/app/controllers/vault/cluster/clients/index.js index 4b69910b55..7892c93569 100644 --- a/ui/app/controllers/vault/cluster/clients/index.js +++ b/ui/app/controllers/vault/cluster/clients/index.js @@ -1,8 +1,3 @@ import Controller from '@ember/controller'; -export default class ClientsController extends Controller { - queryParams = ['tab', 'start', 'end']; // ARG TODO remove - tab = null; - start = null; - end = null; -} +export default class ClientsController extends Controller {} diff --git a/ui/app/router.js b/ui/app/router.js index be9bfe8570..d128850146 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -18,7 +18,8 @@ Router.map(function () { this.mount('open-api-explorer', { path: '/api-explorer' }); this.route('license'); this.route('clients', function () { - this.route('index', { path: '/' }); + this.route('history'); + this.route('config'); this.route('edit'); }); this.route('storage', { path: '/storage/raft' }); diff --git a/ui/app/routes/vault/cluster/clients/config.js b/ui/app/routes/vault/cluster/clients/config.js new file mode 100644 index 0000000000..352789b802 --- /dev/null +++ b/ui/app/routes/vault/cluster/clients/config.js @@ -0,0 +1,19 @@ +import Route from '@ember/routing/route'; +import { action } from '@ember/object'; + +export default class ConfigRoute extends Route { + model() { + return this.store.queryRecord('clients/config', {}); + } + @action + async loading(transition) { + // eslint-disable-next-line ember/no-controller-access-in-routes + let controller = this.controllerFor('vault.cluster.clients.config'); + if (controller) { + controller.currentlyLoading = true; + transition.promise.finally(function () { + controller.currentlyLoading = false; + }); + } + } +} diff --git a/ui/app/routes/vault/cluster/clients/history.js b/ui/app/routes/vault/cluster/clients/history.js new file mode 100644 index 0000000000..ed4d9ef108 --- /dev/null +++ b/ui/app/routes/vault/cluster/clients/history.js @@ -0,0 +1,78 @@ +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; +import { action } from '@ember/object'; + +export default class HistoryRoute extends Route { + async getActivity(start_time) { + try { + return this.store.queryRecord('clients/activity', { start_time }); + } catch (e) { + // ARG TODO handle + return e; + } + } + + async getLicense() { + try { + return this.store.queryRecord('license', {}); + } catch (e) { + // ARG TODO handle + return e; + } + } + + async getVersionHistory() { + try { + let arrayOfModels = []; + let response = await this.store.findAll('clients/version-history'); // returns a class with nested models + response.forEach((model) => { + arrayOfModels.push({ + id: model.id, + perviousVersion: model.previousVersion, + timestampInstalled: model.timestampInstalled, + }); + }); + return arrayOfModels; + } catch (e) { + console.debug(e); + return []; + } + } + + parseRFC3339(timestamp) { + // convert '2021-03-21T00:00:00Z' --> ['2021', 2] (e.g. 2021 March, month is zero indexed) + return timestamp + ? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1] + : null; + } + + async model() { + let config = await this.store.queryRecord('clients/config', {}).catch((e) => { + console.debug(e); + // swallowing error so activity can show if no config permissions + return {}; + }); + let license = await this.getLicense(); + let activity = await this.getActivity(license.startTime); + + return RSVP.hash({ + config, + activity, + startTimeFromLicense: this.parseRFC3339(license.startTime), + endTimeFromResponse: activity ? this.parseRFC3339(activity.endTime) : null, + versionHistory: this.getVersionHistory(), + }); + } + + @action + async loading(transition) { + // eslint-disable-next-line ember/no-controller-access-in-routes + let controller = this.controllerFor('vault.cluster.clients.history'); + if (controller) { + controller.currentlyLoading = true; + transition.promise.finally(function () { + controller.currentlyLoading = false; + }); + } + } +} diff --git a/ui/app/routes/vault/cluster/clients/index.js b/ui/app/routes/vault/cluster/clients/index.js index 0e4e3ca694..38fa31d94e 100644 --- a/ui/app/routes/vault/cluster/clients/index.js +++ b/ui/app/routes/vault/cluster/clients/index.js @@ -1,28 +1,8 @@ import Route from '@ember/routing/route'; -import ClusterRoute from 'vault/mixins/cluster-route'; -import { hash } from 'rsvp'; - -export default Route.extend(ClusterRoute, { - queryParams: { - tab: { - refreshModel: true, - }, - start: { - refreshModel: true, - }, - end: { - refreshModel: true, - }, - }, - - async getActivity(start_time) { - try { - return this.store.queryRecord('clients/activity', { start_time }); - } catch (e) { - return e; - } - }, +import RSVP from 'rsvp'; +import { action } from '@ember/object'; +export default class ClientsRoute extends Route { async getVersionHistory() { try { let arrayOfModels = []; @@ -36,66 +16,34 @@ export default Route.extend(ClusterRoute, { }); return arrayOfModels; } catch (e) { - return null; + console.debug(e); + return []; } - }, - - async getLicense() { - try { - return await this.store.queryRecord('license', {}); - } catch (e) { - return e; - } - }, - - async getMonthly() { - try { - return await this.store.queryRecord('clients/monthly', {}); - } catch (e) { - return e; - } - }, - - rfc33395ToMonthYear(timestamp) { - // return ['2021', 2] (e.g. 2021 March, make 0-indexed) - return timestamp - ? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1] - : null; - }, + } async model() { - let config = this.store.queryRecord('clients/config', {}).catch((e) => { + let config = await this.store.queryRecord('clients/config', {}).catch((e) => { console.debug(e); // swallowing error so activity can show if no config permissions return {}; }); - let license = await this.getLicense(); // get default start_time - let activity = await this.getActivity(license.startTime); // returns client counts using license start_time. - let monthly = await this.getMonthly(); // returns the partial month endpoint - let versionHistory = await this.getVersionHistory(); - let endTimeFromResponse = activity ? this.rfc33395ToMonthYear(activity.endTime) : null; - let startTimeFromLicense = this.rfc33395ToMonthYear(license.startTime); - - return hash({ - // ARG TODO will remove "hash" once remove "activity," which currently relies on it. - activity, - monthly, + return RSVP.hash({ config, - endTimeFromResponse, - startTimeFromLicense, - versionHistory, + monthly: await this.store.queryRecord('clients/monthly', {}), + versionHistory: this.getVersionHistory(), }); - }, + } - actions: { - loading(transition) { - // eslint-disable-next-line ember/no-controller-access-in-routes - let controller = this.controllerFor('vault.cluster.clients.index'); - controller.set('currentlyLoading', true); + @action + async loading(transition) { + // eslint-disable-next-line ember/no-controller-access-in-routes + let controller = this.controllerFor('vault.cluster.clients.index'); + if (controller) { + controller.currentlyLoading = true; transition.promise.finally(function () { - controller.set('currentlyLoading', false); + controller.currentlyLoading = false; }); - }, - }, -}); + } + } +} diff --git a/ui/app/serializers/clients/activity.js b/ui/app/serializers/clients/activity.js index d3b71f2657..ec4b105c97 100644 --- a/ui/app/serializers/clients/activity.js +++ b/ui/app/serializers/clients/activity.js @@ -48,8 +48,8 @@ export default class ActivitySerializer extends ApplicationSerializer { return object; } - rfc33395ToMonthYear(timestamp) { - // return ['2021', 2] (e.g. 2021 March, make 0-indexed) + parseRFC3339(timestamp) { + // convert '2021-03-21T00:00:00Z' --> ['2021', 2] (e.g. 2021 March, month is zero indexed) return timestamp ? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1] : null; @@ -65,8 +65,8 @@ export default class ActivitySerializer extends ApplicationSerializer { response_timestamp, by_namespace: this.flattenDataset(payload.data.by_namespace), total: this.homogenizeClientNaming(payload.data.total), - formatted_end_time: this.rfc33395ToMonthYear(payload.data.end_time), - formatted_start_time: this.rfc33395ToMonthYear(payload.data.start_time), + formatted_end_time: this.parseRFC3339(payload.data.end_time), + formatted_start_time: this.parseRFC3339(payload.data.start_time), }; delete payload.data.by_namespace; delete payload.data.total; @@ -81,7 +81,6 @@ payload.data.by_namespace = [ { namespace_id: '5SWT8', namespace_path: 'namespacelonglonglong4/', - _comment1: 'client counts are nested within own object', counts: { entity_clients: 171, non_entity_clients: 20, @@ -103,7 +102,6 @@ payload.data.by_namespace = [ transformedPayload.by_namespace = [ { label: 'namespacelonglonglong4/', - _comment2: 'remove nested object', entity_clients: 171, non_entity_clients: 20, clients: 191, diff --git a/ui/app/serializers/clients/monthly.js b/ui/app/serializers/clients/monthly.js index e5fe2b4c09..667277c58c 100644 --- a/ui/app/serializers/clients/monthly.js +++ b/ui/app/serializers/clients/monthly.js @@ -49,6 +49,9 @@ export default class MonthlySerializer extends ApplicationSerializer { } normalizeResponse(store, primaryModelClass, payload, id, requestType) { + if (payload.id === 'no-data') { + return super.normalizeResponse(store, primaryModelClass, payload, id, requestType); + } let response_timestamp = formatISO(new Date()); let transformedPayload = { ...payload, diff --git a/ui/app/styles/components/tabs.scss b/ui/app/styles/components/tabs.scss index fc7851fd35..d0cbf79528 100644 --- a/ui/app/styles/components/tabs.scss +++ b/ui/app/styles/components/tabs.scss @@ -44,3 +44,29 @@ outline: none; } } + +.nav-tab-link { + color: $grey; + font-weight: $font-weight-semibold; + text-decoration: none; + padding: $size-6 $size-8 $size-8; + border-bottom: 2px solid transparent; + transition: background-color $speed, border-color $speed; + + &:hover, + &:active { + border-color: $grey-light; + } + + &:hover { + background-color: $ui-gray-050; + } + + &:focus { + box-shadow: none; + } + &.is-active { + border-color: $blue !important; + color: $blue !important; + } +} diff --git a/ui/app/templates/components/clients/config.hbs b/ui/app/templates/components/clients/config.hbs index 90eefb7e6a..a79cfb3337 100644 --- a/ui/app/templates/components/clients/config.hbs +++ b/ui/app/templates/components/clients/config.hbs @@ -1,47 +1,16 @@ -{{#if (eq @mode "edit")}} -
-
- - {{#each @model.configAttrs as |attr|}} - {{#if (and (eq attr.type "string") (eq attr.options.editType "boolean"))}} - - {{#if attr.options.helpText}} -

- {{attr.options.helpText}} - {{#if attr.options.docLink}} - - See our documentation - - for help. - {{/if}} -

- {{/if}} -
- - -
- {{else if (eq attr.type "number")}} -
- - {{#if attr.options.subText}} +{{#if @isLoading}} + +{{else}} + {{#if (eq @mode "edit")}} + +
+ + {{#each @model.configAttrs as |attr|}} + {{#if (and (eq attr.type "string") (eq attr.options.editType "boolean"))}} + + {{#if attr.options.helpText}}

- {{attr.options.subText}} + {{attr.options.helpText}} {{#if attr.options.docLink}} See our documentation @@ -50,82 +19,117 @@ {{/if}}

{{/if}} -
+
+
-
+ {{else if (eq attr.type "number")}} +
+ {{/if}} + {{/each}} +
+
+
+ + + Cancel + +
+
+ + + +
+ + +
+
+ {{else}} +
+ {{#each this.infoRows as |item|}} + {{/each}}
-
-
- - - Cancel - -
-
- - - -
- - -
-
-{{else}} -
- {{#each this.infoRows as |item|}} - - {{/each}} -
+ {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/clients/current.hbs b/ui/app/templates/components/clients/current.hbs index 2806471a7b..f7d6221ed5 100644 --- a/ui/app/templates/components/clients/current.hbs +++ b/ui/app/templates/components/clients/current.hbs @@ -8,31 +8,29 @@ @message="Tracking is disabled and data is not being collected. To turn it on edit the configuration." > {{#if @model.config.configPath.canUpdate}} - + Go to configuration {{/if}} {{else}} - {{#if this.totalClientsData}} -
- FILTERS - - - - - -
- {{/if}} +
+ FILTERS + + + + + +
{{#if this.countsIncludeOlderData}} {{concat "You upgraded to Vault " this.firstUpgradeVersion " on " (date-format this.upgradeDate "MMMM d, yyyy.")}} @@ -46,20 +44,24 @@ {{#if @isLoading}} {{else}} - - {{#if this.totalClientsData}} - + {{#if this.totalClientsData}} + + {{/if}} + {{else}} + {{/if}} {{/if}} {{/if}} diff --git a/ui/app/templates/components/clients/dashboard.hbs b/ui/app/templates/components/clients/dashboard.hbs index 2241a0ba55..89183734a2 100644 --- a/ui/app/templates/components/clients/dashboard.hbs +++ b/ui/app/templates/components/clients/dashboard.hbs @@ -1,218 +1,26 @@ -
-

- 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 - here. -

-

- Billing start month -

-
-

{{this.startTimeDisplay}}

- -
-

- This date comes from your license, and defines when client counting starts. Without this starting point, the data shown - is not reliable. -

- {{#if (eq @model.config.queriesAvailable false)}} - {{#if (eq @model.config.enabled "On")}} - - {{else}} - - {{#if @model.config.configPath.canUpdate}} -

- - Go to configuration - -

- {{/if}} -
- {{/if}} - {{else}} - {{#if (eq @model.config.enabled "Off")}} - - Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need to - - edit the configuration - - to enable tracking again. - - {{/if}} - {{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}} - {{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}} -
- FILTERS - - - - {{#if this.namespaceArray}} - - {{/if}} - - -
- {{#if this.countsIncludeOlderData}} - -
    - {{#if this.responseRangeDiffMessage}} -
  • {{this.responseRangeDiffMessage}}
  • - {{/if}} -
  • - {{concat - "You upgraded to Vault " - this.firstUpgradeVersion - " on " - (date-format this.upgradeDate "MMMM d, yyyy.") - }} - How we count clients changed in 1.9, so please keep in mind when looking at the data below. Furthermore, - namespace attribution is available only for 1.9 data. - - Learn more here. - -
  • -
-
- {{/if}} - {{#if @isLoading}} - - {{else}} - - {{#if this.totalClientsData}} - - {{/if}} - {{/if}} - {{else}} - - {{/if}} - {{/if}} +{{! this dashboard template displays in three routes so @model varies slightly: index, history and config }} + + +

+ Vault Client Count +

+
+
- {{! BILLING START DATE MODAL }} - - -
- - -
-
+
+
\ No newline at end of file diff --git a/ui/app/templates/components/clients/history-old.hbs b/ui/app/templates/components/clients/history-old.hbs new file mode 100644 index 0000000000..f625a276dd --- /dev/null +++ b/ui/app/templates/components/clients/history-old.hbs @@ -0,0 +1,213 @@ +{{#if (and (eq @tab "history") (eq @model.config.queriesAvailable false))}} + {{#if (eq @model.config.enabled "On")}} + + {{else}} + + {{#if @model.config.configPath.canUpdate}} +

+ + Go to configuration + +

+ {{/if}} +
+ {{/if}} +{{else}} +
+ {{#if (eq @tab "current")}} +

+ Current month +

+

+ The below data is for the current month starting from the first day. For historical data, see the monthly history + tab. +

+ {{#if (eq @model.config.enabled "Off")}} + + {{#if @model.config.configPath.canUpdate}} + + Go to configuration + + {{/if}} + + {{/if}} + {{else}} + {{#if (eq @model.config.enabled "Off")}} + + Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need + to + + edit the configuration + + to enable tracking again. + + {{/if}} +

+ Monthly history +

+

+ 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. +

+ + {{/if}} + {{#if @isLoading}} + + {{else if this.hasClientData}} +
+
+
+
+

+ Total usage +

+

+ These totals are within this namespace and all its children. +

+
+ + Learn more + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ {{#if this.showGraphs}} +
+
+ + + +
+
+
+
+ {{#if (and this.barChartSelection this.selectedNamespace)}} + +
    +
  • +
    + {{or this.selectedNamespace.namespace_path "root"}} +
    +
    + +
    +
  • +
+ {{else}} + + {{/if}} + {{#if this.selectedNamespace}} +
+
+ +
+
+
+
+ +
+
+ +
+
+ {{else}} + + {{/if}} +
+
+
+
+ {{/if}} + {{else if (eq @tab "current")}} + {{#if (eq @model.config.enabled "On")}} + + {{/if}} + {{else}} + + {{/if}} +
+{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/clients/history.hbs b/ui/app/templates/components/clients/history.hbs index 146b6a75e8..97bdfca889 100644 --- a/ui/app/templates/components/clients/history.hbs +++ b/ui/app/templates/components/clients/history.hbs @@ -1,213 +1,222 @@ -{{#if (and (eq @tab "history") (eq @model.config.queriesAvailable false))}} - {{#if (eq @model.config.enabled "On")}} - - {{else}} - - {{#if @model.config.configPath.canUpdate}} -

- - Go to configuration - -

- {{/if}} -
- {{/if}} -{{else}} -
- {{#if (eq @tab "current")}} -

- Current month -

-

- The below data is for the current month starting from the first day. For historical data, see the monthly history - tab. -

- {{#if (eq @model.config.enabled "Off")}} - - {{#if @model.config.configPath.canUpdate}} - +
+

+ 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 + here. +

+

+ Billing start month +

+
+

{{this.startTimeDisplay}}

+ +
+

+ This date comes from your license, and defines when client counting starts. Without this starting point, the data shown + is not reliable. +

+ {{#if (eq @model.config.queriesAvailable false)}} + {{#if (eq @model.config.enabled "On")}} + + {{else}} + + {{#if @model.config.configPath.canUpdate}} +

+ Go to configuration - {{/if}} - - {{/if}} - {{else}} - {{#if (eq @model.config.enabled "Off")}} - - Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need - to - - edit the configuration - - to enable tracking again. +

+ {{/if}} +
+ {{/if}} + {{else}} + {{#if (eq @model.config.enabled "Off")}} + + Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need to + + edit the configuration + + to enable tracking again. + + {{/if}} + {{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}} + {{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}} +
+ FILTERS + + + + {{#if this.namespaceArray}} + + {{/if}} + + +
+ {{#if this.countsIncludeOlderData}} + +
    + {{#if this.responseRangeDiffMessage}} +
  • {{this.responseRangeDiffMessage}}
  • + {{/if}} +
  • + {{concat + "You upgraded to Vault " + this.firstUpgradeVersion + " on " + (date-format this.upgradeDate "MMMM d, yyyy.") + }} + How we count clients changed in 1.9, so please keep in mind when looking at the data below. Furthermore, + namespace attribution is available only for 1.9 data. + + Learn more here. + +
  • +
{{/if}} -

- Monthly history -

-

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

- - {{/if}} - {{#if @isLoading}} - - {{else if this.hasClientData}} -
-
-
-
-

- Total usage -

-

- These totals are within this namespace and all its children. -

-
- - Learn more - -
-
-
-
- -
-
- -
-
- -
-
-
-
- {{#if this.showGraphs}} -
-
- - - -
-
-
-
- {{#if (and this.barChartSelection this.selectedNamespace)}} - -
    -
  • -
    - {{or this.selectedNamespace.namespace_path "root"}} -
    -
    - -
    -
  • -
- {{else}} - - {{/if}} - {{#if this.selectedNamespace}} -
-
- -
-
-
-
- -
-
- -
-
- {{else}} - - {{/if}} -
-
-
-
- {{/if}} - {{else if (eq @tab "current")}} - {{#if (eq @model.config.enabled "On")}} - + {{#if @isLoading}} + + {{else}} + {{#if this.totalUsageCounts}} + + {{#if this.totalClientsData}} + + {{/if}} + {{else}} + + {{/if}} {{/if}} {{else}} - + {{/if}} -
-{{/if}} \ No newline at end of file + {{/if}} + + {{! BILLING START DATE MODAL }} + + +
+ + +
+
+
\ No newline at end of file diff --git a/ui/app/templates/components/clients/horizontal-bar-chart.hbs b/ui/app/templates/components/clients/horizontal-bar-chart.hbs index a3f16c0f12..b317da54f8 100644 --- a/ui/app/templates/components/clients/horizontal-bar-chart.hbs +++ b/ui/app/templates/components/clients/horizontal-bar-chart.hbs @@ -1,4 +1,5 @@ +