UI/Current month view (#13788)

* add timestamp to attribution

* create usage stat component

* updates stat text boxes

* remove flex-header css

* remove comment

* add empty state if no data

* update monthly serializer

* remove empty state - unnecessary
This commit is contained in:
claire bontempo 2022-01-27 10:59:08 -08:00 committed by GitHub
parent d0efb80c23
commit 34ad4ca9f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 134 additions and 147 deletions

View File

@ -16,6 +16,7 @@ import { tracked } from '@glimmer/tracking';
* @startTimeDisplay={{this.startTimeDisplay}}
* @endTimeDisplay={{this.endTimeDisplay}}
* @isDateRange={{this.isDateRange}}
* @timestamp={{this.responseTimestamp}}
* />
* ```
* @param {array} chartLegend - (passed to child) array of objects with key names 'key' and 'label' so data can be stacked
@ -25,6 +26,7 @@ import { tracked } from '@glimmer/tracking';
* @param {string} startTimeDisplay - start date for CSV modal
* @param {string} endTimeDisplay - end date for CSV modal
* @param {boolean} isDateRange - getter calculated in parent to relay if dataset is a date range or single month
* @param {string} timestamp - timestamp response was received from API
*/
export default class Attribution extends Component {

View File

@ -0,0 +1,22 @@
import Component from '@glimmer/component';
export default class Current extends Component {
chartLegend = [
{ key: 'entity_clients', label: 'entity clients' },
{ key: 'non_entity_clients', label: 'non-entity clients' },
];
// data for horizontal bar chart in attribution component
get topTenNamespaces() {
return this.args.model.monthly?.byNamespace;
}
// top level TOTAL client counts from response for given month
get runningTotals() {
return this.args.model.monthly?.total;
}
get responseTimestamp() {
return this.args.model.monthly?.responseTimestamp;
}
}

View File

@ -21,8 +21,8 @@ export default class Dashboard extends Component {
];
maxNamespaces = 10;
chartLegend = [
{ key: 'entity_clients', label: 'unique entities' },
{ key: 'non_entity_clients', label: 'non-entity tokens' },
{ key: 'entity_clients', label: 'entity clients' },
{ key: 'non_entity_clients', label: 'non-entity clients' },
];
// TODO remove this adapter variable? or set to /clients/activity ?
adapter = this.store.adapterFor('clients/new-init-activity');
@ -85,6 +85,13 @@ export default class Dashboard extends Component {
return this.args.model.activity.byNamespace;
}
get responseTimestamp() {
if (!this.args.model.activity || !this.args.model.activity.responseTimestamp) {
return null;
}
return this.args.model.activity.responseTimestamp;
}
@action
async handleClientActivityQuery(month, year, dateType) {
if (dateType === 'cancel') {

View File

@ -1,5 +1,6 @@
import Model, { attr } from '@ember-data/model';
export default class Activity extends Model {
@attr('string') responseTimestamp;
@attr('array') byNamespace;
@attr('string') endTime;
@attr('string') startTime;

View File

@ -1,15 +1,7 @@
import Model, { attr } from '@ember-data/model';
// ARG TODO copied from before, modify for what you need
export default class Monthly extends Model {
@attr('string') responseTimestamp;
@attr('array') byNamespace;
@attr('object') total;
@attr('string') timestamp;
// TODO CMB remove 'clients' and use 'total' object?
@attr('number') clients;
// new names
@attr('number') entityClients;
@attr('number') nonEntityClients;
// old names
@attr('number') distinctEntities;
@attr('number') nonEntityTokens;
}

View File

@ -1,4 +1,5 @@
import ApplicationSerializer from '../application';
import { formatISO } from 'date-fns';
/*
SAMPLE PAYLOAD BEFORE/AFTER:
@ -109,9 +110,10 @@ export default ApplicationSerializer.extend({
},
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
let response_timestamp = formatISO(new Date());
let transformedPayload = {
...payload,
// TODO CMB should these be nested under "data" to go to model correctly?)
response_timestamp,
by_namespace: this.flattenDataset(payload.data.by_namespace),
};
delete payload.data.by_namespace;

View File

@ -1,14 +1,14 @@
import { formatISO } from 'date-fns';
import ApplicationSerializer from '../application';
import { formatISO } from 'date-fns';
export default ApplicationSerializer.extend({
flattenDataset(payload) {
let topTen = payload.slice(0, 10);
let topTen = payload ? payload.slice(0, 10) : [];
return topTen.map((ns) => {
// 'namespace_path' is an empty string for root
if (ns['namespace_id'] === 'root') ns['namespace_path'] = 'root';
let label = ns['namespace_path'] || ns['namespace_id']; // TODO CMB will namespace_path ever be empty?
let label = ns['namespace_path'];
let flattenedNs = {};
// we don't want client counts nested within the 'counts' object for stacked charts
Object.keys(ns['counts']).forEach((key) => (flattenedNs[key] = ns['counts'][key]));
@ -39,16 +39,16 @@ export default ApplicationSerializer.extend({
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
let { data } = payload;
let { clients, distinct_entities, non_entity_tokens } = data;
let timestamp = formatISO(new Date());
let response_timestamp = formatISO(new Date());
let transformedPayload = {
...payload,
// TODO CMB should these be nested under "data" to go to model correctly?)
timestamp,
response_timestamp,
by_namespace: this.flattenDataset(data.by_namespace),
// nest within 'total' object to mimic /activity response shape
total: {
clients,
entity_clients: distinct_entities,
non_entity_clients: non_entity_tokens,
entityClients: distinct_entities,
nonEntityClients: non_entity_tokens,
},
};
delete payload.data.by_namespace;

View File

@ -103,15 +103,3 @@
}
}
}
.has-gap {
gap: $spacing-xxl;
}
.stat-text-flex-wrapper {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
column-gap: 70px;
row-gap: 20px;
}

View File

@ -38,14 +38,23 @@
}
}
.has-export {
.has-header-link {
display: grid;
grid-template-columns: 4fr 1fr;
.header-right {
text-align: right;
> button {
font-size: $size-8;
&:hover,
&.is-hovered,
&:focus,
&.is-focused {
background-color: transparent;
background-color: darken($ui-gray-050, 5%);
border-color: darken($ui-gray-300, 5%);
}
}
> a {
&:hover {
text-decoration: underline;
}
@ -102,6 +111,11 @@
}
}
.chart-empty-state {
grid-column-start: 1;
grid-column-end: -1;
}
.chart-subTitle {
grid-column-start: 1;
grid-column-end: 3;

View File

@ -1,5 +1,5 @@
<div class="chart-wrapper single-chart-grid">
<div class="chart-header has-export has-bottom-margin-m">
<div class="chart-header has-header-link has-bottom-margin-m">
<div class="header-left">
<h2 class="chart-title">Attribution</h2>
<p class="chart-description">{{this.chartText.description}}</p>
@ -13,34 +13,31 @@
</div>
</div>
{{#if this.totalClientsData}}
<div class="chart-container-wide">
<Clients::HorizontalBarChart
@dataset={{this.totalClientsData}}
@chartLegend={{@chartLegend}}
@clientTotals={{@runningTotals}}
/>
</div>
<div class="chart-container-wide">
<Clients::HorizontalBarChart
@dataset={{this.totalClientsData}}
@chartLegend={{@chartLegend}}
@clientTotals={{@runningTotals}}
/>
</div>
<div class="chart-subTitle">
<p class="chart-subtext">{{this.chartText.totalCopy}}</p>
</div>
<div class="chart-subTitle">
<p class="chart-subtext">{{this.chartText.totalCopy}}</p>
</div>
<div class="data-details-top">
<h3 class="data-details">Top {{lowercase this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.label}}</p>
</div>
<div class="data-details-top">
<h3 class="data-details">Top {{lowercase this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.label}}</p>
</div>
<div class="data-details-bottom">
<h3 class="data-details">Clients in {{lowercase this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.clients}}</p>
</div>
{{else}}
<EmptyState />
{{/if}}
<div class="data-details-bottom">
<h3 class="data-details">Clients in {{lowercase this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.clients}}</p>
</div>
<div class="timestamp">
Updated Nov 15 2021, 4:07:32 pm
Updated
{{date-format @timestamp "MMM dd yyyy, h:mm:ss aaa"}}
</div>
<div class="legend-center">

View File

@ -18,64 +18,16 @@
{{#if @isLoading}}
<LayoutLoading />
{{else}}
<div class="card has-bottom-margin-m">
<div class="card-content">
<div class="is-flex is-flex-center">
<div class="is-flex-1">
<h2 class="title is-5 is-marginless">
Total usage
</h2>
<p class="sub-text">
These totals are within this namespace and all its children.
</p>
</div>
<LearnLink @path="/tutorials/vault/usage-metrics">
Learn more
</LearnLink>
</div>
<hr />
<div class="columns">
<div class="column" data-test-client-count-stats>
<StatText
@label="Total active clients"
{{! ARG TODO naming is changing }}
@value={{or @model.monthly.clients "0"}}
@size="l"
@subText="The sum of unique entities and non-entity tokens; Vault's primary billing metric."
/>
</div>
<div class="column">
<StatText
class="column"
@label="Unique entities"
{{! ARG TODO naming is changing }}
@value={{or @model.monthly.distinctEntities "0"}}
@size="l"
@subText="Representation of a particular user, client or application that created a token via login."
/>
</div>
<div class="column">
<StatText
class="column"
@label="Non-entity tokens"
@value={{or @model.monthly.nonEntityTokens "0"}}
@size="l"
@subText="Tokens created via a method that is not associated with an entity."
/>
</div>
</div>
</div>
</div>
<Clients::UsageStats @title={{date-format this.responseTimestamp "MMMM"}} @runningTotals={{this.runningTotals}} />
<Clients::Attribution
@chartLegend={{array
(hash key="entity_clients" label="Unique entities")
(hash key="non_entity_clients" label="Non-entity tokens")
}}
@topTenNamespaces={{@model.monthly.byNamespace}}
@runningTotals={{@model.monthly.total}}
@selectedNamespace={{@model.monthly.selectedNamespace}}
@startTimeDisplay={{date-format @model.monthly.timestamp "MMMM yyyy"}}
@chartLegend={{this.chartLegend}}
@topTenNamespaces={{this.topTenNamespaces}}
@runningTotals={{this.runningTotals}}
@selectedNamespace={{this.selectedNamespace}}
@startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
@isDateRange={{false}}
@timestamp={{this.responseTimestamp}}
/>
{{/if}}
</div>

View File

@ -74,6 +74,7 @@
@startTimeDisplay={{this.startTimeDisplay}}
@endTimeDisplay={{this.endTimeDisplay}}
@isDateRange={{this.isDateRange}}
@timestamp={{this.responseTimestamp}}
/>
{{else}}
{{! ARG TODO remove once we have this dialed }}

View File

@ -1,33 +0,0 @@
<div class="chart-wrapper">
<div class="chart-header has-bottom-margin-l">
<h2 class="chart-title">{{@month}}</h2>
</div>
<div class="stat-text-flex-wrapper">
<div class="column-left">
<StatText
@label="New clients"
@value={{2376}}
@size="l"
@subText="This is the number of clients which were created in Vault for the first time in the selected month."
/>
<div class="has-top-margin-l is-flex-start has-gap">
<StatText @label={{capitalize @chartLegend.0.label}} @value={{521}} @size="m" />
<StatText @label={{capitalize @chartLegend.1.label}} @value={{1855}} @size="m" />
</div>
</div>
<div class="column-right">
<StatText
@label="Total monthly clients"
@value={{3013}}
@size="l"
@subText="This is the number of total clients which used Vault for the given month, both new and previous."
/>
<div class="has-top-margin-l is-flex-start has-gap">
<StatText @label={{capitalize @chartLegend.0.label}} @value={{521}} @size="m" />
<StatText @label={{capitalize @chartLegend.1.label}} @value={{1855}} @size="m" />
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,42 @@
<div class="chart-wrapper">
<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>
</div>
<div class="header-right">
<LearnLink @path="/tutorials/vault/usage-metrics">
Learn more
</LearnLink>
</div>
</div>
<div class="columns">
<div class="column" data-test-client-count-stats>
<StatText
@label="Total clients"
@value={{or @runningTotals.clients "0"}}
@size="l"
@subText="The sum of entity clientIDs and non-entity clientIDs; this is Vaults primary billing metric."
/>
</div>
<div class="column">
<StatText
class="column"
@label="Entity clients"
@value={{or @runningTotals.entityClients "0"}}
@size="l"
@subText="Representations of a particular user, client, or application that created a token via login."
/>
</div>
<div class="column">
<StatText
class="column"
@label="Non-entity clients"
@value={{or @runningTotals.nonEntityClients "0"}}
@size="l"
@subText="Clients created with a shared set of permissions, but not associated with an entity. "
/>
</div>
</div>
</div>