diff --git a/changelog/client-counts.txt b/changelog/client-counts.txt new file mode 100644 index 000000000..271414e45 --- /dev/null +++ b/changelog/client-counts.txt @@ -0,0 +1,3 @@ +```release-note:feature +**UI Client Count Improvements**: Restructures client count dashboard, making use of billing start date to improve accuracy. Adds mount-level distribution and filtering. +``` \ No newline at end of file diff --git a/ui/app/components/clients/history-old.js b/ui/app/components/clients/history-old.js deleted file mode 100644 index 024357449..000000000 --- a/ui/app/components/clients/history-old.js +++ /dev/null @@ -1,124 +0,0 @@ -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/pricing-metrics-dates.js b/ui/app/components/pricing-metrics-dates.js deleted file mode 100644 index a1d1a5c12..000000000 --- a/ui/app/components/pricing-metrics-dates.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @module PricingMetricsDates - * PricingMetricsDates components are used on the Pricing Metrics page to handle queries related to pricing metrics. - * This component assumes that query parameters (as in, from route params) are being passed in with the format MM-yyyy, - * while the inputs expect a format of MM/yyyy. - * - * @example - * ```js - * - * ``` - * @param {object} resultStart - resultStart is the start date of the metrics returned. Should be a valid date string that the built-in Date() fn can parse - * @param {object} resultEnd - resultEnd is the end date of the metrics returned. Should be a valid date string that the built-in Date() fn can parse - * @param {string} [queryStart] - queryStart is the route param (formatted MM-yyyy) that the result will be measured against for showing discrepancy warning - * @param {string} [queryEnd] - queryEnd is the route param (formatted MM-yyyy) that the result will be measured against for showing discrepancy warning - * @param {number} [defaultSpan=12] - setting for default time between start and end input dates - * @param {number} [retentionMonths=24] - setting for the retention months, which informs valid dates to query by - */ -import { set, computed } from '@ember/object'; -import { inject as service } from '@ember/service'; -import Component from '@ember/component'; -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'; - -export default Component.extend({ - layout, - router: service(), - - queryStart: null, - queryEnd: null, - resultStart: null, - resultEnd: null, - - start: null, - end: null, - - defaultSpan: 12, - retentionMonths: 24, - - startDate: computed('start', function () { - if (!this.start) return null; - let date; - try { - date = parseDateString(this.start, '/'); - if (date) return date; - return null; - } catch (e) { - return null; - } - }), - endDate: computed('end', function () { - if (!this.end) return null; - let date; - try { - date = parseDateString(this.end, '/'); - if (date) return endOfMonth(date); - return null; - } catch (e) { - return null; - } - }), - - error: computed('end', 'endDate', 'retentionMonths', 'start', 'startDate', function () { - if (!this.startDate) { - return 'Start date is invalid. Please use format MM/yyyy'; - } - if (!this.endDate) { - return 'End date is invalid. Please use format MM/yyyy'; - } - if (isBefore(this.endDate, this.startDate)) { - return 'Start date is after end date'; - } - const lastMonthAvailable = endOfMonth(subMonths(startOfToday(), 1)); - if (isBefore(lastMonthAvailable, this.endDate)) { - return `Data is not available until the end of the month`; - } - const earliestRetained = startOfMonth(subMonths(lastMonthAvailable, this.retentionMonths)); - if (isBefore(this.startDate, earliestRetained)) { - return `No data retained before ${format(earliestRetained, 'MM/yyyy')} due to your settings`; - } - - return null; - }), - - init() { - this._super(...arguments); - let initialEnd; - let initialStart; - - initialEnd = subMonths(startOfToday(), 1); - if (this.queryEnd) { - initialEnd = parseDateString(this.queryEnd, '-'); - } else { - // if query isn't passed in, set it so that showResultsWarning works - this.queryEnd = format(initialEnd, 'MM-yyyy'); - } - initialStart = subMonths(initialEnd, this.defaultSpan); - if (this.queryStart) { - initialStart = parseDateString(this.queryStart, '-'); - } else { - // if query isn't passed in, set it so that showResultsWarning works - this.queryStart = format(initialStart, 'MM-yyyy'); - } - - set(this, 'start', format(initialStart, 'MM/yyyy')); - set(this, 'end', format(initialEnd, 'MM/yyyy')); - }, - - actions: { - handleQuery() { - const start = format(this.startDate, 'MM-yyyy'); - const end = format(this.endDate, 'MM-yyyy'); - this.router.transitionTo('vault.cluster.clients', { - queryParams: { - start, - end, - }, - }); - }, - }, -}); diff --git a/ui/app/styles/components/pricing-metrics-dates.scss b/ui/app/styles/components/pricing-metrics-dates.scss deleted file mode 100644 index b7483fdf3..000000000 --- a/ui/app/styles/components/pricing-metrics-dates.scss +++ /dev/null @@ -1,4 +0,0 @@ -.pricing-metrics-date-form { - display: flex; - align-items: flex-end; -} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index 567cbdb6f..21f8a188b 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -82,7 +82,6 @@ @import './components/navigate-input'; @import './components/page-header'; @import './components/popup-menu'; -@import './components/pricing-metrics-dates'; @import './components/radio-card'; @import './components/radial-progress'; @import './components/raft-join'; diff --git a/ui/app/templates/components/clients/history-old.hbs b/ui/app/templates/components/clients/history-old.hbs deleted file mode 100644 index f625a276d..000000000 --- a/ui/app/templates/components/clients/history-old.hbs +++ /dev/null @@ -1,213 +0,0 @@ -{{#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/pricing-metrics-dates.hbs b/ui/app/templates/components/pricing-metrics-dates.hbs deleted file mode 100644 index 26f8b9282..000000000 --- a/ui/app/templates/components/pricing-metrics-dates.hbs +++ /dev/null @@ -1,46 +0,0 @@ -
-
-
- -
- -
-
-
- -
- -
-
- -
-
- -{{#if this.error}} - {{this.error}} -{{/if}} -
- {{#if (and this.resultStart this.resultEnd)}} -

- {{date-format this.resultStart "MMM dd, yyyy" dateOnly=true}} - through - {{date-format this.resultEnd "MMM dd, yyyy" dateOnly=true}} -

- {{/if}} -
\ No newline at end of file diff --git a/ui/lib/core/addon/components/bar-chart.js b/ui/lib/core/addon/components/bar-chart.js deleted file mode 100644 index d9c65ddfe..000000000 --- a/ui/lib/core/addon/components/bar-chart.js +++ /dev/null @@ -1,310 +0,0 @@ -/** - * @module BarChart - * BarChart components are used to display data in the form of a stacked bar chart, with accompanying legend and tooltip. Anything passed into the block will display in the top right of the chart header. - * - * @example - * ```js - * - * /> - * - * - * mapLegendSample = [{ - * key: "api_key_for_label", - * label: "Label Displayed on Legend" - * }] - * ``` - * - * @param {string} title - title of the chart - * @param {array} mapLegend - array of objects with key names 'key' and 'label' for the map legend - * @param {object} dataset - dataset for the chart - * @param {any} tooltipData - misc. information needed to display tooltip (i.e. total clients from query params) - * @param {string} [description] - description of the chart - * @param {string} [labelKey=label] - labelKey is the key name in the dataset passed in that corresponds to the value labeling the y-axis - * @param {function} [onClick] - takes function from parent and passes it to click event on data bars - * - */ - -import Component from '@glimmer/component'; -import layout from '../templates/components/bar-chart'; -import { setComponentTemplate } from '@ember/component'; -import { assert } from '@ember/debug'; -import { action } from '@ember/object'; -import { guidFor } from '@ember/object/internals'; -import { scaleLinear, scaleBand } from 'd3-scale'; -import { axisLeft } from 'd3-axis'; -import { max } from 'd3-array'; -import { stack } from 'd3-shape'; -// eslint-disable-next-line no-unused-vars -import { select, event, selectAll } from 'd3-selection'; -// eslint-disable-next-line no-unused-vars -import { transition } from 'd3-transition'; - -// SIZING CONSTANTS -const CHART_MARGIN = { top: 10, left: 137 }; // makes space for y-axis legend -const CHAR_LIMIT = 18; // character count limit for y-axis labels to trigger truncating -const LINE_HEIGHT = 24; // each bar w/ padding is 24 pixels thick - -// COLOR THEME: -const LIGHT_AND_DARK_BLUE = ['#BFD4FF', '#8AB1FF']; -const BAR_COLOR_HOVER = ['#1563FF', '#0F4FD1']; -const TOOLTIP_BACKGROUND = '#525761'; -const GREY = '#EBEEF2'; -class BarChartComponent extends Component { - get labelKey() { - return this.args.labelKey || 'label'; - } - - get mapLegend() { - assert( - 'map legend is required, must be an array of objects with key names of "key" and "label"', - this.hasLegend() - ); - return this.args.mapLegend; - } - - hasLegend() { - if (!this.args.mapLegend || !Array.isArray(this.args.mapLegend)) { - return false; - } else { - let legendKeys = this.args.mapLegend.map((obj) => Object.keys(obj)); - return legendKeys.map((array) => array.includes('key', 'label')).every((element) => element === true); - } - } - - @action - renderBarChart(element, args) { - let elementId = guidFor(element); - let dataset = args[0]; - let totalCount = args[1]; - let handleClick = this.args.onClick; - let labelKey = this.labelKey; - let stackFunction = stack().keys(this.mapLegend.map((l) => l.key)); - // creates an array of data for each map legend key - // each array contains coordinates for each data bar - let stackedData = stackFunction(dataset); - - // creates and appends tooltip - let container = select('.bar-chart-container'); - container - .append('div') - .attr('class', 'chart-tooltip') - .attr('style', 'position: absolute; opacity: 0;') - .style('color', 'white') - .style('background', `${TOOLTIP_BACKGROUND}`) - .style('font-size', '.929rem') - .style('padding', '10px') - .style('border-radius', '4px'); - - let xScale = scaleLinear() - .domain([0, max(dataset.map((d) => d.total))]) - .range([0, 75]); // 25% reserved for margins - - let yScale = scaleBand() - .domain(dataset.map((d) => d[labelKey])) - .range([0, dataset.length * LINE_HEIGHT]) - .paddingInner(0.765); // percent of the total width to reserve for padding between bars - - let chartSvg = select(element); - chartSvg.attr('viewBox', `0 0 710 ${(dataset.length + 1) * LINE_HEIGHT}`); - chartSvg.attr('id', elementId); - - // creates group for each array of stackedData - let groups = chartSvg - .selectAll('g') - .remove() - .exit() - .data(stackedData) - .enter() - .append('g') - // shifts chart to accommodate y-axis legend - .attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top})`) - .style('fill', (d, i) => LIGHT_AND_DARK_BLUE[i]); - - let yAxis = axisLeft(yScale).tickSize(0); - yAxis(chartSvg.append('g').attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top})`)); - - let truncate = (selection) => - selection.text((string) => - string.length < CHAR_LIMIT ? string : string.slice(0, CHAR_LIMIT - 3) + '...' - ); - - chartSvg.selectAll('.tick text').call(truncate); - - groups - .selectAll('rect') - // iterate through the stacked data and chart respectively - .data((stackedData) => stackedData) - .enter() - .append('rect') - .attr('class', 'data-bar') - .style('cursor', 'pointer') - .attr('width', (chartData) => `${xScale(chartData[1] - chartData[0]) - 0.25}%`) - .attr('height', yScale.bandwidth()) - .attr('x', (chartData) => `${xScale(chartData[0])}%`) - .attr('y', ({ data }) => yScale(data[labelKey])) - .attr('rx', 3) - .attr('ry', 3); - - let actionBars = chartSvg - .selectAll('.action-bar') - .data(dataset) - .enter() - .append('rect') - .style('cursor', 'pointer') - .attr('class', 'action-bar') - .attr('width', '100%') - .attr('height', `${LINE_HEIGHT}px`) - .attr('x', '0') - .attr('y', (chartData) => yScale(chartData[labelKey])) - .style('fill', `${GREY}`) - .style('opacity', '0') - .style('mix-blend-mode', 'multiply'); - - let yLegendBars = chartSvg - .selectAll('.label-bar') - .data(dataset) - .enter() - .append('rect') - .style('cursor', 'pointer') - .attr('class', 'label-action-bar') - .attr('width', CHART_MARGIN.left) - .attr('height', `${LINE_HEIGHT}px`) - .attr('x', '0') - .attr('y', (chartData) => yScale(chartData[labelKey])) - .style('opacity', '0') - .style('mix-blend-mode', 'multiply'); - - let dataBars = chartSvg.selectAll('rect.data-bar'); - let actionBarSelection = chartSvg.selectAll('rect.action-bar'); - let compareAttributes = (elementA, elementB, attr) => - select(elementA).attr(`${attr}`) === elementB.getAttribute(`${attr}`); - - // handles click and mouseover/out/move event for data bars - actionBars - .on('click', function (chartData) { - if (handleClick) { - handleClick(chartData); - } - }) - .on('mouseover', function () { - select(this).style('opacity', 1); - dataBars - .filter(function () { - return compareAttributes(this, event.target, 'y'); - }) - .style('fill', (b, i) => `${BAR_COLOR_HOVER[i]}`); - select('.chart-tooltip').transition().duration(200).style('opacity', 1); - }) - .on('mouseout', function () { - select(this).style('opacity', 0); - select('.chart-tooltip').style('opacity', 0); - dataBars - .filter(function () { - return compareAttributes(this, event.target, 'y'); - }) - .style('fill', (b, i) => `${LIGHT_AND_DARK_BLUE[i]}`); - }) - .on('mousemove', function (chartData) { - select('.chart-tooltip') - .style('opacity', 1) - .style('max-width', '200px') - .style('left', `${event.pageX - 325}px`) - .style('top', `${event.pageY - 140}px`) - .text( - `${Math.round((chartData.total * 100) / totalCount)}% of total client counts: - ${chartData.non_entity_tokens} non-entity tokens, ${chartData.distinct_entities} unique entities. - ` - ); - }); - - // handles mouseover/out/move event for y-axis legend - yLegendBars - .on('click', function (chartData) { - if (handleClick) { - handleClick(chartData); - } - }) - .on('mouseover', function (chartData) { - dataBars - .filter(function () { - return compareAttributes(this, event.target, 'y'); - }) - .style('fill', (b, i) => `${BAR_COLOR_HOVER[i]}`); - actionBarSelection - .filter(function () { - return compareAttributes(this, event.target, 'y'); - }) - .style('opacity', '1'); - if (chartData.label.length >= CHAR_LIMIT) { - select('.chart-tooltip').transition().duration(200).style('opacity', 1); - } - }) - .on('mouseout', function () { - select('.chart-tooltip').style('opacity', 0); - dataBars - .filter(function () { - return compareAttributes(this, event.target, 'y'); - }) - .style('fill', (b, i) => `${LIGHT_AND_DARK_BLUE[i]}`); - actionBarSelection - .filter(function () { - return compareAttributes(this, event.target, 'y'); - }) - .style('opacity', '0'); - }) - .on('mousemove', function (chartData) { - if (chartData.label.length >= CHAR_LIMIT) { - select('.chart-tooltip') - .style('left', `${event.pageX - 300}px`) - .style('top', `${event.pageY - 100}px`) - .text(`${chartData.label}`) - .style('max-width', 'fit-content'); - } else { - select('.chart-tooltip').style('opacity', 0); - } - }); - - chartSvg - .append('g') - .attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top + 2})`) - .selectAll('text') - .data(dataset) - .enter() - .append('text') - .text((d) => d.total) - .attr('fill', '#000') - .attr('class', 'total-value') - .style('font-size', '.8rem') - .attr('text-anchor', 'start') - .attr('alignment-baseline', 'mathematical') - .attr('x', (chartData) => `${xScale(chartData.total)}%`) - .attr('y', (chartData) => yScale(chartData.label)); - - chartSvg.select('.domain').remove(); - - // TODO: if mapLegend has more than 4 keys, y attrs ('cy' and 'y') will need to be set to a variable. Currently map keys are centered in the legend SVG (50%) - // each map key symbol & label takes up 20% of legend SVG width - let startingXCoordinate = 100 - this.mapLegend.length * 20; // subtract from 100% to find starting x-coordinate - let legendSvg = select('.legend'); - this.mapLegend.map((legend, i) => { - let xCoordinate = startingXCoordinate + i * 20; - legendSvg - .append('circle') - .attr('cx', `${xCoordinate}%`) - .attr('cy', '50%') - .attr('r', 6) - .style('fill', `${LIGHT_AND_DARK_BLUE[i]}`); - legendSvg - .append('text') - .attr('x', `${xCoordinate + 2}%`) - .attr('y', '50%') - .text(`${legend.label}`) - .style('font-size', '.8rem') - .attr('alignment-baseline', 'middle'); - }); - } -} - -export default setComponentTemplate(layout, BarChartComponent); diff --git a/ui/lib/core/addon/templates/components/bar-chart.hbs b/ui/lib/core/addon/templates/components/bar-chart.hbs deleted file mode 100644 index 8e4769c40..000000000 --- a/ui/lib/core/addon/templates/components/bar-chart.hbs +++ /dev/null @@ -1,32 +0,0 @@ -
-
-
-

{{@title}}

- {{#if @description}} -

{{@description}}

- {{/if}} -
-
- {{#if @dataset}} - {{yield}} - {{/if}} -
-
- {{#if @dataset}} -
-
- -
-
-
- -
- {{else}} -
- - {{/if}} -
\ No newline at end of file diff --git a/ui/lib/core/app/components/bar-chart.js b/ui/lib/core/app/components/bar-chart.js deleted file mode 100644 index 085613131..000000000 --- a/ui/lib/core/app/components/bar-chart.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'core/components/bar-chart'; diff --git a/ui/lib/core/stories/bar-chart.md b/ui/lib/core/stories/bar-chart.md deleted file mode 100644 index 56eff06f0..000000000 --- a/ui/lib/core/stories/bar-chart.md +++ /dev/null @@ -1,37 +0,0 @@ - - -## BarChart -BarChart components are used to display data in the form of a stacked bar chart, with accompanying legend and tooltip. Anything passed into the block will display in the top right of the chart header. - -**Params** - -| Param | Type | Default | Description | -| --- | --- | --- | --- | -| title | string | | title of the chart | -| mapLegend | array | | array of objects with key names 'key' and 'label' for the map legend | -| dataset | object | | dataset for the chart | -| [description] | string | | description of the chart | -| [labelKey] | string | "label" | labelKey is the key name in the dataset passed in that corresponds to the value labeling the y-axis | -| [onClick] | function | | takes function from parent and passes it to click event on data bars | - -**Example** - -```js - - /> - - - mapLegendSample = [{ - key: "api_key_for_label", - label: "Label Displayed on Legend" - }] -``` - -**See** - -- [Uses of BarChart](https://github.com/hashicorp/vault/search?l=Handlebars&q=BarChart+OR+bar-chart) -- [BarChart Source Code](https://github.com/hashicorp/vault/blob/main/ui/lib/core/addon/components/bar-chart.js) - ---- diff --git a/ui/lib/core/stories/bar-chart.stories.js b/ui/lib/core/stories/bar-chart.stories.js deleted file mode 100644 index 761e7daab..000000000 --- a/ui/lib/core/stories/bar-chart.stories.js +++ /dev/null @@ -1,99 +0,0 @@ -import hbs from 'htmlbars-inline-precompile'; -import { storiesOf } from '@storybook/ember'; -import { object, text, withKnobs } from '@storybook/addon-knobs'; -import notes from './bar-chart.md'; - -const dataset = [ - { - namespace_id: 'root', - namespace_path: 'root', - counts: { - distinct_entities: 268, - non_entity_tokens: 985, - clients: 1253, - }, - }, - { - namespace_id: 'O0i4m', - namespace_path: 'top-namespace', - counts: { - distinct_entities: 648, - non_entity_tokens: 220, - clients: 868, - }, - }, - { - namespace_id: '1oihz', - namespace_path: 'anotherNamespace', - counts: { - distinct_entities: 547, - non_entity_tokens: 337, - clients: 884, - }, - }, - { - namespace_id: '1oihz', - namespace_path: 'someOtherNamespaceawgagawegawgawgawgaweg', - counts: { - distinct_entities: 807, - non_entity_tokens: 234, - clients: 1041, - }, - }, -]; - -const flattenData = () => { - 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'], - }; - }); -}; - -storiesOf('BarChart', module) - .addParameters({ options: { showPanel: true } }) - .addDecorator(withKnobs()) - .add( - `BarChart`, - () => ({ - template: hbs` -
Bar Chart
- -

dataset is passed to a function in the parent to format it appropriately for the chart. Any data passed should be flattened (not nested).

-

The legend typically displays within the bar chart border, below the second grey divider. There is also a tooltip that pops up when hovering over the data bars and overflowing labels. Gotta love storybook :)

-
- - - -
-
Legend:
- - - Active direct tokens - - Unique Entities -
- `, - context: { - title: text('title', 'Top Namespaces'), - description: text( - 'description', - 'Each namespaces client count includes clients in child namespaces.' - ), - dataset: object('dataset', flattenData()), - }, - }), - { notes } - ); diff --git a/ui/tests/integration/components/bar-chart-test.js b/ui/tests/integration/components/bar-chart-test.js deleted file mode 100644 index a87e3dc2b..000000000 --- a/ui/tests/integration/components/bar-chart-test.js +++ /dev/null @@ -1,85 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | bar-chart', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - let dataset = [ - { - namespace_id: 'root', - namespace_path: 'root', - counts: { - distinct_entities: 268, - non_entity_tokens: 985, - clients: 1253, - }, - }, - { - namespace_id: 'O0i4m', - namespace_path: 'top-namespace', - counts: { - distinct_entities: 648, - non_entity_tokens: 220, - clients: 868, - }, - }, - { - namespace_id: '1oihz', - namespace_path: 'anotherNamespace', - counts: { - distinct_entities: 547, - non_entity_tokens: 337, - clients: 884, - }, - }, - { - namespace_id: '1oihz', - namespace_path: 'someOtherNamespaceawgagawegawgawgawgaweg', - counts: { - distinct_entities: 807, - non_entity_tokens: 234, - clients: 1041, - }, - }, - ]; - - let flattenData = () => { - 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'], - }; - }); - }; - - this.set('title', 'Top Namespaces'); - this.set('description', 'Each namespaces client count includes clients in child namespaces.'); - this.set('dataset', flattenData()); - - await render(hbs` - - - - `); - assert.dom('[data-test-bar-chart]').exists('bar chart renders'); - assert.dom('[data-test-bar-chart] .chart-title').hasText('Top Namespaces', 'displays title'); - assert - .dom('[data-test-bar-chart] .chart-description') - .hasText('Each namespaces client count includes clients in child namespaces.', 'displays description'); - assert.dom('.data-bar').exists({ count: 8 }, 'bars render'); - }); -}); diff --git a/ui/tests/integration/components/pricing-metrics-dates-test.js b/ui/tests/integration/components/pricing-metrics-dates-test.js deleted file mode 100644 index 4832027f4..000000000 --- a/ui/tests/integration/components/pricing-metrics-dates-test.js +++ /dev/null @@ -1,98 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, fillIn } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import { subMonths, startOfToday, format, endOfMonth } from 'date-fns'; - -module('Integration | Component | pricing-metrics-dates', function (hooks) { - setupRenderingTest(hooks); - - test('by default it sets the start and end inputs', async function (assert) { - const expectedEnd = subMonths(startOfToday(), 1); - const expectedStart = subMonths(expectedEnd, 12); - await render(hbs` - - `); - assert.dom('[data-test-end-input]').hasValue(format(expectedEnd, 'MM/yyyy'), 'End input is last month'); - assert - .dom('[data-test-start-input]') - .hasValue(format(expectedStart, 'MM/yyyy'), 'Start input is 12 months before last month'); - }); - - test('On init if end date passed, start is calculated', async function (assert) { - const expectedStart = subMonths(new Date(2020, 8, 15), 12); - this.set('queryEnd', '09-2020'); - await render(hbs` - - `); - assert.dom('[data-test-end-input]').hasValue('09/2020', 'End input matches query'); - assert - .dom('[data-test-start-input]') - .hasValue(format(expectedStart, 'MM/yyyy'), 'Start input is 12 months before end input'); - }); - - test('On init if query start date passed, end is default', async function (assert) { - const expectedEnd = subMonths(startOfToday(), 1); - this.set('queryStart', '01-2020'); - await render(hbs` - - `); - assert.dom('[data-test-end-input]').hasValue(format(expectedEnd, 'MM/yyyy'), 'End input is last month'); - assert.dom('[data-test-start-input]').hasValue('01/2020', 'Start input matches query'); - }); - - test('If result and query dates are within 1 day, warning is not shown', async function (assert) { - this.set('resultStart', new Date(2020, 1, 1)); - this.set('resultEnd', new Date(2020, 9, 31)); - await render(hbs` - - `); - assert.dom('[data-test-results-date-warning]').doesNotExist('Does not show result states warning'); - }); - - test('it shows appropriate errors on input form', async function (assert) { - const lastAvailable = endOfMonth(subMonths(startOfToday(), 1)); - const firstAvailable = subMonths(lastAvailable, 12); - await render(hbs` - - `); - assert.dom('[data-test-form-error]').doesNotExist('No form error shows by default'); - - await fillIn('[data-test-start-input]', format(subMonths(firstAvailable, 1), 'MM/yyyy')); - assert - .dom('[data-test-form-error]') - .includesText( - `No data retained before ${format(firstAvailable, 'MM/yyyy')}`, - 'shows the correct error message for starting before the configured retainment period' - ); - - await fillIn('[data-test-end-input]', format(subMonths(lastAvailable, -1), 'MM/yyyy')); - assert - .dom('[data-test-form-error]') - .includesText( - 'Data is not available until the end of the month', - 'shows the correct error message for ending after the end of the last month' - ); - - await fillIn('[data-test-end-input]', 'not/date'); - assert - .dom('[data-test-form-error]') - .includesText( - 'End date is invalid. Please use format MM/yyyy', - 'shows the correct error message for non-date input' - ); - - await fillIn('[data-test-start-input]', `13/${format(lastAvailable, 'yyyy')}`); - assert - .dom('[data-test-form-error]') - .includesText( - 'Start date is invalid. Please use format MM/yyyy', - 'shows the correct error message for an invalid month' - ); - }); -});