diff --git a/changelog/31393.txt b/changelog/31393.txt new file mode 100644 index 0000000000..62cee05fd7 --- /dev/null +++ b/changelog/31393.txt @@ -0,0 +1,3 @@ +```release-note:change +ui/client-counts: removes tabs for each client count type and adds split view for counts per type in overview stacked bar chart +``` diff --git a/ui/app/components/clients/chart-container.hbs b/ui/app/components/clients/chart-container.hbs index e05f688e75..132a64447d 100644 --- a/ui/app/components/clients/chart-container.hbs +++ b/ui/app/components/clients/chart-container.hbs @@ -3,49 +3,43 @@ SPDX-License-Identifier: BUSL-1.1 }} -{{! HBS display template for rendering title, description and stat boxes with a chart on the right }} +{{! Template displays a title and description above a flex row with a 1/3-width left element and +a 2/3-width right element. If a @legend arg exists, it renders centered below the right element }} -
-
+ +

{{@title}}

-

- {{@description}} -

+ {{#if @description}} +

{{@description}}

+ {{/if}} + {{#if (has-block "customSubheader")}} + {{yield to="customSubheader"}} + {{/if}}
- {{#if @hasChartData}} - {{#if (has-block "subTitle")}} -
- {{yield to="subTitle"}} -
- {{/if}} - - {{#if (has-block "stats")}} - {{yield to="stats"}} - {{/if}} - - {{#if (has-block "chart")}} -
- {{yield to="chart"}} -
- {{/if}} - - {{#if (has-block "legend")}} -
- {{yield to="legend"}} -
- {{else if @legend}} -
- {{#each @legend as |legend idx|}} - - {{capitalize legend.label}} - {{/each}} -
- {{/if}} - - {{else}} -
- {{yield to="emptyState"}} +
+
+ {{#if (has-block "dataLeft")}} + {{yield to="dataLeft"}} + {{/if}}
- {{/if}} -
\ No newline at end of file + +
+ {{#if (has-block "dataRight")}} + {{yield to="dataRight"}} + {{/if}} + + {{#if @legend}} +
+ {{#each @legend as |l|}} +
+ + {{capitalize l.label}} +
+ {{/each}} +
+ {{/if}} + +
+
+
\ No newline at end of file diff --git a/ui/app/components/clients/charts/vertical-bar-basic.hbs b/ui/app/components/clients/charts/vertical-bar-basic.hbs index 93779b33b4..06bff4181c 100644 --- a/ui/app/components/clients/charts/vertical-bar-basic.hbs +++ b/ui/app/components/clients/charts/vertical-bar-basic.hbs @@ -73,7 +73,7 @@ {{#if this.activeDatum}}
- - {{#let - (scale-band domain=this.xBounds range=(array 0 width) padding=0.1) - (scale-linear range=(array this.chartHeight 0) domain=this.yBounds) - (scale-linear range=(array 0 this.chartHeight) domain=this.yBounds) - as |xScale yScale hScale| - }} - - {{@chartTitle}} - - {{#if (and xScale.isValid yScale.isValid)}} - - - {{/if}} - - {{#each @legend as |l idx|}} - - {{/each}} - - {{! TOOLTIP target rectangles }} - {{#if (and xScale.isValid yScale.isValid)}} - {{#each this.aggregatedData as |d|}} - - {{/each}} - {{/if}} - - - {{#if this.activeDatum}} -
-
-

{{this.activeDatum.legendX}}

- {{#each this.activeDatum.legendY as |stat|}} -

{{stat}}

- {{/each}} - {{#if this.activeDatum.tooltipUpgrade}} -
-

{{this.activeDatum.tooltipUpgrade}}

- {{/if}} -
-
-
- {{/if}} - {{/let}} -
-
- - {{#if @showTable}} -
- {{@chartTitle}} data - - <:head as |H|> - - Timestamp - {{#each this.dataKeys as |key|}} - {{humanize key}} - {{/each}} - - - <:body as |B|> - {{#each @data as |row|}} - - {{row.timestamp}} - {{#each this.dataKeys as |key|}} - {{or (get row key) "-"}} - {{/each}} - - {{/each}} - - -
- {{/if}} -{{else}} - -{{/if}} \ No newline at end of file diff --git a/ui/app/components/clients/charts/vertical-bar-grouped.ts b/ui/app/components/clients/charts/vertical-bar-grouped.ts deleted file mode 100644 index 0c3022652b..0000000000 --- a/ui/app/components/clients/charts/vertical-bar-grouped.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { BAR_WIDTH, numericalAxisLabel } from 'vault/utils/chart-helpers'; -import { formatNumber } from 'core/helpers/format-number'; -import { parseAPITimestamp } from 'core/utils/date-formatters'; -import { flatGroup } from 'd3-array'; -import type { MonthlyChartData } from 'vault/vault/charts/client-counts'; -import type { ClientTypes } from 'core/utils/client-count-utils'; -import ClientsVersionHistoryModel from 'vault/vault/models/clients/version-history'; - -interface Args { - legend: Legend[]; - data: MonthlyChartData[]; - upgradeData?: ClientsVersionHistoryModel[]; - chartTitle?: string; - chartHeight?: number; -} - -interface Legend { - key: ClientTypes; - label: string; -} -interface AggregatedDatum { - x: string; - y: number; - legendX: string; - legendY: string[]; -} - -type ChartDatum = { - timestamp: string; - clientType: string; -} & { - [key in ClientTypes]?: number | undefined; -}; - -interface UpgradeByMonth { - [key: string]: ClientsVersionHistoryModel; -} - -/** - * @module VerticalBarGrouped - * Renders a grouped bar chart of counts for different client types over time. Which client types render - * is mapped from the "key" values of the @legend arg. - * - * @example - * - */ -export default class VerticalBarGrouped extends Component { - barWidth = BAR_WIDTH; - @tracked activeDatum: AggregatedDatum | null = null; - - get chartHeight() { - return this.args.chartHeight || 190; - } - - get dataKeys(): ClientTypes[] { - return this.args.legend.map((l: Legend) => l.key); - } - - label(legendKey: string) { - return this.args.legend.find((l: Legend) => l.key === legendKey)?.label; - } - - get chartData() { - let dataset: [string, number | undefined, string, ChartDatum[]][] = []; - // each datum needs to be its own object - for (const key of this.dataKeys) { - const chartData: ChartDatum[] = this.args.data.map((d: MonthlyChartData) => ({ - timestamp: d.timestamp, - clientType: key, - [key]: d[key], - })); - - const group = flatGroup( - chartData, - // order here must match destructure order in return below - (d) => d.timestamp, - (d) => d[key], - (d) => d.clientType - ); - dataset = [...dataset, ...group]; - } - - return dataset.map(([timestamp, counts, clientType]) => ({ - timestamp, // x value - counts, // y value - clientType, // corresponds to chart's @color arg - })); - } - - // for yBounds scale, tooltip target area and tooltip text data - get aggregatedData(): AggregatedDatum[] { - return this.args.data.map((datum: MonthlyChartData) => { - const values = this.dataKeys - .map((k: string) => datum[k as ClientTypes]) - .filter((count) => Number.isInteger(count)); - const maximum = values.length - ? values.reduce((prev, currentValue) => (prev > currentValue ? prev : currentValue), 0) - : null; - const xValue = datum.timestamp; - const legend = { - x: xValue, - y: maximum ?? 0, // y-axis point where tooltip renders - legendX: parseAPITimestamp(xValue, 'MMMM yyyy') as string, - legendY: - maximum === null - ? ['No data'] - : this.dataKeys.map((k) => `${formatNumber([datum[k]])} ${this.label(k)}`), - tooltipUpgrade: this.upgradeMessage(datum), - }; - return legend; - }); - } - - get yBounds() { - const counts: number[] = this.aggregatedData - .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 xBounds() { - const domain = this.args.data.map((d) => d.timestamp); - return new Set(domain); - } - - // UPGRADE STUFF - 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 - ); - } - - upgradeMessage(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; - } - - // TEMPLATE HELPERS - barOffset = (bandwidth: number, idx = 0) => { - const withPadding = this.barWidth + 4; - const moved = (bandwidth - withPadding * this.args.legend.length) / 2; - return moved + idx * withPadding; - }; - - tooltipX = (original: number, bandwidth: number) => (original + bandwidth / 2).toString(); - - tooltipY = (original: number) => (!original ? '0' : `${original}`); - - formatTicksX = (timestamp: string): string => parseAPITimestamp(timestamp, 'M/yy') as string; - - formatTicksY = (num: number): string => numericalAxisLabel(num) || num.toString(); -} diff --git a/ui/app/components/clients/charts/vertical-bar-stacked.hbs b/ui/app/components/clients/charts/vertical-bar-stacked.hbs index e9ec70eb3b..0119a0922b 100644 --- a/ui/app/components/clients/charts/vertical-bar-stacked.hbs +++ b/ui/app/components/clients/charts/vertical-bar-stacked.hbs @@ -47,7 +47,7 @@ @xScale={{xScale}} @yScale={{yScale}} @color="clientType" - @colorScale="blue-bar" + @colorScale="stacked-bar" transform="translate({{this.barOffset xScale.bandwidth}},0)" data-test-vertical-bar /> @@ -75,7 +75,7 @@ {{#if this.activeDatum}}
  • - - Entity/Non-entity clients - -
  • - {{#if @showSecretsSyncClientCounts}} -
  • - - Secrets sync clients + {{#if this.isNotProduction}} + + Client list -
  • - {{/if}} -
  • - - ACME clients - + {{/if}}
  • \ No newline at end of file diff --git a/ui/app/components/clients/counts/nav-bar.ts b/ui/app/components/clients/counts/nav-bar.ts new file mode 100644 index 0000000000..63af623e19 --- /dev/null +++ b/ui/app/components/clients/counts/nav-bar.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +// TODO this component just exists while the client-list tab is in development for the 1.21 release +// Unless more is added to it, it can be removed when the `client list` tab+route is unhidden +import Component from '@glimmer/component'; +import config from 'vault/config/environment'; + +export default class NavBar extends Component { + get isNotProduction() { + return config.environment !== 'production'; + } +} diff --git a/ui/app/components/clients/page/acme.hbs b/ui/app/components/clients/page/acme.hbs deleted file mode 100644 index 6797b169d5..0000000000 --- a/ui/app/components/clients/page/acme.hbs +++ /dev/null @@ -1,42 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -}} - -{{! ACME clients added in 1.17 }} - -{{#if (not this.byMonthNewClients)}} - {{! byMonthNewClients is an empty array if there is no monthly data (monthly breakdown was added in 1.11) - this means the user has queried dates before ACME clients existed. we render an empty state instead of - "0 acme clients" (which is what the activity response returns) to be more explicit }} - -{{else if this.isDateRange}} - - <:subTitle> - - - - <:chart> - - - -{{else}} - - - -{{/if}} \ No newline at end of file diff --git a/ui/app/components/clients/page/acme.ts b/ui/app/components/clients/page/acme.ts deleted file mode 100644 index 767be64be0..0000000000 --- a/ui/app/components/clients/page/acme.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ActivityComponent from '../activity'; - -export default class ClientsAcmePageComponent extends ActivityComponent { - title = 'ACME usage'; - get description() { - return 'ACME clients which interacted with Vault for the first time each month. Each bar represents the total new ACME clients for that month.'; - } -} diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index 11c22673fb..4c3ff32484 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -117,7 +117,7 @@ {{/if}} - + {{! CLIENT COUNT PAGE COMPONENTS RENDER HERE }} {{yield}} diff --git a/ui/app/components/clients/page/sync.hbs b/ui/app/components/clients/page/sync.hbs deleted file mode 100644 index 0e20249579..0000000000 --- a/ui/app/components/clients/page/sync.hbs +++ /dev/null @@ -1,59 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -}} -{{#if this.flags.secretsSyncIsActivated}} - {{#if (not this.byMonthNewClients)}} - {{! byMonthNewClients is an empty array if there is no monthly data (monthly breakdown was added in 1.11) - this means the user has queried dates before sync clients existed. we render an empty state instead of - "0 sync clients" (which is what the activity response returns) to be more explicit }} - - {{else if this.isDateRange}} - - <:subTitle> - - - - <:chart> - - - - {{else}} - - - - {{/if}} -{{else}} - - - -{{/if}} \ No newline at end of file diff --git a/ui/app/components/clients/page/sync.ts b/ui/app/components/clients/page/sync.ts deleted file mode 100644 index 58364b0762..0000000000 --- a/ui/app/components/clients/page/sync.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ActivityComponent from '../activity'; -import { service } from '@ember/service'; -import type FlagsService from 'vault/services/flags'; -export default class SyncComponent extends ActivityComponent { - @service declare readonly flags: FlagsService; - - title = 'Secrets sync usage'; - description = - 'Secrets sync clients which interacted with Vault for the first time each month. Each bar represents the total new sync clients for that month.'; -} diff --git a/ui/app/components/clients/page/token.hbs b/ui/app/components/clients/page/token.hbs deleted file mode 100644 index 5d1c06b62c..0000000000 --- a/ui/app/components/clients/page/token.hbs +++ /dev/null @@ -1,65 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -}} - -{{#if (and this.byMonthNewClients this.isDateRange)}} - - <:subTitle> - - - - <:chart> - - - -{{else}} - {{! Renders when viewing a single month or activity log data that predates the monthly breakdown added in 1.11 }} - - - - - -{{/if}} \ No newline at end of file diff --git a/ui/app/components/clients/page/token.ts b/ui/app/components/clients/page/token.ts deleted file mode 100644 index f0329edd3c..0000000000 --- a/ui/app/components/clients/page/token.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ActivityComponent from '../activity'; - -import type { - ByMonthNewClients, - MountNewClients, - NamespaceByKey, - NamespaceNewClients, -} from 'core/utils/client-count-utils'; - -export default class ClientsTokenPageComponent extends ActivityComponent { - legend = [ - { key: 'entity_clients', label: 'entity clients' }, - { key: 'non_entity_clients', label: 'non-entity clients' }, - ]; - - calculateClientAverages( - dataset: - | (NamespaceByKey | undefined)[] - | (ByMonthNewClients | NamespaceNewClients | MountNewClients | undefined)[] - ) { - return ['entity_clients', 'non_entity_clients'].reduce((count, key) => { - const average = this.average(dataset, key); - return (count += average || 0); - }, 0); - } - - get hasNewClients() { - return this.byMonthNewClients.find((m) => m.entity_clients || m.non_entity_clients); - } - - get tokenStats() { - if (this.totalUsageCounts) { - const { entity_clients, non_entity_clients } = this.totalUsageCounts; - return { - total: entity_clients + non_entity_clients, - entity_clients, - non_entity_clients, - }; - } - return null; - } -} diff --git a/ui/app/components/clients/running-total.hbs b/ui/app/components/clients/running-total.hbs index 018bcb7e42..7abcf257d7 100644 --- a/ui/app/components/clients/running-total.hbs +++ b/ui/app/components/clients/running-total.hbs @@ -4,8 +4,24 @@ }} {{#if (gt @byMonthNewClients.length 1)}} - - <:subTitle> + + <:customSubheader> +
    + + Split by client type + +
    + + + <:dataLeft> - - <:stats> -
    -
    - - -
    +
    + +
    -
    +
    {{#if @isSecretsSyncActivated}} - + {{/if}}
    - - - <:chart> - - - - <:legend> - {{#each this.chartLegend as |l|}} - {{capitalize l.label}} - {{/each}} - + + <:dataRight> + {{#if this.showStacked}} + + {{else}} + + {{/if}} + {{else}} {{#let (get @byMonthNewClients "0") as |singleMonthData|}} diff --git a/ui/app/components/clients/running-total.ts b/ui/app/components/clients/running-total.ts index b4e49948dc..70f4ea3c3a 100644 --- a/ui/app/components/clients/running-total.ts +++ b/ui/app/components/clients/running-total.ts @@ -4,6 +4,8 @@ */ import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + import type { ByMonthNewClients, TotalClients } from 'core/utils/client-count-utils'; import type ClientsVersionHistoryModel from 'vault/vault/models/clients/version-history'; @@ -19,6 +21,8 @@ interface Args { } export default class RunningTotal extends Component { + @tracked showStacked = false; + get chartContainerText() { const { isSecretsSyncActivated } = this.args; return `The total clients in the specified date range, displayed per month. This includes entity, non-entity${ @@ -34,6 +38,14 @@ export default class RunningTotal extends Component { } get chartLegend() { + if (this.showStacked) { + return [ + { key: 'entity_clients', label: 'entity clients' }, + { key: 'non_entity_clients', label: 'non-entity clients' }, + ...(this.args.isSecretsSyncActivated ? [{ key: 'secret_syncs', label: 'secret sync clients' }] : []), + { key: 'acme_clients', label: 'acme clients' }, + ]; + } return [{ key: 'new_clients', label: 'new clients' }]; } } diff --git a/ui/app/router.js b/ui/app/router.js index 9ab5db8936..553cc6507c 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -29,9 +29,10 @@ Router.map(function () { this.route('clients', function () { this.route('counts', function () { this.route('overview'); - this.route('token'); - this.route('sync'); - this.route('acme'); + // TODO remove this conditional when client count feature work for 1.21 is complete + if (config.environment !== 'production') { + this.route('client-list'); + } }); this.route('config'); this.route('edit'); diff --git a/ui/app/routes/vault/cluster/clients/counts/acme.ts b/ui/app/routes/vault/cluster/clients/counts/acme.ts deleted file mode 100644 index bdb6b802e7..0000000000 --- a/ui/app/routes/vault/cluster/clients/counts/acme.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Route from '@ember/routing/route'; - -export default class ClientsCountsAcmeRoute extends Route {} diff --git a/ui/app/routes/vault/cluster/clients/counts/sync.ts b/ui/app/routes/vault/cluster/clients/counts/sync.ts deleted file mode 100644 index ef99d44bba..0000000000 --- a/ui/app/routes/vault/cluster/clients/counts/sync.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Route from '@ember/routing/route'; - -export default class ClientsCountsSyncRoute extends Route {} diff --git a/ui/app/routes/vault/cluster/clients/counts/token.ts b/ui/app/routes/vault/cluster/clients/counts/token.ts deleted file mode 100644 index 44a3f92f38..0000000000 --- a/ui/app/routes/vault/cluster/clients/counts/token.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Route from '@ember/routing/route'; - -export default class ClientsCountsOverviewRoute extends Route {} diff --git a/ui/app/styles/components/chart-container.scss b/ui/app/styles/components/chart-container.scss index 9c4c5266d9..da3514c987 100644 --- a/ui/app/styles/components/chart-container.scss +++ b/ui/app/styles/components/chart-container.scss @@ -8,93 +8,40 @@ * SPDX-License-Identifier: BUSL-1.1 */ -.chart-wrapper { - border: box-shadow_variables.$light-border; - border-radius: size_variables.$radius-large; +.chart-container { padding: size_variables.$spacing-12 size_variables.$spacing-24; - margin-bottom: size_variables.$spacing-16; -} + margin-bottom: size_variables.$spacing-24; -// GRID LAYOUT // -.single-chart-grid { - display: grid; - grid-template-columns: 1fr 0.3fr 3.7fr; - grid-template-rows: 0.5fr 1fr 1fr 0.5fr 0.25fr; - width: 100%; - &.no-legend { - grid-template-rows: 0.5fr 1fr 1fr 0.25fr; - } -} - -.chart-header { - grid-column-start: 1; - grid-column-end: span col4-end; - grid-row-start: 1; - box-shadow: inset 0 -1px 0 var(--token-color-palette-neutral-200); -} - -.chart-container-wide { - grid-column-start: 3; - grid-column-end: 4; - grid-row-start: 2; - grid-row-end: span 3; - justify-self: center; - height: 300px; - max-width: 700px; - width: 100%; - - svg.chart { - width: 100%; - height: 100%; - } -} - -.chart-empty-state { - place-self: center stretch; - grid-row-end: span 2; - grid-column-start: 1; - grid-column-end: span 3; - max-width: none; - padding-right: 20px; - padding-left: 20px; - display: flex; - - > div { - box-shadow: none !important; + .header { + margin-bottom: size_variables.$spacing-36; } - > div.empty-state { - white-space: nowrap; - align-self: stretch; - width: 100%; + // LEGEND POSITION (color and styling is in chart.scss) + .legend-container { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + + .legend-item { + display: flex; + align-items: center; + gap: 0.5rem; + } } -} -.chart-subTitle { - grid-column-start: 1; - grid-column-end: 3; - grid-row-start: 2; -} + .chart-flex-row { + display: flex; + gap: 1rem; -.data-details-top { - grid-column-start: 1; - grid-column-end: 3; - grid-row-start: 3; -} - -.data-details-bottom { - grid-column-start: 1; - grid-column-end: 3; - grid-row-start: 4; -} - -.legend { - grid-row-start: 5; - grid-column-start: 2; - grid-column-end: 6; - align-self: center; - justify-self: center; - font-size: size_variables.$size-9; + .item-left { + flex: 1 1 33.3333%; + } + .item-right { + flex: 2 1 66.6666%; + } + } } // FONT STYLES // @@ -119,15 +66,3 @@ p.chart-subtext { line-height: 16px; margin-top: size_variables.$spacing-8; } - -h3.data-details { - font-weight: font_variables.$font-weight-bold; - font-size: size_variables.$size-8; - line-height: 14px; - margin-bottom: size_variables.$spacing-8; -} - -p.data-details { - font-weight: font_variables.$font-weight-normal; - font-size: size_variables.$size-4; -} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index f69630e1c0..5478a1cb76 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -22,7 +22,6 @@ @use 'core/box'; @use 'core/buttons'; @use 'core/charts'; -@use 'core/charts-lineal'; @use 'core/checkbox-and-radio'; @use 'core/columns'; @use 'core/containers'; diff --git a/ui/app/styles/core/charts-lineal.scss b/ui/app/styles/core/charts-lineal.scss deleted file mode 100644 index 9abafabe43..0000000000 --- a/ui/app/styles/core/charts-lineal.scss +++ /dev/null @@ -1,60 +0,0 @@ -@use '../utils/size_variables'; - -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -// LINEAL STYLING // -.lineal-chart { - position: relative; - padding: 10px 10px 20px 50px; - width: 100%; - svg { - overflow: visible; - } -} -.lineal-chart-bar { - fill: var(--token-color-palette-blue-500); -} -.lineal-axis { - color: var(--token-color-palette-neutral-400); - text { - font-size: 0.75rem; - } - line { - color: var(--token-color-palette-neutral-300); - } -} -.lineal-tooltip-position { - position: absolute; - transform-style: preserve-3d; - bottom: 30px; - left: -20px; - pointer-events: none; - width: 140px; - transform: translate(calc(1px * var(--x, 0)), calc(-1px * var(--y, 0))); - transform-origin: bottom left; - z-index: 100; - margin-bottom: size_variables.$spacing-8; -} - -// @colorScale arg for Lineal::VBars is "blue-bar", indices are added by lineal -.blue-bar-1 { - color: var(--token-color-palette-blue-100); - fill: var(--token-color-palette-blue-100); -} -.blue-bar-2 { - color: var(--token-color-palette-blue-200); - fill: var(--token-color-palette-blue-200); -} - -// custom bar class for manually applying bar styles -.custom-bar-clients { - color: var(--token-color-palette-blue-200); - fill: var(--token-color-palette-blue-200); -} -.custom-bar-new_clients { - fill: var(--token-color-palette-blue-200); - color: var(--token-color-palette-blue-200); -} diff --git a/ui/app/styles/core/charts.scss b/ui/app/styles/core/charts.scss index d372cac143..6b3e6cbb3a 100644 --- a/ui/app/styles/core/charts.scss +++ b/ui/app/styles/core/charts.scss @@ -6,40 +6,36 @@ * SPDX-License-Identifier: BUSL-1.1 */ -// MISC STYLES for non-lineal charts +// LEGEND STYLING (positioning is in chart-container.scss) +$green-cyan: #06d092; +$cerulean: #02a8ef; +$blue-500: var(--token-color-palette-blue-500); +$purple-300: var(--token-color-palette-purple-300); -.legend-colors { - height: 10px; - width: 10px; - border-radius: 50%; - display: inline-block; - // numbers are indices because chart legend is iterated over - &.dot-0 { - background-color: var(--token-color-palette-blue-100); +.legend-container { + .dots { + height: 10px; + width: 10px; + border-radius: 50%; + display: inline-block; } - &.dot-1 { - background-color: var(--token-color-palette-blue-200); + .legend-new_clients { + background-color: $blue-500; } - &.dot-2 { - background-color: var(--token-color-palette-neutral-300); + .legend-entity_clients { + background-color: $blue-500; } - &.dot-3 { - background-color: var(--token-color-palette-neutral-500); + .legend-non_entity_clients { + background-color: $green-cyan; } - // custom naming for running totals, which matches the custom-bar naming - &.dot-clients { - background-color: var(--token-color-palette-blue-200); + .legend-secret_syncs { + background-color: $purple-300; } - &.dot-new_clients { - background-color: var(--token-color-palette-blue-200); + .legend-acme_clients { + background-color: $cerulean; } } -.legend-label { - padding-left: size_variables.$spacing-8; - padding-right: size_variables.$spacing-36; -} - .chart-tooltip { background-color: hsl(0, 0%, 4%); color: white; @@ -52,10 +48,17 @@ .bold { font-weight: font_variables.$font-weight-bold; } -} - -.is-label-fit-content { - max-width: fit-content !important; + // styling below handles tooltip position + position: absolute; + transform-style: preserve-3d; + bottom: 30px; + left: -20px; + pointer-events: none; + width: 140px; + transform: translate(calc(1px * var(--x, 0)), calc(-1px * var(--y, 0))); + transform-origin: bottom left; + z-index: 100; + margin-bottom: size_variables.$spacing-8; } .chart-tooltip-arrow { @@ -70,22 +73,44 @@ left: calc(50% - 5px); } -.has-grid { - g > text { - color: var(--token-color-palette-neutral-400); - font-size: size_variables.$size-9; +// LINEAL STYLING // +.lineal-chart { + position: relative; + padding: 10px 10px 20px 50px; + width: 100%; + svg { + overflow: visible; } +} - g > line { - // TODO: mix-blend doesn't work in firefox browser? - mix-blend-mode: darken; +.lineal-chart-bar { + fill: var(--token-color-palette-blue-500); +} + +.lineal-axis { + color: var(--token-color-palette-neutral-400); + text { + font-size: 0.75rem; + } + line { color: var(--token-color-palette-neutral-300); } } -.is-horizontal { - .tick > text { - font-weight: font_variables.$font-weight-semibold; - font-size: size_variables.$size-9; - } +// @colorScale arg for Lineal::VBars is "stacked-bar", indices are added by lineal +.stacked-bar-1 { + color: $blue-500; + fill: $blue-500; +} +.stacked-bar-2 { + color: $green-cyan; + fill: $green-cyan; +} +.stacked-bar-3 { + color: $purple-300; + fill: $purple-300; +} +.stacked-bar-4 { + color: $cerulean; + fill: $cerulean; } diff --git a/ui/app/templates/vault/cluster/clients/counts/acme.hbs b/ui/app/templates/vault/cluster/clients/counts/acme.hbs deleted file mode 100644 index 8802920648..0000000000 --- a/ui/app/templates/vault/cluster/clients/counts/acme.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -}} - - \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/clients/counts/sync.hbs b/ui/app/templates/vault/cluster/clients/counts/sync.hbs deleted file mode 100644 index b546827abd..0000000000 --- a/ui/app/templates/vault/cluster/clients/counts/sync.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -}} - - \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/clients/counts/token.hbs b/ui/app/templates/vault/cluster/clients/counts/token.hbs deleted file mode 100644 index c280e4274f..0000000000 --- a/ui/app/templates/vault/cluster/clients/counts/token.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -}} - - \ No newline at end of file diff --git a/ui/tests/acceptance/clients/counts-test.js b/ui/tests/acceptance/clients/counts-test.js index 2ac7bdc760..81f069a79a 100644 --- a/ui/tests/acceptance/clients/counts-test.js +++ b/ui/tests/acceptance/clients/counts-test.js @@ -56,10 +56,10 @@ module('Acceptance | clients | counts', function (hooks) { 'Start and end times added as query params' ); - await click(GENERAL.tab('token')); + await click(GENERAL.tab('client list')); assert.strictEqual( currentURL(), - '/vault/clients/counts/token?end_time=1698710400&start_time=1677628800', + '/vault/clients/counts/client-list?end_time=1698710400&start_time=1677628800', 'Start and end times persist through child route change' ); diff --git a/ui/tests/acceptance/clients/counts/acme-test.js b/ui/tests/acceptance/clients/counts/acme-test.js deleted file mode 100644 index 19f84f5ae2..0000000000 --- a/ui/tests/acceptance/clients/counts/acme-test.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { visit, click, currentURL } from '@ember/test-helpers'; -import { getUnixTime } from 'date-fns'; -import sinon from 'sinon'; -import timestamp from 'core/utils/timestamp'; -import { login } from 'vault/tests/helpers/auth/auth-helpers'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { CHARTS, CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; -import { ACTIVITY_RESPONSE_STUB, assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers'; -import { formatNumber } from 'core/helpers/format-number'; -import { filterActivityResponse, LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; -import { selectChoose } from 'ember-power-select/test-support'; -import { filterByMonthDataForMount } from 'core/utils/client-count-utils'; - -const { searchSelect } = GENERAL; - -// integration test handle general display assertions, acceptance handles nav + filtering -module('Acceptance | clients | counts | acme', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); - this.server.get('sys/internal/counters/activity', (_, req) => { - const namespace = req.requestHeaders['X-Vault-Namespace']; - return { - request_id: 'some-activity-id', - data: filterActivityResponse(ACTIVITY_RESPONSE_STUB, namespace), - }; - }); - // store serialized activity data for value comparison - const activity = await this.owner.lookup('service:store').queryRecord('clients/activity', { - start_time: { timestamp: getUnixTime(LICENSE_START) }, - end_time: { timestamp: getUnixTime(STATIC_NOW) }, - }); - this.nsPath = 'ns1'; - this.mountPath = 'pki-engine-0'; - - this.expectedValues = { - nsTotals: activity.byNamespace - .find((ns) => ns.label === this.nsPath) - .mounts.find((mount) => mount.label === this.mountPath), - nsMonthlyUsage: filterByMonthDataForMount(activity.byMonth, this.nsPath, this.mountPath), - }; - - await login(); - return visit('/vault'); - }); - - test('it navigates to acme tab', async function (assert) { - assert.expect(3); - await click(GENERAL.navLink('Client Count')); - await click(GENERAL.tab('acme')); - assert.strictEqual(currentURL(), '/vault/clients/counts/acme', 'it navigates to acme tab'); - assert.dom(GENERAL.tab('acme')).hasClass('active'); - await click(GENERAL.navLink('Back to main navigation')); - assert.strictEqual(currentURL(), '/vault/dashboard', 'it navigates back to dashboard'); - }); - - test('it filters by mount data and renders charts', async function (assert) { - const { nsTotals, nsMonthlyUsage } = this.expectedValues; - assert.expect(4 + nsMonthlyUsage.length); - - await visit('/vault/clients/counts/acme'); - await selectChoose(CLIENT_COUNT.nsFilter, this.nsPath); - await selectChoose(CLIENT_COUNT.mountFilter, this.mountPath); - - // each chart assertion count is data array length + 2 - assertBarChart(assert, 'ACME usage', nsMonthlyUsage); - assert.strictEqual( - currentURL(), - `/vault/clients/counts/acme?mountPath=pki-engine-0&ns=${this.nsPath}`, - 'namespace filter updates URL query param' - ); - assert - .dom(CLIENT_COUNT.statText('Total ACME clients')) - .hasTextContaining( - `${formatNumber([nsTotals.acme_clients])}`, - 'renders total new acme clients for namespace' - ); - }); - - /** - * This test lives here because we need an acceptance test to make sure the routing works correctly, - * and to intercept the mirage request for counters/activity which doesn't work when using scenarios. - */ - test('it queries activity with namespace header when filters change', async function (assert) { - assert.expect(5); - - let activityCount = 0; - const expectedNSHeader = [undefined, this.nsPath, undefined]; - this.server.get('sys/internal/counters/activity', (_, req) => { - const namespace = req.requestHeaders['X-Vault-Namespace']; - assert.strictEqual( - namespace, - expectedNSHeader[activityCount], - `queries activity with correct namespace header ${activityCount}` - ); - activityCount++; - return { - request_id: 'some-activity-id', - data: filterActivityResponse(ACTIVITY_RESPONSE_STUB, namespace), - }; - }); - - await visit('/vault/clients/counts/acme'); - await selectChoose(CLIENT_COUNT.nsFilter, this.nsPath); - - assert.strictEqual( - currentURL(), - `/vault/clients/counts/acme?ns=${this.nsPath}`, - 'namespace filter updates URL query param' - ); - - await click(`${CLIENT_COUNT.nsFilter} ${searchSelect.removeSelected}`); - assert.strictEqual( - currentURL(), - `/vault/clients/counts/acme`, - 'namespace filter remove updates URL query param' - ); - }); - - test('it renders empty chart for no mount data ', async function (assert) { - assert.expect(3); - await visit('/vault/clients/counts/acme'); - await selectChoose(CLIENT_COUNT.nsFilter, this.nsPath); - await selectChoose(CLIENT_COUNT.mountFilter, 'auth/userpass-0'); - // no data because this is an auth mount (acme_clients come from pki mounts) - assert.dom(CLIENT_COUNT.statText('Total ACME clients')).hasTextContaining('0'); - assert.dom(`${CHARTS.chart('ACME usage')} ${CHARTS.verticalBar}`).isNotVisible(); - assert.dom(CHARTS.container('Monthly new')).doesNotExist(); - }); -}); diff --git a/ui/tests/acceptance/clients/counts/client-list-test.js b/ui/tests/acceptance/clients/counts/client-list-test.js new file mode 100644 index 0000000000..4fe8ef41fc --- /dev/null +++ b/ui/tests/acceptance/clients/counts/client-list-test.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { visit, click, currentURL } from '@ember/test-helpers'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import { login } from 'vault/tests/helpers/auth/auth-helpers'; + +// integration test handle general display assertions, acceptance handles nav + filtering +module.skip('Acceptance | clients | counts | client list', function (hooks) { + setupApplicationTest(hooks); + + hooks.beforeEach(async function () { + await login(); + return visit('/vault'); + }); + + test('it navigates to client list tab', async function (assert) { + assert.expect(3); + await click(GENERAL.navLink('Client Count')); + await click(GENERAL.tab('client list')); + assert.strictEqual(currentURL(), '/vault/clients/counts/client-list', 'it navigates to client list tab'); + assert.dom(GENERAL.tab('client list')).hasClass('active'); + await click(GENERAL.navLink('Back to main navigation')); + assert.strictEqual(currentURL(), '/vault/dashboard', 'it navigates back to dashboard'); + }); +}); diff --git a/ui/tests/acceptance/clients/counts/overview-test.js b/ui/tests/acceptance/clients/counts/overview-test.js index 307d4cd69c..5a8da70717 100644 --- a/ui/tests/acceptance/clients/counts/overview-test.js +++ b/ui/tests/acceptance/clients/counts/overview-test.js @@ -235,102 +235,59 @@ module('Acceptance | clients | overview', function (hooks) { }); }); -module('Acceptance | clients | overview | sync in license, activated', function (hooks) { +module('Acceptance | clients | overview | secrets sync', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); - hooks.beforeEach(async function () { - sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); - - syncHandler(this.server); - - await login(); - return visit('/vault/clients/counts/overview'); - }); - - test('it should render the correct tabs', async function (assert) { - assert.dom(GENERAL.tab('sync')).exists('shows the sync tab'); - }); - - test('it should show secrets sync stats', async function (assert) { - assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists('shows secret sync data on overview'); - }); - - test('it should navigate to secrets sync page', async function (assert) { - await click(GENERAL.tab('sync')); - - assert.dom(GENERAL.tab('sync')).hasClass('active'); - assert.dom(GENERAL.emptyStateTitle).doesNotExist(); - - assert - .dom(CHARTS.chart('Secrets sync usage')) - .exists('chart is shown because feature is active and has data'); - }); -}); - -module('Acceptance | clients | overview | sync in license, not activated', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - this.server.get('/sys/license/features', () => ({ features: ['Secrets Sync'] })); - - await login(); - return visit('/vault/clients/counts/overview'); - }); - - test('it should show the secrets sync tab', async function (assert) { - assert.dom(GENERAL.tab('sync')).exists('sync tab is shown because feature is in license'); - }); - - test('it should hide secrets sync stats', async function (assert) { - assert - .dom(CLIENT_COUNT.statTextValue('Secret sync')) - .doesNotExist('stat is hidden because feature is not activated'); - assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists('other stats are still visible'); - }); -}); - -module('Acceptance | clients | overview | sync not in license', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { + test('it should hide secrets sync stats when feature is NOT on license', async function (assert) { // mocks endpoint for no additional license modules this.server.get('/sys/license/features', () => ({ features: [] })); await login(); - return visit('/vault/clients/counts/overview'); - }); - - test('it should hide the secrets sync tab', async function (assert) { - assert.dom(GENERAL.tab('sync')).doesNotExist(); - }); - - test('it should hide secrets sync stats', async function (assert) { + await visit('/vault/clients/counts/overview'); assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).doesNotExist(); assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists('other stats are still visible'); - }); -}); - -module('Acceptance | clients | overview | HVD', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); - syncHandler(this.server); - this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; - - await login(); - return visit('/vault/clients/counts/overview'); - }); - - test('it should show the secrets sync tab', async function (assert) { - assert.dom(GENERAL.tab('sync')).exists(); - }); - - test('it should show secrets sync stats', async function (assert) { - assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists(); + // TODO add assertion sync clients HIDDEN in running total chart and legend + }); + + module('feature is on license', function (hooks) { + hooks.beforeEach(async function () { + syncHandler(this.server); + }); + + test('it should show secrets sync stats when the feature is activated', async function (assert) { + syncHandler(this.server); + await login(); + await visit('/vault/clients/counts/overview'); + assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists('shows secret sync data on overview'); + // TODO add assertion sync clients SHOW in running total chart and legend + }); + + test('it should hide secrets sync stats when feature is NOT activated', async function (assert) { + this.server.get('/sys/activation-flags', () => { + return { + data: { activated: [], unactivated: ['secrets-sync'] }, + }; + }); + + await login(); + await visit('/vault/clients/counts/overview'); + + assert + .dom(CLIENT_COUNT.statTextValue('Secret sync')) + .doesNotExist('stat is hidden because feature is not activated'); + assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists('other stats are still visible'); + // TODO add assertion sync clients HIDDEN in running total chart and legend + }); + + test('it should show secrets sync stats for HVD managed clusters', async function (assert) { + // mock HVD managed cluster + this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; + + await login(); + await visit('/vault/clients/counts/overview'); + assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists(); + // TODO add assertion sync clients SHOW in running total chart and legend + }); }); }); diff --git a/ui/tests/acceptance/clients/counts/sync-test.js b/ui/tests/acceptance/clients/counts/sync-test.js deleted file mode 100644 index 64cee9ede6..0000000000 --- a/ui/tests/acceptance/clients/counts/sync-test.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import syncHandler from 'vault/mirage/handlers/sync'; -import { CONFIG_RESPONSE, STATIC_NOW } from 'vault/mirage/handlers/clients'; -import { visit, click, currentURL } from '@ember/test-helpers'; -import sinon from 'sinon'; -import timestamp from 'core/utils/timestamp'; -import { login } from 'vault/tests/helpers/auth/auth-helpers'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors'; - -module('Acceptance | clients | sync', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - module('sync activated', function (hooks) { - hooks.beforeEach(async function () { - sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); - syncHandler(this.server); - const version = this.owner.lookup('service:version'); - version.type = 'enterprise'; - await login(); - return visit('/vault/clients/counts/sync'); - }); - - test('it should render charts when secrets sync is activated', async function (assert) { - syncHandler(this.server); - assert.dom(CHARTS.chart('Secrets sync usage')).exists('Secrets sync usage chart is rendered'); - assert.dom(CLIENT_COUNT.statText('Total sync clients')).exists('Total sync clients chart is rendered'); - assert.dom(GENERAL.emptyStateTitle).doesNotExist(); - }); - }); - - module('sync not activated and on license', function (hooks) { - hooks.beforeEach(async function () { - this.server.get('/sys/internal/counters/config', function () { - return CONFIG_RESPONSE; - }); - sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); - syncHandler(this.server); - server.get('/sys/activation-flags', () => { - return { - data: { - activated: [''], - unactivated: ['secrets-sync'], - }, - }; - }); - await login(); - return visit('/vault/clients/counts/sync'); - }); - - test('it should show an empty state when secrets sync is not activated', async function (assert) { - assert.expect(2); - - this.server.get('/sys/activation-flags', () => { - assert.true(true, '/sys/activation-flags/ is called to check if secrets-sync is activated'); - // called once from the higher level cluster route - return { - data: { - activated: [], - unactivated: ['secrets-sync'], - }, - }; - }); - - assert.dom(GENERAL.emptyStateTitle).exists('Shows empty state when secrets-sync is not activated'); - - await click(`${GENERAL.emptyStateActions} .hds-link-standalone`); - assert.strictEqual( - currentURL(), - '/vault/sync/secrets/overview', - 'action button navigates to secrets sync overview page' - ); - }); - }); -}); diff --git a/ui/tests/helpers/clients/client-count-selectors.ts b/ui/tests/helpers/clients/client-count-selectors.ts index c68f95a73e..e7b6bb751b 100644 --- a/ui/tests/helpers/clients/client-count-selectors.ts +++ b/ui/tests/helpers/clients/client-count-selectors.ts @@ -48,7 +48,7 @@ export const CHARTS = { container: (title: string) => `[data-test-chart-container="${title}"]`, timestamp: '[data-test-chart-container-timestamp]', legend: '[data-test-chart-container-legend]', - legendLabel: (nth: number) => `.legend-label:nth-child(${nth * 2})`, // nth * 2 accounts for dots in legend + legendDot: (nth: number) => `.legend-item:nth-child(${nth}) > span`, // chart elements chart: (title: string) => `[data-test-chart="${title}"]`, diff --git a/ui/tests/integration/components/clients/charts/vertical-bar-grouped-test.js b/ui/tests/integration/components/clients/charts/vertical-bar-grouped-test.js deleted file mode 100644 index 230147ff84..0000000000 --- a/ui/tests/integration/components/clients/charts/vertical-bar-grouped-test.js +++ /dev/null @@ -1,249 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'vault/tests/helpers'; -import { render, triggerEvent } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { CHARTS } from 'vault/tests/helpers/clients/client-count-selectors'; -import { assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers'; - -module('Integration | Component | clients/charts/vertical-bar-grouped', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.legend = [ - { key: 'clients', label: 'Total clients' }, - { key: 'foo', label: 'Foo' }, - ]; - this.data = [ - { - timestamp: '2018-04-03T14:15:30', - clients: 14, - foo: 4, - month: '4/18', - }, - { - timestamp: '2018-05-03T14:15:30', - clients: 18, - foo: 8, - month: '5/18', - }, - { - timestamp: '2018-06-03T14:15:30', - clients: 114, - foo: 14, - month: '6/18', - }, - { - timestamp: '2018-07-03T14:15:30', - clients: 110, - foo: 10, - month: '7/18', - }, - ]; - this.renderComponent = async () => { - await render( - hbs`
    - -
    ` - ); - }; - }); - - test('it renders empty state when no data', async function (assert) { - this.data = []; - await this.renderComponent(); - assert.dom(CHARTS.chart('grouped vertical bar chart')).doesNotExist(); - assert.dom(GENERAL.emptyStateSubtitle).hasText('No data to display'); - }); - - test('it renders chart with data as grouped bars', async function (assert) { - await this.renderComponent(); - assert.dom(CHARTS.chart('grouped vertical bar chart')).exists(); - const barCount = this.data.length * this.legend.length; - // bars are what we expect - assert.dom(CHARTS.verticalBar).exists({ count: barCount }); - assert.dom(`.custom-bar-clients`).exists({ count: 4 }, 'clients bars have correct class'); - assert.dom(`.custom-bar-foo`).exists({ count: 4 }, 'foo bars have correct class'); - assertBarChart(assert, 'grouped vertical bar chart', this.data, true); - }); - - test('it renders chart with tooltips when some is missing', async function (assert) { - assert.expect(13); - this.data = [ - { - timestamp: '2018-04-03T14:15:30', - month: '4/18', - expectedTooltip: 'April 2018 No data', - }, - { - timestamp: '2018-05-03T14:15:30', - month: '5/18', - clients: 0, - foo: 0, - }, - { - timestamp: '2018-06-03T14:15:30', - month: '6/18', - clients: 14, - foo: 4, - expectedTooltip: 'June 2018 14 Total clients 4 Foo', - }, - ]; - await this.renderComponent(); - assert.dom(CHARTS.chart('grouped vertical bar chart')).exists(); - const barCount = this.data.length * this.legend.length; - assert.dom(CHARTS.verticalBar).exists({ count: barCount }); - assertBarChart(assert, 'grouped vertical bar chart', this.data, true); - - // TOOLTIPS - NO DATA - await triggerEvent(CHARTS.hover(this.data[0].timestamp), 'mouseover'); - assert.dom(CHARTS.tooltip).isVisible(`renders tooltip on mouseover`); - assert - .dom(CHARTS.tooltip) - .hasText(this.data[0].expectedTooltip, 'renders formatted timestamp with no data message'); - await triggerEvent(CHARTS.hover(this.data[2].timestamp), 'mouseout'); - assert.dom(CHARTS.tooltip).doesNotExist('removes tooltip on mouseout'); - - // TOOLTIPS - WITH DATA - await triggerEvent(CHARTS.hover(this.data[2].timestamp), 'mouseover'); - assert.dom(CHARTS.tooltip).isVisible(`renders tooltip on mouseover`); - assert.dom(CHARTS.tooltip).hasText(this.data[2].expectedTooltip, 'renders formatted timestamp with data'); - await triggerEvent(CHARTS.hover(this.data[2].timestamp), 'mouseout'); - assert.dom(CHARTS.tooltip).doesNotExist('removes tooltip on mouseout'); - }); - - test('it renders upgrade data', async function (assert) { - this.upgradeData = [ - { - version: '1.10.1', - previousVersion: '1.9.2', - timestampInstalled: '2018-05-03T14:15:30', - }, - ]; - await this.renderComponent(); - assert.dom(CHARTS.chart('grouped vertical bar chart')).exists(); - const barCount = this.data.length * this.legend.length; - // bars are what we expect - assert.dom(CHARTS.verticalBar).exists({ count: barCount }); - assert.dom(`.custom-bar-clients`).exists({ count: 4 }, 'clients bars have correct class'); - assert.dom(`.custom-bar-foo`).exists({ count: 4 }, 'foo bars have correct class'); - assertBarChart(assert, 'grouped vertical bar chart', this.data, true); - - // TOOLTIP - await triggerEvent(CHARTS.hover('2018-05-03T14:15:30'), 'mouseover'); - assert.dom(CHARTS.tooltip).isVisible(`renders tooltip on mouseover`); - assert - .dom(CHARTS.tooltip) - .hasText( - 'May 2018 18 Total clients 8 Foo Vault was upgraded from 1.9.2 to 1.10.1', - 'renders formatted timestamp with data' - ); - await triggerEvent(CHARTS.hover('2018-05-03T14:15:30'), 'mouseout'); - assert.dom(CHARTS.tooltip).doesNotExist('removes tooltip on mouseout'); - }); - - test('it updates axis when dataset updates', async function (assert) { - const datasets = { - small: [ - { - timestamp: '2020-04-01', - bar: 4, - month: '4/20', - }, - { - timestamp: '2020-05-01', - bar: 8, - month: '5/20', - }, - { - timestamp: '2020-06-01', - bar: 1, - }, - { - timestamp: '2020-07-01', - bar: 10, - }, - ], - large: [ - { - timestamp: '2020-08-01', - bar: 4586, - month: '8/20', - }, - { - timestamp: '2020-09-01', - bar: 8928, - month: '9/20', - }, - { - timestamp: '2020-10-01', - bar: 11948, - month: '10/20', - }, - { - timestamp: '2020-11-01', - bar: 16943, - month: '11/20', - }, - ], - broken: [ - { - timestamp: '2020-01-01', - bar: null, - month: '1/20', - }, - { - timestamp: '2020-02-01', - bar: 0, - month: '2/20', - }, - { - timestamp: '2020-03-01', - bar: 22, - month: '3/20', - }, - { - timestamp: '2020-04-01', - bar: null, - month: '4/20', - }, - { - timestamp: '2020-05-01', - bar: 70, - month: '5/20', - }, - { - timestamp: '2020-06-01', - bar: 50, - month: '6/20', - }, - ], - }; - this.legend = [{ key: 'bar', label: 'Some thing' }]; - this.set('data', datasets.small); - await this.renderComponent(); - assert.dom('[data-test-y-axis]').hasText('0 2 4 6 8 10', 'y-axis renders correctly for small values'); - assert - .dom('[data-test-x-axis]') - .hasText('4/20 5/20 6/20 7/20', 'x-axis renders correctly for small values'); - - // Update to large dataset - this.set('data', datasets.large); - assert.dom('[data-test-y-axis]').hasText('0 5k 10k 15k', 'y-axis renders correctly for new large values'); - assert - .dom('[data-test-x-axis]') - .hasText('8/20 9/20 10/20 11/20', 'x-axis renders correctly for small values'); - - // Update to broken dataset - this.set('data', datasets.broken); - assert.dom('[data-test-y-axis]').hasText('0 20 40 60', 'y-axis renders correctly for new broken values'); - assert - .dom('[data-test-x-axis]') - .hasText('1/20 2/20 3/20 4/20 5/20 6/20', 'x-axis renders correctly for small values'); - }); -}); diff --git a/ui/tests/integration/components/clients/counts/nav-bar-test.js b/ui/tests/integration/components/clients/counts/nav-bar-test.js index a8dcaf9798..d7e294297d 100644 --- a/ui/tests/integration/components/clients/counts/nav-bar-test.js +++ b/ui/tests/integration/components/clients/counts/nav-bar-test.js @@ -13,12 +13,8 @@ module('Integration | Component | clients/counts/nav-bar', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { - this.showSecretsSyncClientCounts = false; - this.renderComponent = async () => { - await render( - hbs`` - ); + await render(hbs``); }; }); @@ -26,21 +22,6 @@ module('Integration | Component | clients/counts/nav-bar', function (hooks) { await this.renderComponent(); assert.dom(GENERAL.tab('overview')).hasText('Overview'); - assert.dom(GENERAL.tab('token')).hasText('Entity/Non-entity clients'); - assert.dom(GENERAL.tab('acme')).hasText('ACME clients'); - }); - - test('it shows secrets sync tab if showSecretsSyncClientCounts is true', async function (assert) { - this.showSecretsSyncClientCounts = true; - await this.renderComponent(); - - assert.dom(GENERAL.tab('sync')).exists(); - }); - - test('it should not show secrets sync tab if showSecretsSyncClientCounts is false', async function (assert) { - this.showSecretsSyncClientCounts = false; - await this.renderComponent(); - - assert.dom(GENERAL.tab('sync')).doesNotExist(); + assert.dom(GENERAL.tab('client list')).hasText('Client list'); }); }); diff --git a/ui/tests/integration/components/clients/page/acme-test.js b/ui/tests/integration/components/clients/page/acme-test.js deleted file mode 100644 index d8cd2632dd..0000000000 --- a/ui/tests/integration/components/clients/page/acme-test.js +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { setRunOptions } from 'ember-a11y-testing/test-support'; -import { render, findAll } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; -import { getUnixTime } from 'date-fns'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors'; -import { formatNumber } from 'core/helpers/format-number'; -import { assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers'; - -const START_TIME = getUnixTime(LICENSE_START); -const END_TIME = getUnixTime(STATIC_NOW); -const { statText, usageStats } = CLIENT_COUNT; - -module('Integration | Component | clients | Clients::Page::Acme', function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - clientsHandler(this.server); - this.store = this.owner.lookup('service:store'); - const activityQuery = { - start_time: { timestamp: START_TIME }, - end_time: { timestamp: END_TIME }, - }; - // set this to 0 - this.activity = await this.store.queryRecord('clients/activity', activityQuery); - this.startTimestamp = START_TIME; - this.endTimestamp = END_TIME; - - this.renderComponent = () => - render(hbs` - - `); - // Fails on #ember-testing-container - setRunOptions({ - rules: { - 'scrollable-region-focusable': { enabled: false }, - }, - }); - }); - - test('it should render with full month activity data charts', async function (assert) { - const monthCount = this.activity.byMonth.length; - assert.expect(3 + monthCount); - - const expectedTotal = formatNumber([this.activity.total.acme_clients]); - - await this.renderComponent(); - assert - .dom(statText('Total ACME clients')) - .hasText( - `Total ACME clients The total number of ACME requests made to Vault during this time period. ${expectedTotal}`, - `renders correct total acme stat ${expectedTotal}` - ); - - assertBarChart(assert, 'ACME usage', this.activity.byMonth); - }); - - test('it should render stats without chart for a single month', async function (assert) { - assert.expect(2); - const activityQuery = { start_time: { timestamp: END_TIME }, end_time: { timestamp: END_TIME } }; - this.activity = await this.store.queryRecord('clients/activity', activityQuery); - - const expectedTotal = formatNumber([this.activity.total.acme_clients]); - - await this.renderComponent(); - assert.dom(CHARTS.chart('ACME usage')).doesNotExist('total usage chart does not render'); - assert - .dom(usageStats('ACME usage')) - .hasText( - `ACME usage Client usage tutorial ACME clients which interacted with Vault for the first time each month. Each bar represents the total new ACME clients for that month. Total ACME clients ${expectedTotal}`, - 'it renders usage stats with single month copy' - ); - }); - - // EMPTY STATES - test('it should render empty state when ACME data does not exist for a date range', async function (assert) { - assert.expect(7); - // this happens when a user queries historical data that predates the monthly breakdown (added in 1.11) - // only entity + non-entity clients existed then, so we show an empty state for ACME clients - // because the activity response just returns { acme_clients: 0 } which isn't very clear - this.activity.byMonth = []; - - await this.renderComponent(); - - assert.dom(GENERAL.emptyStateTitle).hasText('No ACME clients'); - assert - .dom(GENERAL.emptyStateMessage) - .hasText('There is no ACME client data available for this date range.'); - - assert.dom(CHARTS.chart('ACME usage')).doesNotExist('vertical bar chart does not render'); - assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render'); - assert.dom(statText('Total ACME clients')).doesNotExist(); - assert.dom(statText('Average new ACME clients per month')).doesNotExist(); - assert.dom(usageStats('ACME usage')).doesNotExist(); - }); - - test('it should render empty state when ACME data does not exist for a single month', async function (assert) { - assert.expect(1); - const activityQuery = { start_time: { timestamp: START_TIME }, end_time: { timestamp: START_TIME } }; - this.activity = await this.store.queryRecord('clients/activity', activityQuery); - this.activity.byMonth = []; - - await this.renderComponent(); - - assert.dom(GENERAL.emptyStateMessage).hasText('There is no ACME client data available for this month.'); - }); - - test('it should render empty total usage chart when monthly counts are null or 0', async function (assert) { - assert.expect(8); - // manually stub because mirage isn't setup to handle mixed data yet - const counts = { - acme_clients: 0, - clients: 19, - entity_clients: 0, - non_entity_clients: 19, - secret_syncs: 0, - }; - this.activity.byMonth = [ - { - month: '3/24', - timestamp: '2024-03-01T00:00:00Z', - namespaces: [], - new_clients: { - month: '3/24', - timestamp: '2024-03-01T00:00:00Z', - namespaces: [], - }, - }, - { - month: '4/24', - timestamp: '2024-04-01T00:00:00Z', - ...counts, - namespaces: [], - new_clients: { - month: '4/24', - timestamp: '2024-04-01T00:00:00Z', - namespaces: [], - }, - }, - ]; - this.activity.total = counts; - - await this.renderComponent(); - - assert.dom(CHARTS.chart('ACME usage')).exists('renders empty ACME usage chart'); - assert - .dom(statText('Total ACME clients')) - .hasTextContaining('The total number of ACME requests made to Vault during this time period. 0'); - findAll(`${CHARTS.chart('ACME usage')} ${CHARTS.xAxisLabel}`).forEach((e, i) => { - assert - .dom(e) - .hasText( - `${this.activity.byMonth[i].month}`, - `renders x-axis labels for empty bar chart: ${this.activity.byMonth[i].month}` - ); - }); - findAll(`${CHARTS.chart('ACME usage')} ${CHARTS.verticalBar}`).forEach((e, i) => { - assert.dom(e).isNotVisible(`does not render data bar for: ${this.activity.byMonth[i].month}`); - }); - - assert - .dom(CHARTS.container('Monthly new')) - .doesNotExist('empty monthly new chart does not render at all'); - assert.dom(statText('Average new ACME clients per month')).doesNotExist(); - }); -}); diff --git a/ui/tests/integration/components/clients/page/sync-test.js b/ui/tests/integration/components/clients/page/sync-test.js deleted file mode 100644 index 7cf3f4c12f..0000000000 --- a/ui/tests/integration/components/clients/page/sync-test.js +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { render, findAll } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; -import syncHandler from 'vault/mirage/handlers/sync'; -import { getUnixTime } from 'date-fns'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors'; -import { formatNumber } from 'core/helpers/format-number'; -import { assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers'; - -const START_TIME = getUnixTime(LICENSE_START); -const END_TIME = getUnixTime(STATIC_NOW); -const { statText, usageStats } = CLIENT_COUNT; - -module('Integration | Component | clients | Clients::Page::Sync', function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - this.renderComponent = () => - render(hbs` - - `); - }); - - module('with secrets sync not activated', function () { - test('it should render an empty state', async function (assert) { - await this.renderComponent(); - - assert.dom(GENERAL.emptyStateTitle).hasText('No Secrets Sync clients'); - assert - .dom(GENERAL.emptyStateMessage) - .hasText('No data is available because Secrets Sync has not been activated.'); - assert.dom(GENERAL.emptyStateActions).hasText('Activate Secrets Sync'); - - assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist(); - assert.dom(statText('Total sync clients')).doesNotExist(); - }); - }); - - module('with secrets sync activated', function (hooks) { - hooks.beforeEach(async function () { - syncHandler(this.server); - this.owner.lookup('service:flags').activatedFlags = ['secrets-sync']; - - this.store = this.owner.lookup('service:store'); - const activityQuery = { - start_time: { timestamp: START_TIME }, - end_time: { timestamp: END_TIME }, - }; - // set this to 0 - this.activity = await this.store.queryRecord('clients/activity', activityQuery); - this.startTimestamp = START_TIME; - this.endTimestamp = END_TIME; - }); - - test('it should render with full month activity data', async function (assert) { - const monthCount = this.activity.byMonth.length; - assert.expect(3 + monthCount); - const expectedTotal = formatNumber([this.activity.total.secret_syncs]); - await this.renderComponent(); - assert - .dom(statText('Total sync clients')) - .hasText( - `Total sync clients The total number of secrets synced from Vault to other destinations during this date range. ${expectedTotal}`, - `renders correct total sync stat ${expectedTotal}` - ); - - assertBarChart(assert, 'Secrets sync usage', this.activity.byMonth); - }); - - test('it should render stats without chart for a single month', async function (assert) { - const activityQuery = { start_time: { timestamp: END_TIME }, end_time: { timestamp: END_TIME } }; - this.activity = await this.store.queryRecord('clients/activity', activityQuery); - const expectedTotal = formatNumber([this.activity.total.secret_syncs]); - - await this.renderComponent(); - - assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist('total usage chart does not render'); - assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render'); - assert.dom(statText('Average new sync clients per month')).doesNotExist(); - assert - .dom(usageStats('Secrets sync usage')) - .hasText( - `Secrets sync usage Client usage tutorial Secrets sync clients which interacted with Vault for the first time each month. Each bar represents the total new sync clients for that month. Total sync clients ${expectedTotal}`, - 'it renders usage stats with single month copy' - ); - }); - - // EMPTY STATES - test('it should render empty state when sync data does not exist for a date range', async function (assert) { - assert.expect(7); - // this happens when a user queries historical data that predates the monthly breakdown (added in 1.11) - // only entity + non-entity clients existed then, so we show an empty state for sync clients - // because the activity response just returns { secret_syncs: 0 } which isn't very clear - this.activity.byMonth = []; - - await this.renderComponent(); - - assert.dom(GENERAL.emptyStateTitle).hasText('No secrets sync clients'); - assert.dom(GENERAL.emptyStateMessage).hasText('There is no sync data available for this date range.'); - - assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist('vertical bar chart does not render'); - assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render'); - assert.dom(statText('Total sync clients')).doesNotExist(); - assert.dom(statText('Average new sync clients per month')).doesNotExist(); - assert.dom(usageStats('Secrets sync usage')).doesNotExist(); - }); - - test('it should render empty state when sync data does not exist for a single month', async function (assert) { - assert.expect(1); - const activityQuery = { start_time: { timestamp: START_TIME }, end_time: { timestamp: START_TIME } }; - this.activity = await this.store.queryRecord('clients/activity', activityQuery); - this.activity.byMonth = []; - await this.renderComponent(); - - assert.dom(GENERAL.emptyStateMessage).hasText('There is no sync data available for this month.'); - }); - - test('it should render an empty total usage chart if secrets sync is activated but monthly syncs are null or 0', async function (assert) { - // manually stub because mirage isn't setup to handle mixed data yet - const counts = { - clients: 10, - entity_clients: 4, - non_entity_clients: 6, - secret_syncs: 0, - }; - const monthData = { - month: '1/24', - timestamp: '2024-01-01T00:00:00-08:00', - ...counts, - namespaces: [ - { - label: 'root', - ...counts, - mounts: [], - }, - ], - }; - this.activity.byMonth = [ - { - ...monthData, - new_clients: { - ...monthData, - }, - }, - ]; - this.activity.total = counts; - - assert.expect(6); - await this.renderComponent(); - - assert.dom(CHARTS.chart('Secrets sync usage')).exists('renders empty sync usage chart'); - assert - .dom(statText('Total sync clients')) - .hasText( - 'Total sync clients The total number of secrets synced from Vault to other destinations during this date range. 0' - ); - findAll(`${CHARTS.chart('Secrets sync usage')} ${CHARTS.xAxisLabel}`).forEach((e, i) => { - assert - .dom(e) - .hasText( - `${this.activity.byMonth[i].month}`, - `renders x-axis labels for empty bar chart: ${this.activity.byMonth[i].month}` - ); - }); - findAll(`${CHARTS.chart('Secrets sync usage')} ${CHARTS.verticalBar}`).forEach((e, i) => { - assert.dom(e).isNotVisible(`does not render data bar for: ${this.activity.byMonth[i].month}`); - }); - - assert - .dom(CHARTS.container('Monthly new')) - .doesNotExist('empty monthly new chart does not render at all'); - assert.dom(statText('Average new sync clients per month')).doesNotExist(); - }); - }); -}); diff --git a/ui/tests/integration/components/clients/page/token-test.js b/ui/tests/integration/components/clients/page/token-test.js deleted file mode 100644 index c502febf9c..0000000000 --- a/ui/tests/integration/components/clients/page/token-test.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { setRunOptions } from 'ember-a11y-testing/test-support'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; -import { getUnixTime } from 'date-fns'; -import { formatNumber } from 'core/helpers/format-number'; -import { CHARTS, CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; -import { assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers'; - -const START_TIME = getUnixTime(LICENSE_START); -const END_TIME = getUnixTime(STATIC_NOW); - -module('Integration | Component | clients | Clients::Page::Token', function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - clientsHandler(this.server); - const store = this.owner.lookup('service:store'); - const activityQuery = { - start_time: { timestamp: START_TIME }, - end_time: { timestamp: END_TIME }, - }; - this.activity = await store.queryRecord('clients/activity', activityQuery); - this.newActivity = this.activity.byMonth.map((d) => d.new_clients); - this.versionHistory = await store - .findAll('clients/version-history') - .then((response) => { - return response.map(({ version, previousVersion, timestampInstalled }) => { - return { - version, - previousVersion, - timestampInstalled, - }; - }); - }) - .catch(() => []); - this.startTimestamp = START_TIME; - this.endTimestamp = END_TIME; - this.renderComponent = () => - render(hbs` - - `); - // Fails on #ember-testing-container - setRunOptions({ - rules: { - 'scrollable-region-focusable': { enabled: false }, - }, - }); - }); - - test('it should render monthly total chart', async function (assert) { - const count = this.activity.byMonth.length; - const { entity_clients, non_entity_clients } = this.activity.total; - assert.expect(count + 6); - - const expectedTotal = formatNumber([entity_clients + non_entity_clients]); - const chart = CHARTS.container('Entity/Non-entity clients usage'); - await this.renderComponent(); - - assert - .dom(CLIENT_COUNT.statTextValue('Total clients')) - .hasText(expectedTotal, 'renders correct total clients'); - - // assert bar chart is correct - assert.dom(`${chart} ${CHARTS.xAxis}`).hasText('7/23 8/23 9/23 10/23 11/23 12/23 1/24'); - assertBarChart(assert, 'Entity/Non-entity clients usage', this.activity.byMonth, true); - - assert.dom(`${chart} ${CHARTS.legendLabel(1)}`).hasText('Entity clients', 'Legend label renders'); - assert.dom(`${chart} ${CHARTS.legendLabel(2)}`).hasText('Non-entity clients', 'Legend label renders'); - }); - - test('it should render usage stats', async function (assert) { - assert.expect(6); - - this.activity.endTime = this.activity.startTime; - - const checkUsage = () => { - const { entity_clients, non_entity_clients } = this.activity.total; - assert - .dom(CLIENT_COUNT.statTextValue('Total clients')) - .hasText(formatNumber([entity_clients + non_entity_clients]), 'Total clients value renders'); - assert - .dom(CLIENT_COUNT.statTextValue('Entity')) - .hasText(formatNumber([entity_clients]), 'Entity value renders'); - assert - .dom(CLIENT_COUNT.statTextValue('Non-entity')) - .hasText(formatNumber([non_entity_clients]), 'Non-entity value renders'); - }; - - // total usage should display for single month query - await this.renderComponent(); - checkUsage(); - - // total usage should display when there is no monthly data - this.activity.byMonth = null; - await this.renderComponent(); - checkUsage(); - }); -}); diff --git a/ui/tests/integration/components/clients/running-total-test.js b/ui/tests/integration/components/clients/running-total-test.js index 343d29d5dd..be8ec5c0f3 100644 --- a/ui/tests/integration/components/clients/running-total-test.js +++ b/ui/tests/integration/components/clients/running-total-test.js @@ -6,16 +6,17 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; -import { render } from '@ember/test-helpers'; +import { click, find, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; import sinon from 'sinon'; -import { formatRFC3339, getUnixTime } from 'date-fns'; +import { getUnixTime } from 'date-fns'; import { findAll } from '@ember/test-helpers'; import { formatNumber } from 'core/helpers/format-number'; import timestamp from 'core/utils/timestamp'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; const START_TIME = getUnixTime(LICENSE_START); @@ -32,15 +33,9 @@ module('Integration | Component | clients/running-total', function (hooks) { end_time: { timestamp: getUnixTime(timestamp.now()) }, }; const activity = await store.queryRecord('clients/activity', activityQuery); - this.byMonthActivity = activity.byMonth; - this.newActivity = this.byMonthActivity.map((d) => d.new_clients); + this.byMonthNewClients = activity.byMonth.map((d) => d.new_clients); this.totalUsageCounts = activity.total; - this.set('timestamp', formatRFC3339(timestamp.now())); - this.set('chartLegend', [ - { label: 'entity clients', key: 'entity_clients' }, - { label: 'non-entity clients', key: 'non_entity_clients' }, - ]); this.isSecretsSyncActivated = true; this.isHistoricalMonth = false; @@ -48,7 +43,7 @@ module('Integration | Component | clients/running-total', function (hooks) { await render(hbs` ({ - ...d, - new_clients: { month: d.month }, - })); - + test('it toggles to split chart by client type', async function (assert) { await this.renderComponent(); + await click(GENERAL.inputByAttr('toggle view')); assert.dom(CHARTS.container('Vault client counts')).exists('running total component renders'); assert.dom(CHARTS.chart('Vault client counts')).exists('bar chart renders'); + assert.dom(CHARTS.legend).hasText('Entity clients Non-entity clients Secret sync clients Acme clients'); - const expectedValues = { - Entity: formatNumber([this.totalUsageCounts.entity_clients]), - 'Non-entity': formatNumber([this.totalUsageCounts.non_entity_clients]), - ACME: formatNumber([this.totalUsageCounts.acme_clients]), - 'Secret sync': formatNumber([this.totalUsageCounts.secret_syncs]), - }; - for (const label in expectedValues) { + // assert each legend item is correct + const expectedLegend = [ + { label: 'Entity clients', color: 'rgb(28, 52, 95)' }, + { label: 'Non-entity clients', color: 'rgb(6, 208, 146)' }, + { label: 'Secret sync clients', color: 'rgb(145, 28, 237)' }, + { label: 'Acme clients', color: 'rgb(2, 168, 239)' }, + ]; + + findAll('.legend-item').forEach((e, i) => { + const { label, color } = expectedLegend[i]; + assert.dom(e).hasText(label, `legend renders label: ${label}`); + const dotColor = getComputedStyle(find(CHARTS.legendDot(i + 1))).backgroundColor; + assert.strictEqual(dotColor, color, `actual color: ${dotColor}, expected color: ${color}`); + }); + + // assert bar chart is correct + findAll(CHARTS.xAxisLabel).forEach((e, i) => { assert - .dom(CLIENT_COUNT.statTextValue(label)) + .dom(e) .hasText( - `${expectedValues[label]}`, - `stat label: ${label} renders correct total: ${expectedValues[label]}` + `${this.byMonthNewClients[i].month}`, + `renders x-axis labels for bar chart: ${this.byMonthNewClients[i].month}` ); - } + }); + + const months = this.byMonthNewClients.length; + const barsPerMonth = expectedLegend.length; + assert + .dom(CHARTS.verticalBar) + .exists({ count: months * barsPerMonth }, `renders ${barsPerMonth} bars per month`); }); test('it renders with single historical month data', async function (assert) { - const singleMonthNew = this.newActivity[this.newActivity.length - 1]; - this.byMonthActivity = [singleMonthNew]; + const singleMonthNew = this.byMonthNewClients[this.byMonthNewClients.length - 1]; + this.byMonthNewClients = [singleMonthNew]; this.isHistoricalMonth = true; await this.renderComponent(); const expectedStats = { @@ -152,6 +164,8 @@ module('Integration | Component | clients/running-total', function (hooks) { test('it hides secret sync totals when feature is not activated', async function (assert) { this.isSecretsSyncActivated = false; + // reset secret sync clients to 0 + this.byMonthNewClients = this.byMonthNewClients.map((obj) => ({ ...obj, secret_syncs: 0 })); await this.renderComponent(); @@ -160,5 +174,31 @@ module('Integration | Component | clients/running-total', function (hooks) { assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists(); assert.dom(CLIENT_COUNT.statTextValue('Non-entity')).exists(); assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).doesNotExist('does not render secret syncs'); + + // check toggle view + await click(GENERAL.inputByAttr('toggle view')); + assert + .dom(CHARTS.legend) + .hasText('Entity clients Non-entity clients Acme clients', 'legend does not include sync clients'); + + // assert each legend item is correct + const expectedLegend = [ + { label: 'Entity clients', color: 'rgb(28, 52, 95)' }, + { label: 'Non-entity clients', color: 'rgb(6, 208, 146)' }, + { label: 'Acme clients', color: 'rgb(2, 168, 239)' }, + ]; + + findAll('.legend-item').forEach((e, i) => { + const { label, color } = expectedLegend[i]; + assert.dom(e).hasText(label, `legend renders label: ${label}`); + const dotColor = getComputedStyle(find(CHARTS.legendDot(i + 1))).backgroundColor; + assert.strictEqual(dotColor, color, `actual color: ${dotColor}, expected color: ${color}`); + }); + + const months = this.byMonthNewClients.length; + const barsPerMonth = expectedLegend.length; + assert + .dom(CHARTS.verticalBar) + .exists({ count: months * barsPerMonth }, `renders ${barsPerMonth} bars per month`); }); });