diff --git a/ui/app/components/clients/attribution.js b/ui/app/components/clients/attribution.js new file mode 100644 index 00000000000..7df0575cadd --- /dev/null +++ b/ui/app/components/clients/attribution.js @@ -0,0 +1,45 @@ +import Component from '@glimmer/component'; + +// TODO: fill out below!! +/** + * @module Attribution + * Attribution components are used to... + * + * @example + * ```js + * + * Pass in export button + * + * ``` + * @param {object} requiredParam - requiredParam is... + * @param {string} [optionalParam] - optionalParam is... + * @param {string} [param1=defaultValue] - param1 is... + */ + +export default class Attribution extends Component { + get dateRange() { + // some conditional that returns "date range" or "month" depending on what the params are + return 'date range'; + } + + get chartText() { + // something that determines if data is by namespace or by auth method + // and returns text + // if byNamespace + 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 ${this.dateRange}. + This aids in understanding which namespaces create and use new clients + ${this.dateRange === 'date range' ? ' over time.' : '.'}`, + totalCopy: `The total clients in the namespace for this ${this.dateRange}. This number is useful for identifying overall usage volume.`, + }; + // if byAuthMethod + // return + // byAuthMethod = { + // description: "This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where new clients and total clients are originating. Authentication methods are organized by path.", + // newCopy: `The new clients used by the auth method for this {{@range}}. This aids in understanding which auth methods create and use new clients ${this.dateRange === "date range" ? " over time." : "."}`, + // totalCopy: `The total clients used by the auth method for this ${this.dateRange}. This number is useful for identifying overall usage volume. ` + // } + } +} diff --git a/ui/app/components/clients/dashboard.js b/ui/app/components/clients/dashboard.js index 57e6154f22a..7cb300076d6 100644 --- a/ui/app/components/clients/dashboard.js +++ b/ui/app/components/clients/dashboard.js @@ -6,8 +6,8 @@ import { format } from 'date-fns'; export default class Dashboard extends Component { maxNamespaces = 10; chartLegend = [ - { key: 'distinct_entities', label: 'Direct entities' }, - { key: 'non_entity_tokens', label: 'Active direct tokens' }, + { key: 'distinct_entities', label: 'unique entities' }, + { key: 'non_entity_tokens', label: 'non-entity tokens' }, ]; @tracked selectedNamespace = null; @@ -62,6 +62,60 @@ export default class Dashboard extends Component { }); } + // TODO: dataset for line chart + get lineChartData() { + return [ + { month: '1/21', clients: 100, new: 100 }, + { month: '2/21', clients: 300, new: 200 }, + { month: '3/21', clients: 300, new: 0 }, + { month: '4/21', clients: 300, new: 0 }, + { month: '5/21', clients: 300, new: 0 }, + { month: '6/21', clients: 300, new: 0 }, + { month: '7/21', clients: 300, new: 0 }, + { month: '8/21', clients: 350, new: 50 }, + { month: '9/21', clients: 400, new: 50 }, + { month: '10/21', clients: 450, new: 50 }, + { month: '11/21', clients: 500, new: 50 }, + { month: '12/21', clients: 1000, new: 1000 }, + ]; + } + + // TODO: dataset for new monthly clients vertical bar chart (manage in serializer?) + get newMonthlyClients() { + return [ + { month: 'January', distinct_entities: 1000, non_entity_tokens: 322, total: 1322 }, + { month: 'February', distinct_entities: 1500, non_entity_tokens: 122, total: 1622 }, + { month: 'March', distinct_entities: 4300, non_entity_tokens: 700, total: 5000 }, + { month: 'April', distinct_entities: 1550, non_entity_tokens: 229, total: 1779 }, + { month: 'May', distinct_entities: 5560, non_entity_tokens: 124, total: 5684 }, + { month: 'June', distinct_entities: 1570, non_entity_tokens: 142, total: 1712 }, + { month: 'July', distinct_entities: 300, non_entity_tokens: 112, total: 412 }, + { month: 'August', distinct_entities: 1610, non_entity_tokens: 130, total: 1740 }, + { month: 'September', distinct_entities: 1900, non_entity_tokens: 222, total: 2122 }, + { month: 'October', distinct_entities: 500, non_entity_tokens: 166, total: 666 }, + { month: 'November', distinct_entities: 480, non_entity_tokens: 132, total: 612 }, + { month: 'December', distinct_entities: 980, non_entity_tokens: 202, total: 1182 }, + ]; + } + + // TODO: dataset for vault usage vertical bar chart (manage in serializer?) + get monthlyUsage() { + return [ + { month: 'January', distinct_entities: 1000, non_entity_tokens: 322, total: 1322 }, + { month: 'February', distinct_entities: 1500, non_entity_tokens: 122, total: 1622 }, + { month: 'March', distinct_entities: 4300, non_entity_tokens: 700, total: 5000 }, + { month: 'April', distinct_entities: 1550, non_entity_tokens: 229, total: 1779 }, + { month: 'May', distinct_entities: 5560, non_entity_tokens: 124, total: 5684 }, + { month: 'June', distinct_entities: 1570, non_entity_tokens: 142, total: 1712 }, + { month: 'July', distinct_entities: 300, non_entity_tokens: 112, total: 412 }, + { month: 'August', distinct_entities: 1610, non_entity_tokens: 130, total: 1740 }, + { month: 'September', distinct_entities: 1900, non_entity_tokens: 222, total: 2122 }, + { month: 'October', distinct_entities: 500, non_entity_tokens: 166, total: 666 }, + { month: 'November', distinct_entities: 480, non_entity_tokens: 132, total: 612 }, + { month: 'December', distinct_entities: 980, non_entity_tokens: 202, total: 1182 }, + ]; + } + // Create namespaces data for csv format get getCsvData() { if (!this.args.model.activity || !this.args.model.activity.byNamespace) { diff --git a/ui/app/components/clients/horizontal-bar-charts.js b/ui/app/components/clients/horizontal-bar-chart.js similarity index 65% rename from ui/app/components/clients/horizontal-bar-charts.js rename to ui/app/components/clients/horizontal-bar-chart.js index 88f331b45a6..a042cddf17a 100644 --- a/ui/app/components/clients/horizontal-bar-charts.js +++ b/ui/app/components/clients/horizontal-bar-chart.js @@ -6,6 +6,8 @@ import { select, event, selectAll } from 'd3-selection'; import { scaleLinear, scaleBand } from 'd3-scale'; import { axisLeft } from 'd3-axis'; import { max, maxIndex } from 'd3-array'; +import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE } from '../../utils/chart-helpers'; +import { tracked } from '@glimmer/tracking'; /** * @module HorizontalBarChart @@ -30,74 +32,10 @@ const TRANSLATE = { down: 13 }; const CHAR_LIMIT = 15; // 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 BAR_COLOR_DEFAULT = ['#BFD4FF', '#1563FF']; -const BAR_COLOR_HOVER = ['#1563FF', '#0F4FD1']; -const BACKGROUND_BAR_COLOR = '#EBEEF2'; - -const SAMPLE_DATA = [ - { - label: 'longlongsuperlongnamespace80/', - non_entity_tokens: 1696, - distinct_entities: 1652, - total: 3348, - }, - { - label: 'namespace12/', - non_entity_tokens: 1568, - distinct_entities: 1663, - total: 3231, - }, - { - label: 'namespace44/', - non_entity_tokens: 1511, - distinct_entities: 1708, - total: 3219, - }, - { - label: 'namespace36/', - non_entity_tokens: 1574, - distinct_entities: 1553, - total: 3127, - }, - { - label: 'namespace2/', - non_entity_tokens: 1784, - distinct_entities: 1333, - total: 3117, - }, - { - label: 'namespace82/', - non_entity_tokens: 1245, - distinct_entities: 1702, - total: 2947, - }, - { - label: 'namespace28/', - non_entity_tokens: 1579, - distinct_entities: 1364, - total: 2943, - }, - { - label: 'namespace60/', - non_entity_tokens: 1962, - distinct_entities: 929, - total: 2891, - }, - { - label: 'namespace5/', - non_entity_tokens: 1448, - distinct_entities: 1418, - total: 2866, - }, - { - label: 'namespace67/', - non_entity_tokens: 1758, - distinct_entities: 1065, - total: 2823, - }, -]; export default class HorizontalBarChart extends Component { + @tracked tooltipTarget = ''; + @tracked tooltipText = ''; + get labelKey() { return this.args.labelKey || 'label'; } @@ -110,6 +48,10 @@ export default class HorizontalBarChart extends Component { return this.args.dataset[maxIndex(this.args.dataset, d => d.total)]; } + @action removeTooltip() { + this.tooltipTarget = null; + } + @action renderChart(element, args) { // chart legend tells stackFunction how to stack/organize data @@ -120,7 +62,6 @@ export default class HorizontalBarChart extends Component { // let dataset = SAMPLE_DATA; let stackedData = stackFunction(dataset); let labelKey = this.labelKey; - let handleClick = this.args.onClick; let xScale = scaleLinear() .domain([0, max(dataset.map(d => d.total))]) @@ -144,7 +85,7 @@ export default class HorizontalBarChart extends Component { .append('g') // shifts chart to accommodate y-axis legend .attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top})`) - .style('fill', (d, i) => BAR_COLOR_DEFAULT[i]); + .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})`)); @@ -184,7 +125,7 @@ export default class HorizontalBarChart extends Component { .attr('height', `${LINE_HEIGHT}px`) .attr('x', '0') .attr('y', chartData => yScale(chartData[labelKey])) - .style('fill', `${BACKGROUND_BAR_COLOR}`) + .style('fill', `${GREY}`) .style('opacity', '0') .style('mix-blend-mode', 'multiply'); @@ -204,59 +145,45 @@ export default class HorizontalBarChart extends Component { 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}`); + select(elementA).attr(`${attr}`) === select(elementB).attr(`${attr}`); - // MOUSE AND CLICK EVENTS FOR DATA BARS + // MOUSE EVENTS FOR DATA BARS actionBars - .on('click', function(chartData) { - if (handleClick) { - handleClick(chartData); - } - }) - .on('mouseover', function() { - select(this).style('opacity', 1); + .on('mouseover', data => { + let hoveredElement = actionBars.filter(bar => bar.label === data.label).node(); + this.tooltipTarget = hoveredElement; + this.tooltipText = `${Math.round((data.total * 100) / 19000)}% of total client counts: + ${data.non_entity_tokens} non-entity tokens, ${data.distinct_entities} unique entities.`; + + select(hoveredElement).style('opacity', 1); + dataBars .filter(function() { - return compareAttributes(this, event.target, 'y'); + return compareAttributes(this, hoveredElement, 'y'); }) .style('fill', (b, i) => `${BAR_COLOR_HOVER[i]}`); - // TODO: change to use modal instead of tooltip div - 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) => `${BAR_COLOR_DEFAULT[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) / 19000)}% of total client counts: - ${chartData.non_entity_tokens} non-entity tokens, ${chartData.distinct_entities} unique entities. - ` - ); + .style('fill', (b, i) => `${LIGHT_AND_DARK_BLUE[i]}`); }); // MOUSE EVENTS FOR Y-AXIS LABELS yLegendBars - .on('click', function(chartData) { - if (handleClick) { - handleClick(chartData); + .on('mouseover', data => { + if (data.label.length >= CHAR_LIMIT) { + let hoveredElement = yLegendBars.filter(bar => bar.label === data.label).node(); + this.tooltipTarget = hoveredElement; + this.tooltipText = data.label; + } else { + this.tooltipTarget = null; } - }) - .on('mouseover', function(chartData) { dataBars .filter(function() { return compareAttributes(this, event.target, 'y'); @@ -267,36 +194,19 @@ export default class HorizontalBarChart extends Component { 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); + this.tooltipTarget = null; dataBars .filter(function() { return compareAttributes(this, event.target, 'y'); }) - .style('fill', (b, i) => `${BAR_COLOR_DEFAULT[i]}`); + .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); - } }); // add client count total values to the right diff --git a/ui/app/components/clients/line-chart.js b/ui/app/components/clients/line-chart.js new file mode 100644 index 00000000000..4fd30a9b66d --- /dev/null +++ b/ui/app/components/clients/line-chart.js @@ -0,0 +1,124 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { max } from 'd3-array'; +// eslint-disable-next-line no-unused-vars +import { select, selectAll, node } from 'd3-selection'; +import { axisLeft, axisBottom } from 'd3-axis'; +import { scaleLinear, scalePoint } from 'd3-scale'; +import { line } from 'd3-shape'; +import { LIGHT_AND_DARK_BLUE, SVG_DIMENSIONS, formatNumbers } from '../../utils/chart-helpers'; + +// TODO fill out below +/** + * @module LineChart + * LineChart components are used to... + * + * @example + * ```js + * + * ``` + * @param {object} requiredParam - requiredParam is... + * @param {string} [optionalParam] - optionalParam is... + * @param {string} [param1=defaultValue] - param1 is... + */ + +export default class LineChart extends Component { + // TODO make just one tracked variable tooltipText? + @tracked tooltipTarget = ''; + @tracked tooltipMonth = ''; + @tracked tooltipTotal = ''; + @tracked tooltipNew = ''; + + @action removeTooltip() { + this.tooltipTarget = null; + } + + @action + renderChart(element, args) { + let dataset = args[0]; + let chartSvg = select(element); + chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions + + // DEFINE AXES SCALES + let yScale = scaleLinear() + .domain([0, max(dataset.map(d => d.clients))]) + .range([0, 100]); + + let yAxisScale = scaleLinear() + .domain([0, max(dataset.map(d => d.clients))]) // TODO will need to recalculate when you get the data + .range([SVG_DIMENSIONS.height, 0]); + + let xScale = scalePoint() // use scaleTime()? + .domain(dataset.map(d => d.month)) + .range([0, SVG_DIMENSIONS.width]) + .padding(0.2); + + // CUSTOMIZE AND APPEND AXES + let yAxis = axisLeft(yAxisScale) + .ticks(7) + .tickPadding(10) + .tickSizeInner(-SVG_DIMENSIONS.width) // makes grid lines length of svg + .tickFormat(formatNumbers); + + let xAxis = axisBottom(xScale).tickSize(0); + + yAxis(chartSvg.append('g')); + xAxis(chartSvg.append('g').attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`)); + + chartSvg.selectAll('.domain').remove(); + + // PATH BETWEEN PLOT POINTS + let lineGenerator = line() + .x(d => xScale(d.month)) + .y(d => yAxisScale(d.clients)); + + chartSvg + .append('g') + .append('path') + .attr('fill', 'none') + .attr('stroke', LIGHT_AND_DARK_BLUE[1]) + .attr('stroke-width', 0.5) + .attr('d', lineGenerator(dataset)); + + // LINE PLOTS (CIRCLES) + chartSvg + .append('g') + .selectAll('circle') + .data(dataset) + .enter() + .append('circle') + .attr('class', 'data-plot') + .attr('cy', d => `${100 - yScale(d.clients)}%`) + .attr('cx', d => xScale(d.month)) + .attr('r', 3.5) + .attr('fill', LIGHT_AND_DARK_BLUE[0]) + .attr('stroke', LIGHT_AND_DARK_BLUE[1]) + .attr('stroke-width', 1.5); + + // LARGER HOVER CIRCLES + chartSvg + .append('g') + .selectAll('circle') + .data(dataset) + .enter() + .append('circle') + .attr('class', 'hover-circle') + .style('cursor', 'pointer') + .style('opacity', '0') + .attr('cy', d => `${100 - yScale(d.clients)}%`) + .attr('cx', d => xScale(d.month)) + .attr('r', 10); + + let hoverCircles = chartSvg.selectAll('.hover-circle'); + + // MOUSE EVENT FOR TOOLTIP + hoverCircles.on('mouseover', data => { + this.tooltipMonth = data.month; + this.tooltipTotal = `${data.clients} total clients`; + this.tooltipNew = `${data.new} new clients`; + let node = hoverCircles.filter(plot => plot.month === data.month).node(); + this.tooltipTarget = node; + }); + } +} diff --git a/ui/app/components/clients/monthly-usage.js b/ui/app/components/clients/monthly-usage.js new file mode 100644 index 00000000000..350d1ff2a3b --- /dev/null +++ b/ui/app/components/clients/monthly-usage.js @@ -0,0 +1,16 @@ +import Component from '@glimmer/component'; + +/** + * TODO fill out + * @module MonthlyUsage + * MonthlyUsage components are used to... + * + * @example + * ```js + * + * ``` + * @param {object} requiredParam - requiredParam is... + * @param {string} [optionalParam] - optionalParam is... + * @param {string} [param1=defaultValue] - param1 is... + */ +export default class MonthlyUsage extends Component {} diff --git a/ui/app/components/clients/running-total.js b/ui/app/components/clients/running-total.js new file mode 100644 index 00000000000..1828efb343a --- /dev/null +++ b/ui/app/components/clients/running-total.js @@ -0,0 +1,15 @@ +import Component from '@glimmer/component'; + +/** + * @module RunningTotal + * RunningTotal components are used to... + * + * @example + * ```js + * + * ``` + * @param {object} requiredParam - requiredParam is... + * @param {string} [optionalParam] - optionalParam is... + * @param {string} [param1=defaultValue] - param1 is... + */ +export default class RunningTotal extends Component {} diff --git a/ui/app/components/clients/total-client-usage.js b/ui/app/components/clients/vertical-bar-chart.js similarity index 54% rename from ui/app/components/clients/total-client-usage.js rename to ui/app/components/clients/vertical-bar-chart.js index e1c137cc1ef..e9b62f47d87 100644 --- a/ui/app/components/clients/total-client-usage.js +++ b/ui/app/components/clients/vertical-bar-chart.js @@ -6,81 +6,67 @@ import { max } from 'd3-array'; import { select, selectAll, node } from 'd3-selection'; import { axisLeft, axisBottom } from 'd3-axis'; import { scaleLinear, scaleBand } from 'd3-scale'; -import { format } from 'd3-format'; import { stack } from 'd3-shape'; - +import { + GREY, + LIGHT_AND_DARK_BLUE, + SVG_DIMENSIONS, + TRANSLATE, + formatNumbers, +} from '../../utils/chart-helpers'; + +// TODO fill out below /** - * ARG TODO fill out - * @module TotalClientUsage - * TotalClientUsage components are used to... + * @module VerticalBarChart + * VerticalBarChart components are used to... * * @example * ```js - * + * * ``` * @param {object} requiredParam - requiredParam is... * @param {string} [optionalParam] - optionalParam is... * @param {string} [param1=defaultValue] - param1 is... */ -// ARG TODO pull in data -const DATA = [ - { month: 'January', directEntities: 1000, nonEntityTokens: 322, total: 1322 }, - { month: 'February', directEntities: 1500, nonEntityTokens: 122, total: 1622 }, - { month: 'March', directEntities: 4300, nonEntityTokens: 700, total: 5000 }, - { month: 'April', directEntities: 1550, nonEntityTokens: 229, total: 1779 }, - { month: 'May', directEntities: 5560, nonEntityTokens: 124, total: 5684 }, - { month: 'June', directEntities: 1570, nonEntityTokens: 142, total: 1712 }, - { month: 'July', directEntities: 300, nonEntityTokens: 112, total: 412 }, - { month: 'August', directEntities: 1610, nonEntityTokens: 130, total: 1740 }, - { month: 'September', directEntities: 1900, nonEntityTokens: 222, total: 2122 }, - { month: 'October', directEntities: 500, nonEntityTokens: 166, total: 666 }, - { month: 'November', directEntities: 480, nonEntityTokens: 132, total: 612 }, - { month: 'December', directEntities: 980, nonEntityTokens: 202, total: 1182 }, -]; - -// COLOR THEME: -const BAR_COLOR_DEFAULT = ['#8AB1FF', '#1563FF']; -const BACKGROUND_BAR_COLOR = '#EBEEF2'; - -// TRANSLATIONS: -const TRANSLATE = { left: -11 }; -const SVG_DIMENSIONS = { height: 190, width: 500 }; - -export default class TotalClientUsage extends Component { +export default class VerticalBarChart extends Component { @tracked tooltipTarget = ''; - @tracked hoveredLabel = ''; - @tracked trackingTest = 0; + @tracked tooltipTotal = ''; + @tracked uniqueEntities = ''; + @tracked nonEntityTokens = ''; - @action - registerListener(element) { - // Define the chart - let dataset = DATA; // will be data passed in as argument + get chartLegend() { + return this.args.chartLegend; + } - let stackFunction = stack().keys(['directEntities', 'nonEntityTokens']); + @action + registerListener(element, args) { + let dataset = args[0]; + // TODO pull out lines 44 - scales into helper? b/c same as line chart? + let stackFunction = stack().keys(this.chartLegend.map(l => l.key)); let stackedData = stackFunction(dataset); + let chartSvg = select(element); + chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions + // DEFINE DATA BAR SCALES let yScale = scaleLinear() .domain([0, max(dataset.map(d => d.total))]) // TODO will need to recalculate when you get the data - .range([0, 100]); + .range([0, 100]) + .nice(); let xScale = scaleBand() .domain(dataset.map(d => d.month)) .range([0, SVG_DIMENSIONS.width]) // set width to fix number of pixels .paddingInner(0.85); - let chartSvg = select(element); - - chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions - - let groups = chartSvg + let dataBars = chartSvg .selectAll('g') .data(stackedData) .enter() .append('g') - .style('fill', (d, i) => BAR_COLOR_DEFAULT[i]); + .style('fill', (d, i) => LIGHT_AND_DARK_BLUE[i]); - groups + dataBars .selectAll('rect') .data(stackedData => stackedData) .enter() @@ -94,33 +80,27 @@ export default class TotalClientUsage extends Component { // MAKE AXES // let yAxisScale = scaleLinear() .domain([0, max(dataset.map(d => d.total))]) // TODO will need to recalculate when you get the data - .range([`${SVG_DIMENSIONS.height}`, 0]); - - // Reference for tickFormat https://www.youtube.com/watch?v=c3MCROTNN8g - let formatNumbers = number => format('.2s')(number).replace('G', 'B'); // for billions to replace G with B. + .range([`${SVG_DIMENSIONS.height}`, 0]) + .nice(); - // customize y-axis let yAxis = axisLeft(yAxisScale) .ticks(7) .tickPadding(10) - .tickSizeInner(-SVG_DIMENSIONS.width) // makes grid lines correct length + .tickSizeInner(-SVG_DIMENSIONS.width) .tickFormat(formatNumbers); - yAxis(chartSvg.append('g')); - - // customize x-axis - let xAxisGenerator = axisBottom(xScale).tickSize(0); - let xAxis = chartSvg.append('g').call(xAxisGenerator); + let xAxis = axisBottom(xScale).tickSize(0); - xAxis.attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`); + yAxis(chartSvg.append('g')); + xAxis(chartSvg.append('g').attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`)); chartSvg.selectAll('.domain').remove(); // remove domain lines - // creating wider area for tooltip hover + // WIDER SELECTION AREA FOR TOOLTIP HOVER let greyBars = chartSvg .append('g') .attr('transform', `translate(${TRANSLATE.left})`) - .style('fill', `${BACKGROUND_BAR_COLOR}`) + .style('fill', `${GREY}`) .style('opacity', '0') .style('mix-blend-mode', 'multiply'); @@ -136,9 +116,12 @@ export default class TotalClientUsage extends Component { .attr('y', '0') // start at bottom .attr('x', data => xScale(data.month)); // not data.data because this is not stacked data - // for tooltip + // MOUSE EVENT FOR TOOLTIP tooltipRect.on('mouseover', data => { - this.hoveredLabel = data.month; + let hoveredMonth = data.month; + this.tooltipTotal = `${data.total} total clients`; + this.uniqueEntities = `${data.distinct_entities} unique entities`; + this.nonEntityTokens = `${data.non_entity_tokens} non-entity tokens`; // let node = chartSvg // .selectAll('rect.tooltip-rect') // .filter(data => data.month === this.hoveredLabel) @@ -146,7 +129,7 @@ export default class TotalClientUsage extends Component { let node = chartSvg .selectAll('rect.data-bar') // filter for the top data bar (so y-coord !== 0) with matching month - .filter(data => data[0] !== 0 && data.data.month === this.hoveredLabel) + .filter(data => data[0] !== 0 && data.data.month === hoveredMonth) .node(); this.tooltipTarget = node; // grab the node from the list of rects }); diff --git a/ui/app/styles/core/charts.scss b/ui/app/styles/core/charts.scss index c1d17b47504..6163608a028 100644 --- a/ui/app/styles/core/charts.scss +++ b/ui/app/styles/core/charts.scss @@ -6,6 +6,13 @@ } // GRID LAYOUT // +.stacked-charts { + display: grid; + width: 100%; + // grid-template-columns: 1fr; + // grid-template-rows: 1fr; +} + .single-chart-grid { display: grid; grid-template-columns: 1fr 0.3fr 3.7fr; @@ -197,9 +204,28 @@ p.data-details { .chart-tooltip { background-color: $ui-gray-700; color: white; - font-size: 0.929rem; - padding: 10px; - border-radius: 4px; + font-size: $size-9; + padding: 6px; + border-radius: $radius-large; + + .bold { + font-weight: $font-weight-bold; + } + + .line-chart { + width: 117px; + } + + .vertical-chart { + text-align: center; + flex-wrap: nowrap; + width: fit-content; + } + + .horizontal-chart { + width: 200px; + padding: $spacing-s; + } } .chart-tooltip-arrow { diff --git a/ui/app/templates/components/clients/attribution.hbs b/ui/app/templates/components/clients/attribution.hbs new file mode 100644 index 00000000000..8a7641639a8 --- /dev/null +++ b/ui/app/templates/components/clients/attribution.hbs @@ -0,0 +1,41 @@ +
+
+
+

{{@title}}

+

{{this.chartText.description}}

+
+
+ {{#if @totalClientsData}} + {{yield}} + {{/if}} +
+
+ +
+

New clients

+

{{this.chartText.newCopy}}

+ +
+ +
+

Total clients

+

{{this.chartText.totalCopy}}

+ +
+ +
+ Updated Nov 15 2021, 4:07:32 pm +
+ +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+
+ diff --git a/ui/app/templates/components/clients/dashboard.hbs b/ui/app/templates/components/clients/dashboard.hbs index 587c9c42c7e..40d8090a24e 100644 --- a/ui/app/templates/components/clients/dashboard.hbs +++ b/ui/app/templates/components/clients/dashboard.hbs @@ -136,25 +136,30 @@ {{!-- ARG TODO end of part that goes to Running Client --}} {{#if this.showGraphs}} {{!-- ARG TODO chart playground --}} - + + - Export attribution data - + + - + + +{{#if this.tooltipTarget}} +{{!-- Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 --}} +{{!-- Component must be in curly bracket notation --}} + {{#modal-dialog + tagName='div' + tetherTarget=this.tooltipTarget + targetAttachment='bottom middle' + attachment='bottom middle' + offset='35px 0' + }} +
+

{{this.tooltipText}}

+
+
+ {{/modal-dialog}} +{{/if}} diff --git a/ui/app/templates/components/clients/horizontal-bar-charts.hbs b/ui/app/templates/components/clients/horizontal-bar-charts.hbs deleted file mode 100644 index 88e6ebef83a..00000000000 --- a/ui/app/templates/components/clients/horizontal-bar-charts.hbs +++ /dev/null @@ -1,43 +0,0 @@ -
-
-
-

{{@title}}

- {{#if @description}} -

{{@description}}

- {{/if}} -
-
- {{#if @dataset}} - {{yield}} - {{/if}} -
-
- -
-

New Clients

-

{{@newClientsDescription}}

- - -
- -
-

Total monthly clients

-

{{@totalDescription}}

- - -
- -
- Updated Nov 15 2021, 4:07:32 pm -
- -
- {{@chartLegend.0.label}} - {{@chartLegend.1.label}} -
-
- diff --git a/ui/app/templates/components/clients/line-chart.hbs b/ui/app/templates/components/clients/line-chart.hbs new file mode 100644 index 00000000000..63942aa9921 --- /dev/null +++ b/ui/app/templates/components/clients/line-chart.hbs @@ -0,0 +1,24 @@ + + + +{{!-- TOOLTIP --}} + +{{#if this.tooltipTarget}} +{{!-- Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 --}} +{{!-- Component must be in curly bracket notation --}} + {{#modal-dialog + tagName='div' + tetherTarget=this.tooltipTarget + targetAttachment='bottom middle' + attachment='bottom middle' + offset='35px 0' + }} +
+

{{this.tooltipMonth}}

+

{{this.tooltipTotal}}

+

{{this.tooltipNew}}

+
+
+ {{/modal-dialog}} +{{/if}} diff --git a/ui/app/templates/components/clients/total-client-usage.hbs b/ui/app/templates/components/clients/monthly-usage.hbs similarity index 50% rename from ui/app/templates/components/clients/total-client-usage.hbs rename to ui/app/templates/components/clients/monthly-usage.hbs index 4940d848a08..62ade88e134 100644 --- a/ui/app/templates/components/clients/total-client-usage.hbs +++ b/ui/app/templates/components/clients/monthly-usage.hbs @@ -5,10 +5,12 @@

{{@description}}

{{/if}}
-
- + +
+
@@ -31,24 +33,8 @@
- {{@chartLegend.0.label}} - {{@chartLegend.1.label}} + {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}}
- {{!-- TOOLTIP --}} - - {{#if this.tooltipTarget}} - {{!-- Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 --}} - {{!-- Component must be in curly bracket notation --}} - {{#modal-dialog - tagName='div' - tetherTarget=this.tooltipTarget - targetAttachment='bottom middle' - attachment='bottom middle' - offset='40px 0' - }} -

{{this.hoveredLabel}}

-
- {{/modal-dialog}} - {{/if}}
diff --git a/ui/app/templates/components/clients/running-total.hbs b/ui/app/templates/components/clients/running-total.hbs new file mode 100644 index 00000000000..d1b76f2b6d8 --- /dev/null +++ b/ui/app/templates/components/clients/running-total.hbs @@ -0,0 +1,65 @@ +
+
+
+

{{@title}}

+

{{@description}}

+
+ +
+ +
+ +
+

Running client total

+

The number of clients which interacted with Vault during this date range.

+
+ +
+

{{capitalize @chartLegend.0.label}}

+

1,307

+
+ +
+

{{capitalize @chartLegend.1.label}}

+

8,005

+
+ +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+
+ +
+
+ +
+ +
+

New monthly clients

+

Clients which interacted with Vault for the first time during this date range, displayed per month.

+
+ +
+

Average new {{@chartLegend.0.label}} per month

+

{{@dataOneData}}

+
+ +
+

Average new {{@chartLegend.1.label}} per month

+

{{@dataTwoData}}

+
+ +
+ Updated Nov 15 2021, 4:07:32 pm +
+ +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+
+
diff --git a/ui/app/templates/components/clients/vertical-bar-chart.hbs b/ui/app/templates/components/clients/vertical-bar-chart.hbs new file mode 100644 index 00000000000..ac63496011b --- /dev/null +++ b/ui/app/templates/components/clients/vertical-bar-chart.hbs @@ -0,0 +1,24 @@ + + + +{{!-- TOOLTIP --}} + +{{#if this.tooltipTarget}} +{{!-- Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 --}} +{{!-- Component must be in curly bracket notation --}} + {{#modal-dialog + tagName='div' + tetherTarget=this.tooltipTarget + targetAttachment='bottom middle' + attachment='bottom middle' + offset='40px 0' + }} +
+

{{this.tooltipTotal}}

+

{{this.uniqueEntities}}

+

{{this.nonEntityTokens}}

+
+
+ {{/modal-dialog}} +{{/if}} diff --git a/ui/app/utils/chart-helpers.js b/ui/app/utils/chart-helpers.js new file mode 100644 index 00000000000..bf4b70ab732 --- /dev/null +++ b/ui/app/utils/chart-helpers.js @@ -0,0 +1,16 @@ +import { format } from 'd3-format'; + +// COLOR THEME: +export const LIGHT_AND_DARK_BLUE = ['#BFD4FF', '#1563FF']; +export const BAR_COLOR_HOVER = ['#1563FF', '#0F4FD1']; +export const GREY = '#EBEEF2'; + +// TRANSLATIONS: +export const TRANSLATE = { left: -11 }; +export const SVG_DIMENSIONS = { height: 190, width: 500 }; + +// Reference for tickFormat https://www.youtube.com/watch?v=c3MCROTNN8g +export function formatNumbers(number) { + // replace SI prefix of 'G' for billions to 'B' + return format('.1s')(number).replace('G', 'B'); +} diff --git a/ui/lib/core/addon/components/bar-chart.js b/ui/lib/core/addon/components/bar-chart.js index aba5c929b0d..0ac33266db1 100644 --- a/ui/lib/core/addon/components/bar-chart.js +++ b/ui/lib/core/addon/components/bar-chart.js @@ -47,11 +47,10 @@ const CHAR_LIMIT = 18; // character count limit for y-axis labels to trigger tru const LINE_HEIGHT = 24; // each bar w/ padding is 24 pixels thick // COLOR THEME: -const BAR_COLOR_DEFAULT = ['#BFD4FF', '#8AB1FF']; +const LIGHT_AND_DARK_BLUE = ['#BFD4FF', '#8AB1FF']; const BAR_COLOR_HOVER = ['#1563FF', '#0F4FD1']; -const BACKGROUND_BAR_COLOR = '#EBEEF2'; const TOOLTIP_BACKGROUND = '#525761'; - +const GREY = '#EBEEF2'; class BarChartComponent extends Component { get labelKey() { return this.args.labelKey || 'label'; @@ -121,7 +120,7 @@ class BarChartComponent extends Component { .append('g') // shifts chart to accommodate y-axis legend .attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top})`) - .style('fill', (d, i) => BAR_COLOR_DEFAULT[i]); + .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})`)); @@ -159,7 +158,7 @@ class BarChartComponent extends Component { .attr('height', `${LINE_HEIGHT}px`) .attr('x', '0') .attr('y', chartData => yScale(chartData[labelKey])) - .style('fill', `${BACKGROUND_BAR_COLOR}`) + .style('fill', `${GREY}`) .style('opacity', '0') .style('mix-blend-mode', 'multiply'); @@ -208,7 +207,7 @@ class BarChartComponent extends Component { .filter(function() { return compareAttributes(this, event.target, 'y'); }) - .style('fill', (b, i) => `${BAR_COLOR_DEFAULT[i]}`); + .style('fill', (b, i) => `${LIGHT_AND_DARK_BLUE[i]}`); }) .on('mousemove', function(chartData) { select('.chart-tooltip') @@ -254,7 +253,7 @@ class BarChartComponent extends Component { .filter(function() { return compareAttributes(this, event.target, 'y'); }) - .style('fill', (b, i) => `${BAR_COLOR_DEFAULT[i]}`); + .style('fill', (b, i) => `${LIGHT_AND_DARK_BLUE[i]}`); actionBarSelection .filter(function() { return compareAttributes(this, event.target, 'y'); @@ -302,7 +301,7 @@ class BarChartComponent extends Component { .attr('cx', `${xCoordinate}%`) .attr('cy', '50%') .attr('r', 6) - .style('fill', `${BAR_COLOR_DEFAULT[i]}`); + .style('fill', `${LIGHT_AND_DARK_BLUE[i]}`); legendSvg .append('text') .attr('x', `${xCoordinate + 2}%`) diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 2e4124ce422..f917f750dc3 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -31,6 +31,284 @@ export default function() { // }; // }); + this.get('/sys/internal/counters/activity', function() { + return { + data: { + start_time: '2019-11-01T00:00:00Z', + end_time: '2020-10-31T23:59:59Z', + total: { + distinct_entities: 200, + non_entity_tokens: 100, + clients: 300, + }, + by_namespace: [ + { + _comment: 'by_namespace will remain as it is', + }, + ], + months: [ + { + 'jan/2022': { + counts: { + distinct_entities: 100, + non_entity_tokens: 50, + clients: 150, + }, + namespaces: [ + { + id: 'root', + path: '', + counts: { + distinct_entities: 50, + non_entity_tokens: 25, + clients: 75, + }, + mounts: [ + { + path: 'auth/aws/login', + counts: { + distinct_entities: 25, + non_entity_tokens: 12, + clients: 37, + }, + }, + { + path: 'auth/approle/login', + counts: { + distinct_entities: 25, + non_entity_tokens: 13, + clients: 38, + }, + }, + ], + }, + { + namespace_id: 'ns1', + namespace_path: '', + counts: { + distinct_entities: 50, + non_entity_tokens: 25, + clients: 75, + }, + mounts: [ + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 20, + non_entity_tokens: 10, + clients: 30, + }, + }, + { + mount_path: 'auth/approle/login', + counts: { + distinct_entities: 30, + non_entity_tokens: 15, + clients: 45, + }, + }, + ], + }, + ], + new: { + counts: { + distinct_entities: 30, + non_entity_tokens: 10, + clients: 40, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + distinct_entities: 15, + non_entity_tokens: 5, + clients: 20, + }, + mounts: [ + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 5, + non_entity_tokens: 2, + clients: 7, + }, + }, + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 10, + non_entity_tokens: 3, + clients: 13, + }, + }, + ], + }, + { + namespace_id: 'ns1', + namespace_path: '', + counts: { + distinct_entities: 15, + non_entity_tokens: 5, + clients: 20, + }, + mounts: [ + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 10, + non_entity_tokens: 1, + clients: 11, + }, + }, + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 5, + non_entity_tokens: 4, + clients: 9, + }, + }, + ], + }, + ], + }, + }, + }, + { + 'feb/2022': { + counts: { + _comment: 'total monthly clients', + distinct_entities: 100, + non_entity_tokens: 50, + clients: 150, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + distinct_entities: 60, + non_entity_tokens: 10, + clients: 70, + }, + mounts: [ + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 30, + non_entity_tokens: 5, + clients: 35, + }, + }, + { + mount_path: 'auth/approle/login', + counts: { + distinct_entities: 30, + non_entity_tokens: 5, + clients: 35, + }, + }, + ], + }, + { + namespace_id: 'ns1', + namespace_path: '', + counts: { + distinct_entities: 40, + non_entity_tokens: 40, + clients: 80, + }, + mounts: [ + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 20, + non_entity_tokens: 20, + clients: 40, + }, + }, + { + mount_path: 'auth/approle/login', + counts: { + distinct_entities: 20, + non_entity_tokens: 20, + clients: 40, + }, + }, + ], + }, + ], + new: { + counts: { + distinct_entities: 20, + non_entity_tokens: 5, + clients: 25, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + distinct_entities: 10, + non_entity_tokens: 3, + clients: 13, + }, + mounts: [ + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 5, + non_entity_tokens: 1, + clients: 6, + }, + }, + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 5, + non_entity_tokens: 2, + clients: 7, + }, + }, + ], + }, + { + namespace_id: 'ns1', + namespace_path: '', + counts: { + distinct_entities: 10, + non_entity_tokens: 2, + clients: 12, + }, + mounts: [ + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 5, + non_entity_tokens: 2, + clients: 7, + }, + }, + { + mount_path: 'auth/aws/login', + counts: { + distinct_entities: 5, + non_entity_tokens: 0, + clients: 5, + }, + }, + ], + }, + ], + }, + }, + }, + ], + }, + }; + }); + this.get('/sys/internal/counters/activity/monthly', function() { return { data: {