diff --git a/changelog/12554.txt b/changelog/12554.txt
new file mode 100644
index 000000000..e3549ecdf
--- /dev/null
+++ b/changelog/12554.txt
@@ -0,0 +1,3 @@
+```release-note:feature
+ui: client count monthly view
+```
\ No newline at end of file
diff --git a/ui/app/adapters/metrics/activity.js b/ui/app/adapters/clients/activity.js
similarity index 77%
rename from ui/app/adapters/metrics/activity.js
rename to ui/app/adapters/clients/activity.js
index 09ea312b0..df54b0ea4 100644
--- a/ui/app/adapters/metrics/activity.js
+++ b/ui/app/adapters/clients/activity.js
@@ -5,7 +5,11 @@ export default Application.extend({
return 'internal/counters/activity';
},
queryRecord(store, type, query) {
- const url = this.urlForQuery(null, type);
+ let url = this.urlForQuery(null, type);
+ if (query.tab === 'current') {
+ url = `${url}/monthly`;
+ query = null;
+ }
// API accepts start and end as query params
return this.ajax(url, 'GET', { data: query }).then(resp => {
let response = resp || {};
diff --git a/ui/app/adapters/metrics/config.js b/ui/app/adapters/clients/config.js
similarity index 100%
rename from ui/app/adapters/metrics/config.js
rename to ui/app/adapters/clients/config.js
diff --git a/ui/app/components/pricing-metrics-config.js b/ui/app/components/clients/config.js
similarity index 82%
rename from ui/app/components/pricing-metrics-config.js
rename to ui/app/components/clients/config.js
index a61ef3d34..9b093ecae 100644
--- a/ui/app/components/pricing-metrics-config.js
+++ b/ui/app/components/clients/config.js
@@ -1,12 +1,12 @@
/**
- * @module PricingMetricsConfig
- * PricingMetricsConfig components are used to show and edit the pricing metrics config information.
+ * @module ClientsConfig
+ * ClientsConfig components are used to show and edit the client count config information.
*
* @example
* ```js
- *
+ *
* ```
- * @param {object} model - model is the DS metrics/config model which should be passed in
+ * @param {object} model - model is the DS clients/config model which should be passed in
* @param {string} [mode=show] - mode is either show or edit. Show results in a table with the config, show has a form.
*/
@@ -16,7 +16,7 @@ import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
-export default class PricingMetricsConfigComponent extends Component {
+export default class ConfigComponent extends Component {
@service router;
@tracked mode = 'show';
@tracked modalOpen = false;
@@ -57,7 +57,7 @@ export default class PricingMetricsConfigComponent extends Component {
this.error = err.message;
return;
}
- this.router.transitionTo('vault.cluster.metrics.config');
+ this.router.transitionTo('vault.cluster.clients.index');
}).drop())
save;
diff --git a/ui/app/components/clients/history.js b/ui/app/components/clients/history.js
new file mode 100644
index 000000000..cb4ba01fb
--- /dev/null
+++ b/ui/app/components/clients/history.js
@@ -0,0 +1,40 @@
+import Component from '@glimmer/component';
+
+export default class HistoryComponent extends Component {
+ max_namespaces = 10;
+
+ get hasClientData() {
+ if (this.args.tab === 'current') {
+ return this.args.model.activity && this.args.model.activity.clients;
+ }
+ return this.args.model.activity && this.args.model.activity.total;
+ }
+
+ get barChartDataset() {
+ if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
+ return null;
+ }
+ let dataset = this.args.model.activity.byNamespace;
+ // Filter out root data
+ dataset = dataset.filter(item => {
+ return item.namespace_id !== 'root';
+ });
+ // Show only top 10 namespaces
+ dataset = dataset.slice(0, this.max_namespaces);
+ return dataset.map(d => {
+ return {
+ label: d['namespace_path'],
+ non_entity_tokens: d['counts']['non_entity_tokens'],
+ distinct_entities: d['counts']['distinct_entities'],
+ total: d['counts']['clients'],
+ };
+ });
+ }
+
+ get showGraphs() {
+ if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
+ return null;
+ }
+ return this.args.model.activity.byNamespace.length > 1;
+ }
+}
diff --git a/ui/app/components/pricing-metrics-dates.js b/ui/app/components/pricing-metrics-dates.js
index 2189ae0f8..0cc7a3b76 100644
--- a/ui/app/components/pricing-metrics-dates.js
+++ b/ui/app/components/pricing-metrics-dates.js
@@ -18,16 +18,7 @@
import { set, computed } from '@ember/object';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
-import {
- differenceInSeconds,
- isValid,
- subMonths,
- startOfToday,
- format,
- endOfMonth,
- startOfMonth,
- isBefore,
-} from 'date-fns';
+import { subMonths, startOfToday, format, endOfMonth, startOfMonth, isBefore } from 'date-fns';
import layout from '../templates/components/pricing-metrics-dates';
import { parseDateString } from 'vault/helpers/parse-date-string';
@@ -69,35 +60,6 @@ export default Component.extend({
}
}),
- // We don't want the warning to show when inputs are being updated before query is made
- /* eslint-disable-next-line ember/require-computed-property-dependencies */
- showResultsWarning: computed('resultEnd', 'resultStart', function() {
- if (!this.queryStart || !this.queryEnd || !this.resultStart || !this.resultEnd) {
- return false;
- }
- const resultStart = new Date(this.resultStart);
- const resultEnd = new Date(this.resultEnd);
- let queryStart, queryEnd;
- try {
- queryStart = parseDateString(this.queryStart, '-');
- queryEnd = parseDateString(this.queryEnd, '-');
- } catch (e) {
- // Log error for debugging purposes
- console.debug(e);
- }
-
- if (!queryStart || !queryEnd || !isValid(resultStart) || !isValid(resultEnd)) {
- return false;
- }
- if (Math.abs(differenceInSeconds(queryStart, resultStart)) >= 86400) {
- return true;
- }
- if (Math.abs(differenceInSeconds(resultEnd, endOfMonth(queryEnd))) >= 86400) {
- return true;
- }
- return false;
- }),
-
error: computed('end', 'endDate', 'retentionMonths', 'start', 'startDate', function() {
if (!this.startDate) {
return 'Start date is invalid. Please use format MM/yyyy';
@@ -148,7 +110,7 @@ export default Component.extend({
handleQuery() {
const start = format(this.startDate, 'MM-yyyy');
const end = format(this.endDate, 'MM-yyyy');
- this.router.transitionTo('vault.cluster.metrics', {
+ this.router.transitionTo('vault.cluster.clients', {
queryParams: {
start,
end,
diff --git a/ui/app/controllers/vault/cluster/clients/index.js b/ui/app/controllers/vault/cluster/clients/index.js
new file mode 100644
index 000000000..8cb0262e7
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/clients/index.js
@@ -0,0 +1,8 @@
+import Controller from '@ember/controller';
+
+export default class ClientsController extends Controller {
+ queryParams = ['tab', 'start', 'end'];
+ tab = null;
+ start = null;
+ end = null;
+}
diff --git a/ui/app/controllers/vault/cluster/metrics/index.js b/ui/app/controllers/vault/cluster/metrics/index.js
deleted file mode 100644
index e355af5b8..000000000
--- a/ui/app/controllers/vault/cluster/metrics/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import Controller from '@ember/controller';
-
-export default Controller.extend({
- queryParams: ['start', 'end'],
-
- start: null,
- end: null,
-});
diff --git a/ui/app/models/metrics/activity.js b/ui/app/models/clients/activity.js
similarity index 55%
rename from ui/app/models/metrics/activity.js
rename to ui/app/models/clients/activity.js
index d220bf184..4cbd4fb11 100644
--- a/ui/app/models/metrics/activity.js
+++ b/ui/app/models/clients/activity.js
@@ -2,6 +2,10 @@ import Model, { attr } from '@ember-data/model';
export default Model.extend({
total: attr('object'),
+ byNamespace: attr('array'),
endTime: attr('string'),
startTime: attr('string'),
+ clients: attr('number'),
+ distinct_entities: attr('number'),
+ non_entity_tokens: attr('number'),
});
diff --git a/ui/app/models/metrics/config.js b/ui/app/models/clients/config.js
similarity index 100%
rename from ui/app/models/metrics/config.js
rename to ui/app/models/clients/config.js
diff --git a/ui/app/router.js b/ui/app/router.js
index ee964bfb7..090def1fa 100644
--- a/ui/app/router.js
+++ b/ui/app/router.js
@@ -15,9 +15,8 @@ Router.map(function() {
this.route('logout');
this.mount('open-api-explorer', { path: '/api-explorer' });
this.route('license');
- this.route('metrics', function() {
+ this.route('clients', function() {
this.route('index', { path: '/' });
- this.route('config');
this.route('edit');
});
this.route('storage', { path: '/storage/raft' });
diff --git a/ui/app/routes/vault/cluster/metrics/edit.js b/ui/app/routes/vault/cluster/clients/edit.js
similarity index 62%
rename from ui/app/routes/vault/cluster/metrics/edit.js
rename to ui/app/routes/vault/cluster/clients/edit.js
index ddb4f482a..51f0bb9b8 100644
--- a/ui/app/routes/vault/cluster/metrics/edit.js
+++ b/ui/app/routes/vault/cluster/clients/edit.js
@@ -2,6 +2,6 @@ import Route from '@ember/routing/route';
export default Route.extend({
model() {
- return this.store.queryRecord('metrics/config', {});
+ return this.store.queryRecord('clients/config', {});
},
});
diff --git a/ui/app/routes/vault/cluster/metrics/index.js b/ui/app/routes/vault/cluster/clients/index.js
similarity index 51%
rename from ui/app/routes/vault/cluster/metrics/index.js
rename to ui/app/routes/vault/cluster/clients/index.js
index 59e7b5a3c..b08d01e07 100644
--- a/ui/app/routes/vault/cluster/metrics/index.js
+++ b/ui/app/routes/vault/cluster/clients/index.js
@@ -4,23 +4,27 @@ import { hash } from 'rsvp';
import { getTime } from 'date-fns';
import { parseDateString } from 'vault/helpers/parse-date-string';
-const getActivityParams = ({ start, end }) => {
+const getActivityParams = ({ tab, start, end }) => {
// Expects MM-yyyy format
// TODO: minStart, maxEnd
let params = {};
- if (start) {
- let startDate = parseDateString(start);
- if (startDate) {
- // TODO: Replace with formatRFC3339 when date-fns is updated
- // converts to milliseconds, divide by 1000 to get epoch
- params.start_time = getTime(startDate) / 1000;
+ if (tab === 'current') {
+ params.tab = tab;
+ } else if (tab === 'history') {
+ if (start) {
+ let startDate = parseDateString(start);
+ if (startDate) {
+ // TODO: Replace with formatRFC3339 when date-fns is updated
+ // converts to milliseconds, divide by 1000 to get epoch
+ params.start_time = getTime(startDate) / 1000;
+ }
}
- }
- if (end) {
- let endDate = parseDateString(end);
- if (endDate) {
- // TODO: Replace with formatRFC3339 when date-fns is updated
- params.end_time = getTime(endDate) / 1000;
+ if (end) {
+ let endDate = parseDateString(end);
+ if (endDate) {
+ // TODO: Replace with formatRFC3339 when date-fns is updated
+ params.end_time = getTime(endDate) / 1000;
+ }
}
}
return params;
@@ -28,6 +32,9 @@ const getActivityParams = ({ start, end }) => {
export default Route.extend(ClusterRoute, {
queryParams: {
+ tab: {
+ refreshModel: true,
+ },
start: {
refreshModel: true,
},
@@ -37,13 +44,13 @@ export default Route.extend(ClusterRoute, {
},
model(params) {
- let config = this.store.queryRecord('metrics/config', {}).catch(e => {
+ let config = this.store.queryRecord('clients/config', {}).catch(e => {
console.debug(e);
// swallowing error so activity can show if no config permissions
return {};
});
const activityParams = getActivityParams(params);
- let activity = this.store.queryRecord('metrics/activity', activityParams);
+ let activity = this.store.queryRecord('clients/activity', activityParams);
return hash({
queryStart: params.start,
diff --git a/ui/app/routes/vault/cluster/metrics/config.js b/ui/app/routes/vault/cluster/metrics/config.js
deleted file mode 100644
index ed1a32d5e..000000000
--- a/ui/app/routes/vault/cluster/metrics/config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
-
-export default Route.extend(ClusterRoute, {
- model() {
- return this.store.queryRecord('metrics/config', {});
- },
-});
diff --git a/ui/app/serializers/metrics/activity.js b/ui/app/serializers/clients/activity.js
similarity index 100%
rename from ui/app/serializers/metrics/activity.js
rename to ui/app/serializers/clients/activity.js
diff --git a/ui/app/serializers/metrics/config.js b/ui/app/serializers/clients/config.js
similarity index 100%
rename from ui/app/serializers/metrics/config.js
rename to ui/app/serializers/clients/config.js
diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js
index c58bb3f39..aa67e2128 100644
--- a/ui/app/services/permissions.js
+++ b/ui/app/services/permissions.js
@@ -29,7 +29,7 @@ const API_PATHS = {
seal: 'sys/seal',
raft: 'sys/storage/raft/configuration',
},
- metrics: {
+ clients: {
activity: 'sys/internal/counters/activity',
config: 'sys/internal/counters/config',
},
diff --git a/ui/app/styles/components/bar-chart.scss b/ui/app/styles/components/bar-chart.scss
index 1cc9bc126..44458d04c 100644
--- a/ui/app/styles/components/bar-chart.scss
+++ b/ui/app/styles/components/bar-chart.scss
@@ -7,14 +7,11 @@
> div.is-border {
border: 0.3px solid $ui-gray-200;
- width: 94%;
- margin-left: 3%;
margin-bottom: $spacing-xxs;
}
}
.chart-header {
- margin-left: $spacing-l;
display: grid;
grid-template-columns: 3fr 1fr;
@@ -47,7 +44,7 @@
}
.bar-chart-container {
- padding: $spacing-m $spacing-l $spacing-m $spacing-l;
+ padding: $spacing-m 0;
}
.bar-chart {
diff --git a/ui/app/styles/components/stat-text.scss b/ui/app/styles/components/stat-text.scss
index 814d8a87b..efd76fa62 100644
--- a/ui/app/styles/components/stat-text.scss
+++ b/ui/app/styles/components/stat-text.scss
@@ -1,5 +1,8 @@
.stat-text-container {
line-height: normal;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
&.l,
&.m {
@@ -13,6 +16,7 @@
font-weight: $font-weight-normal;
color: $ui-gray-700;
line-height: inherit;
+ flex-grow: 1;
}
.stat-value {
font-size: $size-3;
diff --git a/ui/app/templates/components/pricing-metrics-config.hbs b/ui/app/templates/components/clients/config.hbs
similarity index 98%
rename from ui/app/templates/components/pricing-metrics-config.hbs
rename to ui/app/templates/components/clients/config.hbs
index b68137357..0551956de 100644
--- a/ui/app/templates/components/pricing-metrics-config.hbs
+++ b/ui/app/templates/components/clients/config.hbs
@@ -54,7 +54,8 @@
Save
Cancel
diff --git a/ui/app/templates/components/clients/history.hbs b/ui/app/templates/components/clients/history.hbs
new file mode 100644
index 000000000..6936c11cd
--- /dev/null
+++ b/ui/app/templates/components/clients/history.hbs
@@ -0,0 +1,149 @@
+{{#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 @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}}
+ {{#unless this.hasClientData}}
+ {{#if (eq @tab 'current')}}
+ {{#if (eq @model.config.enabled 'On')}}
+
+ {{/if}}
+ {{else}}
+
+ {{/if}}
+ {{else}}
+
+
+
+
+
+ Total usage
+
+
+ These totals are within this namespace and all its children.
+
+
+
+ Learn more
+
+
+
+
+
+
+ {{#if this.showGraphs}}
+
+ {{/if}}
+ {{/unless}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/components/cluster-info.hbs b/ui/app/templates/components/cluster-info.hbs
index c2a28a49c..c404841d7 100644
--- a/ui/app/templates/components/cluster-info.hbs
+++ b/ui/app/templates/components/cluster-info.hbs
@@ -116,12 +116,12 @@
{{/if}}
{{/if}}
- {{#if ( and (has-permission 'metrics' routeParams='activity') (not @cluster.dr.isSecondary) this.auth.currentToken)}}
+ {{#if ( and (has-permission 'clients' routeParams='activity') (not @cluster.dr.isSecondary) this.auth.currentToken)}}