vault/ui/app/components/clients/page-header.js
lane-wetmore cdbb0c49cc
UI: Vault update client count charts to show new clients only (#30506)
* increase bar width, show new clients only, add timestamp to header, update bar color

* remove extra timestamps, switch to basic bar chart

* update docs and styling

* remove unneeded timestamp args

* show new client running totatls

* initial test updates

* update test

* clean up new client total calc into util fn

* bits of clean up and todos

* update tests

* update to avoid activity call when in CE and missing either start or end time

* update todos

* update tests

* tidying

* move new client total onto payload for easier access

* update more tests to align with copy changes and new client totals

* remove addressed TODOs

* Update comment

* add changelog entry

* revert to using total, update tests and clean up

* Update ui/app/components/clients/page/counts.hbs

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>

* remove duplicate charts and update descriptions

* update tests after removing extra charts

* tidy

* update instances of byMonthActivityData to use byMonthNewClients and update tests

* Update ui/app/components/clients/running-total.ts

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>

* update chart styles

---------

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
2025-05-19 15:57:32 -05:00

149 lines
5.2 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { action } from '@ember/object';
import { service } from '@ember/service';
import { waitFor } from '@ember/test-waiters';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { parseAPITimestamp } from 'core/utils/date-formatters';
import { sanitizePath } from 'core/utils/sanitize-path';
import { isSameMonth } from 'date-fns';
import { task } from 'ember-concurrency';
/**
* @module ClientsPageHeader
* ClientsPageHeader components are used to render a header and check for export capabilities before rendering an export button.
*
* @example
* ```js
* <Clients::PageHeader @startTimestamp="2022-06-01T23:00:11.050Z" @endTimestamp="2022-12-01T23:00:11.050Z" @namespace="foo" @upgradesDuringActivity={{array (hash version="1.10.1" previousVersion="1.9.1" timestampInstalled= "2021-11-18T10:23:16Z") }} />
* ```
* @param {string} [billingStartTime] - ISO timestamp of billing start date, to be passed to date picker
* @param {string} [activityTimestamp] - ISO timestamp created in serializer to timestamp the response to be displayed in page header
* @param {string} [startTimestamp] - ISO timestamp of start time, to be passed to export request
* @param {string} [endTimestamp] - ISO timestamp of end time, to be passed to export request
* @param {number} [retentionMonths = 48] - number of months for historical billing, to be passed to date picker
* @param {string} [namespace] - namespace filter. Will be appended to the current namespace in the export request.
* @param {string} [upgradesDuringActivity] - array of objects containing version history upgrade data
* @param {boolean} [noData = false] - when true, export button will hide regardless of capabilities
* @param {function} [onChange] - callback when a new date range is saved, to be passed to date picker
*/
export default class ClientsPageHeaderComponent extends Component {
@service download;
@service namespace;
@service store;
@service version;
@tracked canDownload = false;
@tracked showEditModal = false;
@tracked showExportModal = false;
@tracked exportFormat = 'csv';
@tracked downloadError = '';
constructor() {
super(...arguments);
this.getExportCapabilities(this.args.namespace);
}
get showExportButton() {
if (this.args.noData === true) return false;
return this.canDownload;
}
@waitFor
async getExportCapabilities(ns = '') {
try {
// selected namespace usually ends in /
const url = ns
? `${sanitizePath(ns)}/sys/internal/counters/activity/export`
: 'sys/internal/counters/activity/export';
const cap = await this.store.findRecord('capabilities', url);
this.canDownload = cap.canSudo;
} catch (e) {
// if we can't read capabilities, default to show
this.canDownload = true;
}
}
get formattedStartDate() {
if (!this.args.startTimestamp) return null;
return parseAPITimestamp(this.args.startTimestamp, 'MMMM yyyy');
}
get formattedEndDate() {
if (!this.args.endTimestamp) return null;
return parseAPITimestamp(this.args.endTimestamp, 'MMMM yyyy');
}
get showEndDate() {
// displays on CSV export modal, no need to display duplicate months and years
if (!this.args.endTimestamp) return false;
const startDateObject = parseAPITimestamp(this.args.startTimestamp);
const endDateObject = parseAPITimestamp(this.args.endTimestamp);
return !isSameMonth(startDateObject, endDateObject);
}
get formattedCsvFileName() {
const endRange = this.showEndDate ? `-${this.formattedEndDate}` : '';
const csvDateRange = this.formattedStartDate ? `_${this.formattedStartDate + endRange}` : '';
const ns = this.namespaceFilter ? `_${this.namespaceFilter}` : '';
return `clients_export${ns}${csvDateRange}`;
}
get namespaceFilter() {
const currentNs = this.namespace.path;
const { namespace } = this.args;
return namespace ? sanitizePath(`${currentNs}/${namespace}`) : sanitizePath(currentNs);
}
get showCommunity() {
return this.version.isCommunity && !!this.formattedStartDate && !!this.formattedEndDate;
}
async getExportData() {
const adapter = this.store.adapterFor('clients/activity');
const { startTimestamp, endTimestamp } = this.args;
return adapter.exportData({
// the API only accepts json or csv
format: this.exportFormat === 'jsonl' ? 'json' : 'csv',
start_time: startTimestamp,
end_time: endTimestamp,
namespace: this.namespaceFilter,
});
}
parseAPITimestamp = (timestamp, format) => {
return parseAPITimestamp(timestamp, format);
};
exportChartData = task({ drop: true }, async (filename) => {
try {
const contents = await this.getExportData();
this.download.download(filename, contents, this.exportFormat);
this.showExportModal = false;
} catch (e) {
this.downloadError = e.message;
}
});
@action
setExportFormat(evt) {
const { value } = evt.target;
this.exportFormat = value;
}
@action
resetModal() {
this.showExportModal = false;
this.downloadError = '';
}
@action
setEditModalVisible(visible) {
this.showEditModal = visible;
}
}