/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ import Component from '@glimmer/component'; import { SVG_DIMENSIONS, formatNumbers } from 'vault/utils/chart-helpers'; import { parseAPITimestamp } from 'core/utils/date-formatters'; import { format, isValid } from 'date-fns'; import { debug } from '@ember/debug'; import type { Count, MonthlyChartData, Timestamp } from 'vault/vault/charts/client-counts'; import type ClientsVersionHistoryModel from 'vault/models/clients/version-history'; interface Args { dataset: MonthlyChartData[]; upgradeData: ClientsVersionHistoryModel[]; xKey?: string; yKey?: string; chartHeight?: number; } interface ChartData { x: Date; y: number | null; new: number; tooltipUpgrade: string | null; month: string; // used for test selectors and to match key on upgradeData } interface UpgradeByMonth { [key: string]: ClientsVersionHistoryModel; } /** * @module LineChart * LineChart components are used to display time-based data in a line plot with accompanying tooltip * * @example * ```js * * ``` * @param {array} dataset - array of objects containing data to be plotted * @param {string} [xKey=clients] - string denoting key for x-axis data of dataset. Should reference a timestamp string. * @param {string} [yKey=timestamp] - string denoting key for y-axis data of dataset. Should reference a number or null. * @param {array} [upgradeData=null] - array of objects containing version history from the /version-history endpoint * @param {number} [chartHeight=190] - height of chart in pixels */ export default class LineChart extends Component { // Chart settings get yKey() { return this.args.yKey || 'clients'; } get xKey() { return this.args.xKey || 'timestamp'; } get chartHeight() { return this.args.chartHeight || SVG_DIMENSIONS.height; } // Plot points get data(): ChartData[] { try { return this.args.dataset?.map((datum) => { const timestamp = parseAPITimestamp(datum[this.xKey as keyof Timestamp]) as Date; if (isValid(timestamp) === false) throw new Error(`Unable to parse value "${datum[this.xKey as keyof Timestamp]}" as date`); const upgradeMessage = this.getUpgradeMessage(datum); return { x: timestamp, y: (datum[this.yKey as keyof Count] as number) ?? null, new: this.getNewClients(datum), tooltipUpgrade: upgradeMessage, month: datum.month, }; }); } catch (e) { debug(e as string); return []; } } get upgradedMonths() { return this.data.filter((datum) => datum.tooltipUpgrade); } // Domains get yDomain() { const counts: number[] = this.data .map((d) => d.y) .flatMap((num) => (typeof num === 'number' ? [num] : [])); const max = Math.max(...counts); // if max is <=4, hardcode 4 which is the y-axis tickCount so y-axes are not decimals return [0, max <= 4 ? 4 : max]; } get timeDomain() { // assume data is sorted by time const firstTime = this.data[0]?.x; const lastTime = this.data[this.data.length - 1]?.x; return [firstTime, lastTime]; } get upgradeByMonthYear(): UpgradeByMonth { const empty: UpgradeByMonth = {}; if (!Array.isArray(this.args.upgradeData)) return empty; return ( this.args.upgradeData?.reduce((acc, upgrade) => { if (upgrade.timestampInstalled) { const key = parseAPITimestamp(upgrade.timestampInstalled, 'M/yy'); acc[key as string] = upgrade; } return acc; }, empty) || empty ); } getUpgradeMessage(datum: MonthlyChartData) { const upgradeInfo = this.upgradeByMonthYear[datum.month as string]; if (upgradeInfo) { const { version, previousVersion } = upgradeInfo; return `Vault was upgraded ${previousVersion ? 'from ' + previousVersion : ''} to ${version}`; } return null; } getNewClients(datum: MonthlyChartData) { if (!datum?.new_clients) return 0; return (datum?.new_clients[this.yKey as keyof Count] as number) || 0; } hasValue = (count: number | null) => { return typeof count === 'number' ? true : false; }; // These functions are used by the tooltip formatCount = (count: number) => { return formatNumbers([count]); }; formatMonth = (date: Date) => { return format(date, 'M/yy'); }; tooltipX = (original: number) => { return original.toString(); }; tooltipY = (original: number) => { return `${this.chartHeight - original + 15}`; }; }