[UI] - introduce Posthog for Vault Dedicated managed clusters (#30425)

* add dummy provider and wire it into the app

* add tests for analytics service

* add posthog provider

* wire in posthog

* add HVD limitation for analytics and add unit test

* filter out sensitive event properties

* add changelog

* run copywrite headers

* update logging tests for analytics service

* update changelog format

* disable telemetry in test mode

* remove unnecessary test

* self review

* Update vault-reporting addon with analytics tracking changes

* address review feedback

---------

Co-authored-by: Jim Wright <jim.wright@hashicorp.com>
This commit is contained in:
Evan Moncuso 2025-05-23 12:40:29 -07:00 committed by GitHub
parent d7bb0adfe0
commit 689ede2da5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 700 additions and 47 deletions

3
changelog/30425.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
**UI Telemetry**: add Posthog for UI telemetry tracking on HVD managed clusters
```

View File

@ -25,6 +25,7 @@
@text="Console toggle"
data-test-console-toggle
{{on "click" (fn (mut this.console.isOpen) (not this.console.isOpen))}}
{{on "click" this.trackReplToggle}}
/>
<Sidebar::UserMenu />
</:actions>

View File

@ -7,8 +7,15 @@ import Component from '@glimmer/component';
import { service } from '@ember/service';
import { inject as controller } from '@ember/controller';
import { TOGGLE_WEB_REPL } from 'vault/utils/analytic-events';
export default class SidebarNavComponent extends Component {
@service analytics;
@service currentCluster;
@service console;
@controller('vault.cluster') clusterController;
trackReplToggle = () => {
this.analytics.trackEvent(TOGGLE_WEB_REPL);
};
}

View File

@ -5,10 +5,14 @@
import { service } from '@ember/service';
import Route from '@ember/routing/route';
import ControlGroupError from 'vault/lib/control-group-error';
import { action } from '@ember/object';
import config from 'vault/config/environment';
import ControlGroupError from 'vault/lib/control-group-error';
export default class ApplicationRoute extends Route {
@service analytics;
@service controlGroup;
@service('router') routing;
@service('namespace') namespaceService;
@ -69,4 +73,18 @@ export default class ApplicationRoute extends Route {
beforeModel() {
return this.flagsService.fetchFeatureFlags();
}
afterModel() {
const { environment, APP } = config;
const { ANALYTICS_CONFIG } = APP;
// if the app is built for dev -> attempt to start the analytics service based on the config setting
// if the app is built for prod -> attempt to start the analytics service based on the config setting AND HVD ownership
// if the app is built for test -> don't start the analytics service
if (environment === 'development') {
this.analytics.start('posthog', ANALYTICS_CONFIG);
} else if (environment === 'production' && this.flagsService.isHvdManaged) {
this.analytics.start('posthog', ANALYTICS_CONFIG);
}
}
}

View File

@ -15,6 +15,8 @@ import ClusterRoute from 'vault/mixins/cluster-route';
import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
import { assert } from '@ember/debug';
import { v4 as uuidv4 } from 'uuid';
const POLL_INTERVAL_MS = 10000;
export const getManagedNamespace = (nsParam, root) => {
@ -29,6 +31,8 @@ export const getManagedNamespace = (nsParam, root) => {
export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
auth: service(),
api: service(),
analytics: service(),
currentCluster: service(),
customMessages: service(),
flagsService: service('flags'),
@ -124,8 +128,9 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
.cancelOn('deactivate')
.keepLatest(),
afterModel(model, transition) {
async afterModel(model, transition) {
this._super(...arguments);
this.currentCluster.setCluster(model);
if (model.needsInit && this.auth.currentToken) {
// clear token to prevent infinite load state
@ -137,6 +142,37 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
if (this.namespaceService.path && !this.version.hasNamespaces) {
return this.router.transitionTo(this.routeName, { queryParams: { namespace: '' } });
}
// identify user for analytics service
if (this.analytics.activated) {
let licenseId = '';
try {
const licenseStatus = await this.api.sys.systemReadLicenseStatus();
licenseId = licenseStatus?.data?.autoloaded?.licenseId;
} catch (e) {
// license is not retrievable
licenseId = '';
}
try {
const entity_id = this.auth.authData?.entity_id;
const entity = entity_id ? entity_id : `root_${uuidv4()}`;
this.analytics.identifyUser(entity, {
licenseId: licenseId,
licenseState: model.license?.state || 'community',
version: model.version.version,
storageType: model.storageType,
replicationMode: model.replicationMode,
isEnterprise: Boolean(model.license),
});
} catch (e) {
// eslint-disable-next-line no-console
console.log('unable to start analytics', e);
}
}
return this.transitionToTargetRoute(transition);
},

View File

@ -0,0 +1,87 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Service, { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { DummyProvider, PROVIDER_NAME as DummyProviderName } from 'vault/utils/analytics-providers/dummy';
import {
PostHogProvider,
PROVIDER_NAME as PostHogProviderName,
} from 'vault/utils/analytics-providers/posthog';
import type { AnalyticsConfig, AnalyticsProvider } from 'vault/vault/analytics';
import type RouterService from '@ember/routing/router-service';
import config from 'vault/config/environment';
export default class AnalyticsService extends Service {
@service declare readonly router: RouterService;
@tracked activated = false;
@tracked provider: AnalyticsProvider = new DummyProvider();
debug = config.environment === 'development';
private log(...args: unknown[]) {
if (this.debug) {
// eslint-disable-next-line no-console
console.log(`[Analytics - ${this.provider.name}]`, ...args);
}
}
private setupRouteEventListener() {
// on successful route changes...
this.router.on('routeDidChange', () => {
const { currentRouteName } = this.router;
this.trackPageView(currentRouteName || 'unknown-route');
});
}
identifyUser = (identifer: string, traits: Record<string, string>) => {
this.provider.identify(identifer, traits);
this.log('identifyUser', identifer, traits);
};
start = (provider: string, config: AnalyticsConfig) => {
// fail silently, analytics is nonessential
if (!provider) {
this.provider = new DummyProvider();
this.debug = false;
return;
}
// if analytics are not enabled, don't start the service
if (config.enabled) {
switch (provider) {
case DummyProviderName:
this.provider = new DummyProvider();
break;
case PostHogProviderName:
this.provider = new PostHogProvider();
}
// only start things once we've confirmed we want to
this.provider.start(config);
this.activated = true;
this.setupRouteEventListener();
this.log('start');
}
};
trackPageView = (routeName: string, metadata?: Record<string, string>) => {
this.provider.trackPageView(routeName, metadata || {});
this.log('$pageview', routeName, metadata);
};
trackEvent = (eventName: string, metadata: Record<string, string>) => {
this.provider.trackEvent(eventName, metadata);
this.log('custom event', eventName, metadata);
};
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export const PREFIX = 'vault_ui';
/*
buildEventName is a helper to build conformant analytics event names.
While event names are not strictly controlled in the data warehouse, consistent
naming helps find things predictably.
*/
const buildEventName = (category: string, resource: string, action: string) =>
`${PREFIX}_${category}_${resource}_${action}`;
export const TOGGLE_WEB_REPL = buildEventName('core', 'web-repl', 'toggle');

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
/*
Normally, the default export of this file would _do something_.
In the dummy case, the methods for the provider are noops so we
have a safe fallback when analytics is disabled.
*/
import type { AnalyticsProvider } from 'vault/vault/analytics';
export const PROVIDER_NAME = 'dummy';
export class DummyProvider implements AnalyticsProvider {
name = PROVIDER_NAME;
start() {
/* intentionally blank */
}
identify() {
/* intentionally blank */
}
trackPageView() {
/* intentionally blank */
}
trackEvent() {
/* intentionally blank */
}
}

View File

@ -0,0 +1,170 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import posthog from 'posthog-js/dist/module.no-external';
import type { AnalyticsProvider } from 'vault/vault/analytics';
import type { CaptureResult } from 'posthog-js/dist/module.no-external';
interface PostHogConfig {
enabled: boolean;
project_id: string;
api_host: string;
}
/*
formatEvent takes the default posthog capture event and trims it down to only the things we want to collect
PostHog collects a lot of stuff by default, this removes everything pretaining to urls and possible customer info in favor of generic information
*/
const redactEvent = (cr: CaptureResult | null) => {
if (cr === null) return cr;
const { properties } = cr;
// extract ONLY what we need
// this is definitely ridiculous, but it's the best way to be sure we're not accidentally including things that aren't wanted
const {
$browser,
$browser_language,
$browser_language_prefix,
$browser_version,
$device_id,
$device_type,
$groups,
$insert_id,
$is_identified,
$lib,
$lib_rate_limit_remaining_tokens,
$lib_version,
$os,
$os_version,
$pageview_id,
$process_person_profile,
$raw_user_agent,
$recording_status,
$screen_height,
$screen_width,
$sdk_debug_current_session_duration,
$sdk_debug_replay_internal_buffer_length,
$sdk_debug_replay_internal_buffer_size,
$sdk_debug_retry_queue_size,
$sdk_debug_session_start,
$session_id,
$time,
$user_id,
$viewport_height,
$viewport_width,
$window_id,
distinct_id,
routeName,
token,
} = properties;
// replay into the sent object
return {
...cr,
properties: {
$browser,
$browser_version,
$browser_language,
$browser_language_prefix,
$os,
$os_version,
$device_type,
$raw_user_agent,
$screen_height,
$screen_width,
$viewport_height,
$viewport_width,
$lib,
$lib_version,
$insert_id,
$time,
$device_id,
$user_id,
$groups,
$session_id,
$window_id,
$recording_status,
$sdk_debug_replay_internal_buffer_length,
$sdk_debug_replay_internal_buffer_size,
$sdk_debug_current_session_duration,
$sdk_debug_session_start,
$sdk_debug_retry_queue_size,
$pageview_id,
$is_identified,
$process_person_profile,
$lib_rate_limit_remaining_tokens,
distinct_id,
routeName,
token,
},
};
};
export const PROVIDER_NAME = 'posthog';
export class PostHogProvider implements AnalyticsProvider {
name = PROVIDER_NAME;
client = posthog;
licenseId = '';
get anonymousId() {
return this.client.get_distinct_id();
}
start(config: unknown) {
const { enabled, project_id, api_host } = config as PostHogConfig;
if (enabled) {
this.client.init(project_id, {
api_host,
person_profiles: 'identified_only',
persistence: 'memory',
autocapture: false,
disable_session_recording: true,
advanced_disable_decide: true,
capture_pageview: false,
before_send: redactEvent,
});
}
}
identify(identifier: string, traits: Record<string, string>) {
this.licenseId = traits['licenseId'] || '';
this.client.identify(identifier, {
...traits,
});
}
trackPageView(routeName: string /* metadata: Record<string, string> */) {
// use licenseId as a grouping for this cluster
if (this.licenseId) {
this.client.capture('$pageview', {
routeName,
$groups: {
licenseId: this.licenseId,
},
});
} else {
this.client.capture('$pageview', { routeName });
}
}
trackEvent(eventName: string, metadata: Record<string, string> = {}) {
// use licenseId as a grouping for this cluster
if (this.licenseId) {
this.client.capture(eventName, {
...metadata,
$groups: {
licenseId: this.licenseId,
},
});
} else {
this.client.capture(eventName, metadata);
}
}
}

View File

@ -4,20 +4,24 @@
*/
module.exports = function (environment) {
const policy = {
'default-src': ["'none'"],
'script-src': ["'self'"],
'font-src': ["'self'"],
'connect-src': ["'self'"],
'img-src': ["'self'", 'data:'],
'style-src': ["'unsafe-inline'", "'self'"],
'media-src': ["'self'"],
'form-action': ["'none'"],
};
policy['connect-src'].push('https://eu.i.posthog.com');
return {
delivery: ['header', 'meta'],
enabled: environment !== 'production',
failTests: true,
policy: {
'default-src': ["'none'"],
'script-src': ["'self'"],
'font-src': ["'self'"],
'connect-src': ["'self'"],
'img-src': ["'self'", 'data:'],
'style-src': ["'unsafe-inline'", "'self'"],
'media-src': ["'self'"],
'form-action': ["'none'"],
},
policy,
reportOnly: false,
};
};

View File

@ -38,12 +38,15 @@ module.exports = function (environment) {
],
// number of records to show on a single page by default - this is used by the client-side pagination
DEFAULT_PAGE_SIZE: 100,
ANALYTICS_CONFIG: { enabled: false },
},
flashMessageDefaults: {
timeout: 7000,
sticky: false,
},
};
if (environment === 'development') {
// ENV.APP.LOG_RESOLVER = true;
// ENV.APP.LOG_ACTIVE_GENERATION = true;
@ -56,6 +59,15 @@ module.exports = function (environment) {
handler: process.env.MIRAGE_DEV_HANDLER,
};
}
if (process.env.ENABLE_POSTHOG) {
ENV.APP.ANALYTICS_CONFIG = {
provider: 'posthog',
enabled: true,
project_id: 'phc_zPQ9fPlFj4ZTYKJmThG1C8AE4J4RgPQx8dJJ7agg4SG',
api_host: 'https://eu.i.posthog.com',
};
}
}
if (environment === 'test') {
@ -70,11 +82,22 @@ module.exports = function (environment) {
ENV['ember-cli-mirage'] = {
enabled: false,
};
ENV.APP.ANALYTICS_CONFIG = { enabled: false };
}
if (environment !== 'production') {
ENV.APP.DEFAULT_PAGE_SIZE = 15;
}
if (environment === 'production') {
ENV.APP.ANALYTICS_CONFIG = {
provider: 'posthog',
enabled: true,
project_id: 'phc_pIw6t5numW5jDram4dnJjSnwDOorf9IGd1MmlFp0dHh',
api_host: 'https://eu.i.posthog.com',
};
}
ENV.welcomeMessage = process.env.UI_AUTH_WELCOME;
return ENV;

View File

@ -222,6 +222,7 @@
"handlebars": "4.7.8",
"highlight.js": "10.7.3",
"node-notifier": "8.0.2",
"posthog-js": "^1.202.2",
"uuid": "9.0.1"
},
"packageManager": "yarn@3.5.0"

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import sinon from 'sinon';
import { setupTest } from 'vault/tests/helpers';
class ProviderStub {
name = 'testing';
start = sinon.stub();
identify = sinon.stub();
trackPageView = sinon.stub();
}
module('Unit | Service | analytics', function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
this.service = this.owner.lookup('service:analytics');
});
hooks.afterEach(function () {
sinon.reset();
});
test('#identifyUser passes data to the provider', function (assert) {
const providerStub = new ProviderStub();
this.service.provider = providerStub;
const identifier = 'carl';
const traits = { apples: 'oranges' };
this.service.identifyUser(identifier, traits);
assert.true(providerStub.identify.calledOnce, 'the service calls identify on the provider');
assert.true(
providerStub.identify.calledWith(identifier, traits),
'the provider recieves the expected id and traits'
);
});
test('#trackPageView passes data to the provider', function (assert) {
const providerStub = new ProviderStub();
this.service.provider = providerStub;
this.service.trackPageView('test', { currentRouteName: 'ham' });
assert.true(providerStub.trackPageView.called, 'it calls the tracking method on the provider');
assert.true(
providerStub.trackPageView.calledWith('test', { currentRouteName: 'ham' }),
'it passes the correct args to the provider'
);
});
module('#log', function (hooks) {
hooks.beforeEach(function () {
// eslint-disable-next-line no-console
console.log = sinon.stub(console, 'log');
});
hooks.afterEach(function () {
// eslint-disable-next-line no-console
console.log.restore();
});
test('logging is not shown when inactive', function (assert) {
this.service.debug = false;
// for the next few lines, console.log WILL NOT WORK AS EXPECTED
this.service.trackPageView('a', null);
// eslint-disable-next-line no-console
assert.true(console.log.notCalled, 'console.log is called');
});
});
});

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export interface AnalyticsProvider {
name: string;
identify: (identifier: string, traits: Record<string, string>) => void;
start: (config: Record<string, string | boolean>) => void;
trackPageView: (routeName: string, metadata: Record<string, string>) => void;
trackEvent: (eventName: string, metadata: Record<string, string>) => void;
}
export interface AnalyticsConfig extends Record<string, string | boolean> {
enabled: boolean;
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"version":3,"file":"title-row.d.ts","sourceRoot":"","sources":["../../../../src/components/vault-reporting/base/title-row.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAGL,iBAAiB,EAClB,MAAM,gDAAgD,CAAC;AACxD,OAAO,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE;QACJ,qCAAqC;QACrC,KAAK,EAAE,MAAM,CAAC;QACd,6DAA6D;QAC7D,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;QAClC,wDAAwD;QACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gEAAgE;QAChE,QAAQ,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;QACrC,gEAAgE;QAChE,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gDAAgD;QAChD,UAAU,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;KACjC,CAAC;IAEF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;KACb,CAAC;IAEF,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IAChE,IAAI,OAAO,uBAEV;IAED,IAAI,QAAQ,WAEX;IAED,IAAI,OAAO,WAEV;IAED,IAAI,QAAQ,urTAEX;IAED,IAAI,UAAU,uBAEb;CAqDF"}
{"version":3,"file":"title-row.d.ts","sourceRoot":"","sources":["../../../../src/components/vault-reporting/base/title-row.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAGL,iBAAiB,EAClB,MAAM,gDAAgD,CAAC;AACxD,OAAO,kBAAkB,CAAC;AAI1B,OAAO,KAAK,yBAAyB,MAAM,uCAAuC,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE;QACJ,qCAAqC;QACrC,KAAK,EAAE,MAAM,CAAC;QACd,6DAA6D;QAC7D,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;QAClC,wDAAwD;QACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gEAAgE;QAChE,QAAQ,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;QACrC,gEAAgE;QAChE,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gDAAgD;QAChD,UAAU,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;KACjC,CAAC;IAEF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;KACb,CAAC;IAEF,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IAChE,SAA0B,kBAAkB,EAAE,yBAAyB,CAAC;IAExE,IAAI,OAAO,uBAEV;IAED,IAAI,QAAQ,WAEX;IAED,IAAI,OAAO,WAEV;IAED,IAAI,QAAQ,urTAEX;IAED,IAAI,UAAU,uBAEb;IAED,eAAe,aAMb;CAsDH"}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"version":3,"file":"cluster-replication.d.ts","sourceRoot":"","sources":["../../../src/components/vault-reporting/cluster-replication.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAO3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAIjE,OAAO,4BAA4B,CAAC;AAEpC,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE;QACJ,qBAAqB,EAAE,yBAAyB,GAAG,UAAU,CAAC;QAC9D,gBAAgB,EAAE,yBAAyB,GAAG,UAAU,CAAC;QACzD,gBAAgB,EAAE,OAAO,CAAC;KAC3B,CAAC;IAEF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;KACb,CAAC;IAEF,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,SAAS,CAAC,2BAA2B,CAAC;IACpF,QAAQ,GAAI,QAAO,yBAAyB,GAAG,UAAuB,4CAEpE;IAEF,IAAI,OAAO,YAKV;IAED,IAAI,WAAW,sGAQd;IAED,OAAO,GAAI,QAAO,yBAAyB,GAAG,UAAuB,osTAQnE;IAEF,QAAQ,GAAI,QAAO,yBAAyB,GAAG,UAAuB,oGAQpE;IAEF,IAAI,OAAO,8BAMV;CAuDF"}
{"version":3,"file":"cluster-replication.d.ts","sourceRoot":"","sources":["../../../src/components/vault-reporting/cluster-replication.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAO3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAIjE,OAAO,4BAA4B,CAAC;AAEpC,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE;QACJ,qBAAqB,EAAE,yBAAyB,GAAG,UAAU,CAAC;QAC9D,gBAAgB,EAAE,yBAAyB,GAAG,UAAU,CAAC;QACzD,gBAAgB,EAAE,OAAO,CAAC;KAC3B,CAAC;IAEF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;KACb,CAAC;IAEF,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,SAAS,CAAC,2BAA2B,CAAC;IACpF,QAAQ,WAAW,yBAAyB,GAAG,UAAU,4CAEvD;IAEF,IAAI,OAAO,YAKV;IAED,IAAI,WAAW,sGAQd;IAED,OAAO,WAAW,yBAAyB,GAAG,UAAU,osTAQtD;IAEF,QAAQ,WAAW,yBAAyB,GAAG,UAAU,oGAQvD;IAEF,IAAI,OAAO,8BAMV;CAuDF"}

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@
*/
import Component from '@glimmer/component';
import type { UsageDashboardData } from '../../../types';
import type ReportingAnalyticsService from '../../../services/reporting-analytics';
export interface DashboardExportSignature {
Args: {
data?: UsageDashboardData;
@ -15,6 +16,9 @@ export interface DashboardExportSignature {
}
export default class DashboardExport extends Component<DashboardExportSignature> {
#private;
readonly reportingAnalytics: ReportingAnalyticsService;
handleTrackExportToggle: () => void;
handleTrackExportOption: (option: string) => void;
get dataAsDownloadableJSONString(): string;
get dataAsDownloadableCSVString(): string;
}

View File

@ -1 +1 @@
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../../src/components/vault-reporting/dashboard/export.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAG3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE;QACJ,IAAI,CAAC,EAAE,kBAAkB,CAAC;KAC3B,CAAC;IAEF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;KACb,CAAC;IACF,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,SAAS,CAAC,wBAAwB,CAAC;;IAO9E,IAAI,4BAA4B,WAM/B;IAED,IAAI,2BAA2B,WA2D9B;CA2CF"}
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../../src/components/vault-reporting/dashboard/export.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAK3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,yBAAyB,MAAM,uCAAuC,CAAC;AAGnF,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE;QACJ,IAAI,CAAC,EAAE,kBAAkB,CAAC;KAC3B,CAAC;IAEF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;KACb,CAAC;IACF,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,SAAS,CAAC,wBAAwB,CAAC;;IAC9E,SAA0B,kBAAkB,EAAE,yBAAyB,CAAC;IAQxE,uBAAuB,aAErB;IAEF,uBAAuB,WAAY,MAAM,UAEvC;IAEF,IAAI,4BAA4B,WAM/B;IAED,IAAI,2BAA2B,WA2D9B;CA8CF"}

View File

@ -1 +1 @@
{"version":3,"file":"horizontal-bar-chart.d.ts","sourceRoot":"","sources":["../../../src/components/vault-reporting/horizontal-bar-chart.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,6BAA6B,CAAC;AACrC,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAKL,iBAAiB,EAClB,MAAM,gDAAgD,CAAC;AAWxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,4EAA4E,CAAC;AAE/H,MAAM,WAAW,uCAAuC;IACtD,IAAI,EAAE;QACJ,IAAI,EAAE,WAAW,EAAE,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,wDAAwD;QACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gEAAgE;QAChE,QAAQ,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;QACrC,gEAAgE;QAChE,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gDAAgD;QAChD,UAAU,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;KACjC,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;QACZ;;;;;;;aAOK;QACL,KAAK,EAAE,4BAA4B,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC;KAC1D,CAAC;IAEF,OAAO,EAAE,WAAW,CAAC;CACtB;AACD,MAAM,CAAC,OAAO,OAAO,8BAA+B,SAAQ,SAAS,CAAC,uCAAuC,CAAC;IACnG,iBAAiB,SAAK;IAE/B,IAAI,OAAO,YAMV;IAED,IAAI,IAAI,kBAUP;IAED,IAAI,KAAK,WAIR;IAED,IAAI,SAAS,WAUZ;IAED,IAAI,OAAO,aAEV;IAED,IAAI,OAAO,aAEV;IAED,IAAI,WAAW,WAEd;IAED,IAAI,MAAM,aAET;IAED,IAAI,eAAe,WAElB;IAED,IAAI,qBAAqB,WAGxB;IAED,IAAI,kBAAkB,WAGrB;IAED,IAAI,WAAW,uBAId;IAED,IAAI,OAAO,uBAIV;IAED,SAAS,GAAI,OAAO,MAAM,cAExB;IAEF,gBAAgB,GAAI,aAAa,MAAM,UAErC;CAoKH"}
{"version":3,"file":"horizontal-bar-chart.d.ts","sourceRoot":"","sources":["../../../src/components/vault-reporting/horizontal-bar-chart.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,6BAA6B,CAAC;AACrC,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAKL,iBAAiB,EAClB,MAAM,gDAAgD,CAAC;AAWxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,4EAA4E,CAAC;AAE/H,MAAM,WAAW,uCAAuC;IACtD,IAAI,EAAE;QACJ,IAAI,EAAE,WAAW,EAAE,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,wDAAwD;QACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gEAAgE;QAChE,QAAQ,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;QACrC,gEAAgE;QAChE,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gDAAgD;QAChD,UAAU,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;KACjC,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;QACZ;;;;;;;aAOK;QACL,KAAK,EAAE,4BAA4B,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC;KAC1D,CAAC;IAEF,OAAO,EAAE,WAAW,CAAC;CACtB;AACD,MAAM,CAAC,OAAO,OAAO,8BAA+B,SAAQ,SAAS,CAAC,uCAAuC,CAAC;IACnG,iBAAiB,SAAK;IAE/B,IAAI,OAAO,YAMV;IAED,IAAI,IAAI,kBAUP;IAED,IAAI,KAAK,WAIR;IAED,IAAI,SAAS,WAUZ;IAED,IAAI,OAAO,aAEV;IAED,IAAI,OAAO,aAEV;IAED,IAAI,WAAW,WAEd;IAED,IAAI,MAAM,aAET;IAED,IAAI,eAAe,WAElB;IAED,IAAI,qBAAqB,WAGxB;IAED,IAAI,kBAAkB,WAGrB;IAED,IAAI,WAAW,uBAId;IAED,IAAI,OAAO,uBAIV;IAED,SAAS,UAAW,MAAM,cAExB;IAEF,gBAAgB,gBAAiB,MAAM,UAErC;CAoKH"}

View File

@ -6,6 +6,7 @@ import Component from '@glimmer/component';
import './dashboard.scss';
import type { UsageDashboardData, SimpleDatum, getUsageDataFunction } from '../../../types';
import type { IconName } from '@hashicorp/flight-icons/svg';
import type ReportingAnalyticsService from '../../../services/reporting-analytics';
interface CounterBlock {
title: string;
tooltipMessage: string;
@ -27,11 +28,15 @@ export interface SSUViewDashboardSignature {
Element: HTMLElement;
}
export default class SSUViewDashboard extends Component<SSUViewDashboardSignature> {
readonly reportingAnalytics: ReportingAnalyticsService;
data?: UsageDashboardData;
lastUpdatedTime: string;
error?: unknown;
constructor(owner: unknown, args: SSUViewDashboardSignature['Args']);
fetchAllData: () => void;
handleTrackAnalyticsEvent: (eventName: string, properties?: object, options?: object) => void;
handleTrackSurveyLink: () => void;
handleRefresh: () => void;
getBarChartData: (map: Record<string, number>, exclude?: string[]) => SimpleDatum[];
get isVaultDedicated(): boolean;
get kvSecretsTooltipMessage(): string;

View File

@ -1 +1 @@
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../../src/components/vault-reporting/views/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAE3C,OAAO,kBAAkB,CAAC;AAO1B,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAU5D,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE;QACJ,gBAAgB,EAAE,oBAAoB,CAAC;QACvC,gBAAgB,EAAE,OAAO,CAAC;KAC3B,CAAC;IAEF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;KACb,CAAC;IAEF,OAAO,EAAE,WAAW,CAAC;CACtB;AACD,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,SAAS,CAAC,yBAAyB,CAAC;IAEhF,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAG1B,eAAe,EAAE,MAAM,CAAM;IAG7B,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEJ,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,yBAAyB,CAAC,MAAM,CAAC;IAKnE,YAAY,EAAE,MAAM,IAAI,CAUtB;IAEF,eAAe,EAAE,CACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,KACf,WAAW,EAAE,CAWhB;IAEF,IAAI,gBAAgB,IAAI,OAAO,CAE9B;IAED,IAAI,uBAAuB,IAAI,MAAM,CAepC;IAED,IAAI,QAAQ,IAAI,YAAY,EAAE,CAgC7B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;CAkNF"}
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../../src/components/vault-reporting/views/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAE3C,OAAO,kBAAkB,CAAC;AAO1B,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAU5D,OAAO,KAAK,yBAAyB,MAAM,uCAAuC,CAAC;AAGnF,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE;QACJ,gBAAgB,EAAE,oBAAoB,CAAC;QACvC,gBAAgB,EAAE,OAAO,CAAC;KAC3B,CAAC;IAEF,MAAM,EAAE;QACN,OAAO,EAAE,EAAE,CAAC;KACb,CAAC;IAEF,OAAO,EAAE,WAAW,CAAC;CACtB;AACD,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,SAAS,CAAC,yBAAyB,CAAC;IAChF,SAA0B,kBAAkB,EAAE,yBAAyB,CAAC;IAGxE,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAG1B,eAAe,EAAE,MAAM,CAAM;IAG7B,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEJ,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,yBAAyB,CAAC,MAAM,CAAC;IAKnE,YAAY,EAAE,MAAM,IAAI,CAUtB;IAEF,yBAAyB,cACZ,MAAM,eACJ,MAAM,YACT,MAAM,UAGhB;IAEF,qBAAqB,EAAE,MAAM,IAAI,CAE/B;IAEF,aAAa,EAAE,MAAM,IAAI,CAGvB;IAEF,eAAe,EAAE,CACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,KACf,WAAW,EAAE,CAWhB;IAEF,IAAI,gBAAgB,IAAI,OAAO,CAE9B;IAED,IAAI,uBAAuB,IAAI,MAAM,CAepC;IAED,IAAI,QAAQ,IAAI,YAAY,EAAE,CAgC7B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;CAoNF"}

View File

@ -3,6 +3,10 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Service from '@ember/service';
/**
* This service is used to look up the `analytics` service in the host application and track events if it exists. If it doesn't exist
* or the implementation breaks it falls back gracefully to do nothing.
*/
export default class ReportingAnalytics extends Service {
get analytics(): {
trackEvent: (event: string, properties?: object, options?: object) => void;

View File

@ -1 +1 @@
{"version":3,"file":"reporting-analytics.d.ts","sourceRoot":"","sources":["../../src/services/reporting-analytics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,OAAO,MAAM,gBAAgB,CAAC;AAErC,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,OAAO;IAGrD,IAAI,SAAS,IAEP;QACE,UAAU,EAAE,CACV,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,MAAM,KACb,IAAI,CAAC;KACX,GACD,SAAS,CACd;IAED,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAUhE;AAGD,OAAO,QAAQ,gBAAgB,CAAC;IAC9B,UAAU,QAAQ;QAChB,kBAAkB,EAAE,kBAAkB,CAAC;KACxC;CACF"}
{"version":3,"file":"reporting-analytics.d.ts","sourceRoot":"","sources":["../../src/services/reporting-analytics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,OAAO,MAAM,gBAAgB,CAAC;AACrC;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,OAAO;IAGrD,IAAI,SAAS,IAEP;QACE,UAAU,EAAE,CACV,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,MAAM,KACb,IAAI,CAAC;KACX,GACD,SAAS,CACd;IAED,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAahE;AAGD,OAAO,QAAQ,gBAAgB,CAAC;IAC9B,UAAU,QAAQ;QAChB,kBAAkB,EAAE,kBAAkB,CAAC;KACxC;CACF"}

View File

@ -0,0 +1,5 @@
/**
* Copyright (c) HashiCorp, Inc.
*/
export { default } from "@hashicorp/vault-reporting/services/reporting-analytics";

View File

@ -1,13 +1,20 @@
import Component from '@glimmer/component';
import { HdsTextBody, HdsLinkStandalone, HdsTextDisplay } from '@hashicorp/design-system-components/components';
import { on } from '@ember/modifier';
import { service } from '@ember/service';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@ember/component';
import { g, i } from 'decorator-transforms/runtime';
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
class TitleRow extends Component {
static {
g(this.prototype, "reportingAnalytics", [service]);
}
#reportingAnalytics = (i(this, "reportingAnalytics"), void 0);
get hasLink() {
return this.args.linkUrl;
}
@ -23,12 +30,20 @@ class TitleRow extends Component {
get linkTarget() {
return this.args.linkTarget || '_self';
}
handleLinkClick = () => {
this.reportingAnalytics.trackEvent(`card_link`, {
card: this.args.title,
link: this.linkText,
target: this.linkTarget
});
};
static {
setComponentTemplate(precompileTemplate("\n <div class=\"ssu-title-row\" data-test-dashboard-card-title-row>\n <div class=\"ssu-title-row__container\">\n <HdsTextDisplay data-test-dashboard-card-title @size=\"300\">\n {{@title}}\n </HdsTextDisplay>\n\n {{#if this.hasLink}}\n <HdsLinkStandalone data-test-dashboard-card-title-link class=\"ssu-title-row__container__link\" @text={{this.linkText}} @href={{this.linkUrl}} @icon={{this.linkIcon}} target={{this.linkTarget}} @iconPosition=\"trailing\" />\n {{/if}}\n </div>\n\n {{#if @description}}\n <HdsTextBody class=\"ssu-title-row__description\" data-test-dashboard-card-description>\n {{@description}}\n </HdsTextBody>\n {{/if}}\n </div>\n ", {
setComponentTemplate(precompileTemplate("\n <div class=\"ssu-title-row\" data-test-vault-reporting-dashboard-card-title-row>\n <div class=\"ssu-title-row__container\">\n <HdsTextDisplay data-test-vault-reporting-dashboard-card-title @size=\"300\">\n {{@title}}\n </HdsTextDisplay>\n\n {{#if this.hasLink}}\n <HdsLinkStandalone data-test-vault-reporting-dashboard-card-title-link class=\"ssu-title-row__container__link\" @text={{this.linkText}} @href={{this.linkUrl}} @icon={{this.linkIcon}} target={{this.linkTarget}} @iconPosition=\"trailing\" {{on \"click\" this.handleLinkClick}} />\n {{/if}}\n </div>\n\n {{#if @description}}\n <HdsTextBody class=\"ssu-title-row__description\" data-test-vault-reporting-dashboard-card-description>\n {{@description}}\n </HdsTextBody>\n {{/if}}\n </div>\n ", {
strictMode: true,
scope: () => ({
HdsTextDisplay,
HdsLinkStandalone,
on,
HdsTextBody
})
}), this);

View File

@ -1 +1 @@
{"version":3,"file":"title-row.js","sources":["../../../../src/components/vault-reporting/base/title-row.gts"],"sourcesContent":["/**\n * Copyright (c) HashiCorp, Inc.\n * SPDX-License-Identifier: BUSL-1.1\n */\n\nimport Component from '@glimmer/component';\nimport {\n HdsTextDisplay,\n HdsTextBody,\n HdsLinkStandalone,\n} from '@hashicorp/design-system-components/components';\nimport './title-row.scss';\nimport type { SafeString } from '@ember/template';\n\n/**\n * TitleRow Component\n *\n * A reusable component that displays a title with an optional description and link.\n * Used in dashboard cards to create consistent header styling.\n */\nexport interface TitleRowSignature {\n Args: {\n /** The main title text to display */\n title: string;\n /** Optional description text to display beneath the title */\n description?: string | SafeString;\n /** Custom text for the link (defaults to \"View all\") */\n linkText?: string;\n /** Icon to display with the link (defaults to \"arrow-right\") */\n linkIcon?: HdsLinkStandalone['icon'];\n /** URL for the link - if not provided, no link will be shown */\n linkUrl?: string;\n /** Target for the link - defaults to \"_self\" */\n linkTarget?: '_blank' | '_self';\n };\n\n Blocks: {\n default: [];\n };\n\n Element: HTMLElement;\n}\n\nexport default class TitleRow extends Component<TitleRowSignature> {\n get hasLink() {\n return this.args.linkUrl;\n }\n\n get linkText() {\n return this.args.linkText || 'View all';\n }\n\n get linkUrl() {\n return this.args.linkUrl || '#';\n }\n\n get linkIcon() {\n return this.args.linkIcon || 'arrow-right';\n }\n\n get linkTarget() {\n return this.args.linkTarget || '_self';\n }\n\n <template>\n <div class=\"ssu-title-row\" data-test-dashboard-card-title-row>\n <div class=\"ssu-title-row__container\">\n <HdsTextDisplay data-test-dashboard-card-title @size=\"300\">\n {{@title}}\n </HdsTextDisplay>\n\n {{#if this.hasLink}}\n <HdsLinkStandalone\n data-test-dashboard-card-title-link\n class=\"ssu-title-row__container__link\"\n @text={{this.linkText}}\n @href={{this.linkUrl}}\n @icon={{this.linkIcon}}\n target={{this.linkTarget}}\n @iconPosition=\"trailing\"\n />\n {{/if}}\n </div>\n\n {{#if @description}}\n <HdsTextBody\n class=\"ssu-title-row__description\"\n data-test-dashboard-card-description\n >\n {{@description}}\n </HdsTextBody>\n {{/if}}\n </div>\n </template>\n}\n"],"names":["TitleRow","Component","hasLink","args","linkUrl","linkText","linkIcon","linkTarget","setComponentTemplate","precompileTemplate","strictMode","scope","HdsTextDisplay","HdsLinkStandalone","HdsTextBody"],"mappings":";;;;;AAAA;;;AAGC;AAwCc,MAAMA,iBAAiBC,SAAU,CAAA;EAC9C,IAAIC,OAAUA,GAAA;AACZ,IAAA,OAAO,IAAI,CAACC,IAAI,CAACC,OAAO;AAC1B;EAEA,IAAIC,QAAWA,GAAA;AACb,IAAA,OAAO,IAAI,CAACF,IAAI,CAACE,QAAQ,IAAI,UAAA;AAC/B;EAEA,IAAID,OAAUA,GAAA;AACZ,IAAA,OAAO,IAAI,CAACD,IAAI,CAACC,OAAO,IAAI,GAAA;AAC9B;EAEA,IAAIE,QAAWA,GAAA;AACb,IAAA,OAAO,IAAI,CAACH,IAAI,CAACG,QAAQ,IAAI,aAAA;AAC/B;EAEA,IAAIC,UAAaA,GAAA;AACf,IAAA,OAAO,IAAI,CAACJ,IAAI,CAACI,UAAU,IAAI,OAAA;AACjC;AAEA,EAAA;IAAAC,oBAAA,CAAAC,kBAAA,CA6BA,4uBAAA,EAAA;MAAAC,UAAA,EAAA,IAAA;AAAAC,MAAAA,KAAA,EAAAA,OAAA;QAAAC,cAAA;QAAAC,iBAAA;AAAAC,QAAAA;AAAA,OAAA;KAAU,CAAA,EAAV,IAAW,CAAA;AAAD;AACZ;;;;"}
{"version":3,"file":"title-row.js","sources":["../../../../src/components/vault-reporting/base/title-row.gts"],"sourcesContent":["/**\n * Copyright (c) HashiCorp, Inc.\n * SPDX-License-Identifier: BUSL-1.1\n */\n\nimport Component from '@glimmer/component';\nimport {\n HdsTextDisplay,\n HdsTextBody,\n HdsLinkStandalone,\n} from '@hashicorp/design-system-components/components';\nimport './title-row.scss';\nimport { on } from '@ember/modifier';\n\nimport { service } from '@ember/service';\nimport type ReportingAnalyticsService from '../../../services/reporting-analytics';\nimport type { SafeString } from '@ember/template';\n\n/**\n * TitleRow Component\n *\n * A reusable component that displays a title with an optional description and link.\n * Used in dashboard cards to create consistent header styling.\n */\nexport interface TitleRowSignature {\n Args: {\n /** The main title text to display */\n title: string;\n /** Optional description text to display beneath the title */\n description?: string | SafeString;\n /** Custom text for the link (defaults to \"View all\") */\n linkText?: string;\n /** Icon to display with the link (defaults to \"arrow-right\") */\n linkIcon?: HdsLinkStandalone['icon'];\n /** URL for the link - if not provided, no link will be shown */\n linkUrl?: string;\n /** Target for the link - defaults to \"_self\" */\n linkTarget?: '_blank' | '_self';\n };\n\n Blocks: {\n default: [];\n };\n\n Element: HTMLElement;\n}\n\nexport default class TitleRow extends Component<TitleRowSignature> {\n @service declare readonly reportingAnalytics: ReportingAnalyticsService;\n\n get hasLink() {\n return this.args.linkUrl;\n }\n\n get linkText() {\n return this.args.linkText || 'View all';\n }\n\n get linkUrl() {\n return this.args.linkUrl || '#';\n }\n\n get linkIcon() {\n return this.args.linkIcon || 'arrow-right';\n }\n\n get linkTarget() {\n return this.args.linkTarget || '_self';\n }\n\n handleLinkClick = () => {\n this.reportingAnalytics.trackEvent(`card_link`, {\n card: this.args.title,\n link: this.linkText,\n target: this.linkTarget,\n });\n };\n\n <template>\n <div\n class=\"ssu-title-row\"\n data-test-vault-reporting-dashboard-card-title-row\n >\n <div class=\"ssu-title-row__container\">\n <HdsTextDisplay\n data-test-vault-reporting-dashboard-card-title\n @size=\"300\"\n >\n {{@title}}\n </HdsTextDisplay>\n\n {{#if this.hasLink}}\n <HdsLinkStandalone\n data-test-vault-reporting-dashboard-card-title-link\n class=\"ssu-title-row__container__link\"\n @text={{this.linkText}}\n @href={{this.linkUrl}}\n @icon={{this.linkIcon}}\n target={{this.linkTarget}}\n @iconPosition=\"trailing\"\n {{on \"click\" this.handleLinkClick}}\n />\n {{/if}}\n </div>\n\n {{#if @description}}\n <HdsTextBody\n class=\"ssu-title-row__description\"\n data-test-vault-reporting-dashboard-card-description\n >\n {{@description}}\n </HdsTextBody>\n {{/if}}\n </div>\n </template>\n}\n"],"names":["TitleRow","Component","g","prototype","service","i","void 0","hasLink","args","linkUrl","linkText","linkIcon","linkTarget","handleLinkClick","reportingAnalytics","trackEvent","card","title","link","target","setComponentTemplate","precompileTemplate","strictMode","scope","HdsTextDisplay","HdsLinkStandalone","on","HdsTextBody"],"mappings":";;;;;;;;AAAA;;;AAGC;AA4Cc,MAAMA,iBAAiBC,SAAU,CAAA;AAAA,EAAA;IAAAC,CAAA,CAAA,IAAA,CAAAC,SAAA,EAAA,oBAAA,EAAA,CAC7CC,OAAA,CAAA,CAAA;AAAA;AAAA,EAAA,mBAAA,IAAAC,CAAA,CAAA,IAAA,EAAA,oBAAA,CAAA,EAAAC,MAAA;EAED,IAAIC,OAAUA,GAAA;AACZ,IAAA,OAAO,IAAI,CAACC,IAAI,CAACC,OAAO;AAC1B;EAEA,IAAIC,QAAWA,GAAA;AACb,IAAA,OAAO,IAAI,CAACF,IAAI,CAACE,QAAQ,IAAI,UAAA;AAC/B;EAEA,IAAID,OAAUA,GAAA;AACZ,IAAA,OAAO,IAAI,CAACD,IAAI,CAACC,OAAO,IAAI,GAAA;AAC9B;EAEA,IAAIE,QAAWA,GAAA;AACb,IAAA,OAAO,IAAI,CAACH,IAAI,CAACG,QAAQ,IAAI,aAAA;AAC/B;EAEA,IAAIC,UAAaA,GAAA;AACf,IAAA,OAAO,IAAI,CAACJ,IAAI,CAACI,UAAU,IAAI,OAAA;AACjC;EAEAC,eAAkB,GAAAA,MAAA;AAChB,IAAA,IAAI,CAACC,kBAAkB,CAACC,UAAU,CAAC,WAAW,EAAE;AAC9CC,MAAAA,IAAA,EAAM,IAAI,CAACR,IAAI,CAACS,KAAK;MACrBC,IAAM,EAAA,IAAI,CAACR,QAAQ;MACnBS,MAAQ,EAAA,IAAI,CAACP;AACf,KAAA,CAAA;GACA;AAEF,EAAA;IAAAQ,oBAAA,CAAAC,kBAAA,CAoCA,k1BAAA,EAAA;MAAAC,UAAA,EAAA,IAAA;AAAAC,MAAAA,KAAA,EAAAA,OAAA;QAAAC,cAAA;QAAAC,iBAAA;QAAAC,EAAA;AAAAC,QAAAA;AAAA,OAAA;KAAU,CAAA,EAAV,IAAW,CAAA;AAAD;AACZ;;;;"}

View File

@ -18,7 +18,7 @@ class ClusterReplication extends Component {
}
get description() {
if (this.isEmpty) {
return htmlSafe('Enable <a class="hds-link-inline--color-secondary" href="https://developer.hashicorp.com/vault/docs/internals/replication" target="_blank" data-test-cluster-replication-description-link>replication</a> to replicate data across clusters.');
return htmlSafe('Enable <a class="hds-link-inline--color-secondary" href="https://developer.hashicorp.com/vault/docs/internals/replication" target="_blank" data-test-vault-reporting-cluster-replication-description-link>replication</a> to replicate data across clusters.');
} else {
return 'Status of disaster recovery and performance replication.';
}
@ -51,7 +51,7 @@ class ClusterReplication extends Component {
return 'replication';
}
static {
setComponentTemplate(precompileTemplate("\n <HdsCardContainer data-test-cluster-replication @hasBorder={{true}} class=\"ssu-cluster-replication\" ...attributes>\n <TitleRow @title=\"Cluster replication\" @description={{this.description}} @linkUrl={{this.linkUrl}} />\n\n <HdsTextBody @size=\"300\" data-test-cluster-replication-dr-row>\n Disaster Recovery\n <HdsBadge class=\"ssu-cluster-replication__list-row__badge\" data-test-cluster-replication-dr-badge @icon={{this.getIcon @disasterRecoveryState}} @text={{this.getState @disasterRecoveryState}} @color={{this.getColor @disasterRecoveryState}} @type=\"outlined\" @size=\"small\" />\n </HdsTextBody>\n\n <HdsTextBody @size=\"300\" data-test-cluster-replication-perf-row>\n Performance\n <HdsBadge class=\"ssu-cluster-replication__list-row__badge\" data-test-cluster-replication-perf-badge @icon={{this.getIcon @performanceState}} @text={{this.getState @performanceState}} @color={{this.getColor @performanceState}} @type=\"outlined\" @size=\"small\" />\n </HdsTextBody>\n\n </HdsCardContainer>\n ", {
setComponentTemplate(precompileTemplate("\n <HdsCardContainer data-test-vault-reporting-cluster-replication @hasBorder={{true}} class=\"ssu-cluster-replication\" ...attributes>\n <TitleRow @title=\"Cluster replication\" @description={{this.description}} @linkUrl={{this.linkUrl}} />\n\n <HdsTextBody @size=\"300\" data-test-vault-reporting-cluster-replication-dr-row>\n Disaster Recovery\n <HdsBadge class=\"ssu-cluster-replication__list-row__badge\" data-test-vault-reporting-cluster-replication-dr-badge @icon={{this.getIcon @disasterRecoveryState}} @text={{this.getState @disasterRecoveryState}} @color={{this.getColor @disasterRecoveryState}} @type=\"outlined\" @size=\"small\" />\n </HdsTextBody>\n\n <HdsTextBody @size=\"300\" data-test-vault-reporting-cluster-replication-perf-row>\n Performance\n <HdsBadge class=\"ssu-cluster-replication__list-row__badge\" data-test-vault-reporting-cluster-replication-perf-badge @icon={{this.getIcon @performanceState}} @text={{this.getState @performanceState}} @color={{this.getColor @performanceState}} @type=\"outlined\" @size=\"small\" />\n </HdsTextBody>\n\n </HdsCardContainer>\n ", {
strictMode: true,
scope: () => ({
HdsCardContainer,

File diff suppressed because one or more lines are too long

View File

@ -30,7 +30,7 @@ class SSUReportingCounter extends Component {
return this.args.link;
}
static {
setComponentTemplate(precompileTemplate("\n <div ...attributes data-test-counter={{@title}} class=\"ssu-counter\" aria-label=\"{{@title}} {{this.count}}\">\n <div class=\"ssu-counter__title-row\">\n <HdsTextBody @weight=\"semibold\" @size=\"200\" @color=\"primary\">{{@title}}\n {{#if @tooltipMessage}}\n <HdsTooltipButton data-test-counter-tooltip-button class=\"ssu-counter__title-row__tooltip\" @text={{@tooltipMessage}} aria-label=\"Tooltip for {{@title}}\" @isInline={{true}}>\n <HdsIcon @name=\"help\" @isInline={{true}} />\n </HdsTooltipButton>\n {{/if}}\n </HdsTextBody>\n </div>\n\n <HdsTextBody>\n {{#if this.link}}\n <HdsLinkInline @href={{this.link}} @color=\"secondary\" class=\"ssu-counter__link\" target=\"_self\">{{this.count}}\n </HdsLinkInline>\n {{else}}\n {{this.count}}\n {{/if}}\n </HdsTextBody>\n </div>\n ", {
setComponentTemplate(precompileTemplate("\n <div ...attributes data-test-vault-reporting-counter={{@title}} class=\"ssu-counter\" aria-label=\"{{@title}} {{this.count}}\">\n <div class=\"ssu-counter__title-row\">\n <HdsTextBody @weight=\"semibold\" @size=\"200\" @color=\"primary\">{{@title}}\n {{#if @tooltipMessage}}\n <HdsTooltipButton data-test-vault-reporting-counter-tooltip-button class=\"ssu-counter__title-row__tooltip\" @text={{@tooltipMessage}} aria-label=\"Tooltip for {{@title}}\" @isInline={{true}}>\n <HdsIcon @name=\"help\" @isInline={{true}} />\n </HdsTooltipButton>\n {{/if}}\n </HdsTextBody>\n </div>\n\n <HdsTextBody>\n {{#if this.link}}\n <HdsLinkInline @href={{this.link}} @color=\"secondary\" class=\"ssu-counter__link\" target=\"_self\">{{this.count}}\n </HdsLinkInline>\n {{else}}\n {{this.count}}\n {{/if}}\n </HdsTextBody>\n </div>\n ", {
strictMode: true,
scope: () => ({
HdsTextBody,

View File

@ -1 +1 @@
{"version":3,"file":"counter.js","sources":["../../../src/components/vault-reporting/counter.gts"],"sourcesContent":["/**\n * Copyright (c) HashiCorp, Inc.\n * SPDX-License-Identifier: BUSL-1.1\n */\n\nimport Component from '@glimmer/component';\nimport {\n HdsTextBody,\n HdsIcon,\n HdsTooltipButton,\n HdsLinkInline,\n} from '@hashicorp/design-system-components/components';\nimport type { IconName } from '@hashicorp/flight-icons/svg';\n\nimport './counter.scss';\n\nexport interface SSUReportingCounterSignature {\n Args: {\n count: number;\n title: string;\n tooltipMessage?: string;\n icon?: IconName;\n suffix?: string;\n link?: string;\n emptyText?: string;\n emptyLink?: string;\n };\n\n Blocks: {\n default: [];\n };\n\n Element: HTMLElement;\n}\nexport default class SSUReportingCounter extends Component<SSUReportingCounterSignature> {\n get shouldShowEmptyState() {\n return this.args.count === 0 && this.args.emptyText;\n }\n\n get count() {\n if (this.shouldShowEmptyState) {\n return this.args.emptyText;\n }\n\n if (this.args.suffix) {\n return `${this.args.count} ${this.args.suffix}`;\n }\n\n return this.args.count;\n }\n\n get icon() {\n return this.args.icon || 'info';\n }\n\n get link() {\n if (this.shouldShowEmptyState && this.args.emptyLink) {\n return this.args.emptyLink;\n }\n return this.args.link;\n }\n\n <template>\n <div\n ...attributes\n data-test-counter={{@title}}\n class=\"ssu-counter\"\n aria-label=\"{{@title}} {{this.count}}\"\n >\n <div class=\"ssu-counter__title-row\">\n <HdsTextBody @weight=\"semibold\" @size=\"200\" @color=\"primary\">{{@title}}\n {{#if @tooltipMessage}}\n <HdsTooltipButton\n data-test-counter-tooltip-button\n class=\"ssu-counter__title-row__tooltip\"\n @text={{@tooltipMessage}}\n aria-label=\"Tooltip for {{@title}}\"\n @isInline={{true}}\n >\n <HdsIcon @name=\"help\" @isInline={{true}} />\n </HdsTooltipButton>\n {{/if}}\n </HdsTextBody>\n </div>\n\n <HdsTextBody>\n {{#if this.link}}\n <HdsLinkInline\n @href={{this.link}}\n @color=\"secondary\"\n class=\"ssu-counter__link\"\n target=\"_self\"\n >{{this.count}}\n </HdsLinkInline>\n {{else}}\n {{this.count}}\n {{/if}}\n </HdsTextBody>\n </div>\n </template>\n}\n"],"names":["SSUReportingCounter","Component","shouldShowEmptyState","args","count","emptyText","suffix","icon","link","emptyLink","setComponentTemplate","precompileTemplate","strictMode","scope","HdsTextBody","HdsTooltipButton","HdsIcon","HdsLinkInline"],"mappings":";;;;;AAAA;;;AAGC;AA+Bc,MAAMA,4BAA4BC,SAAU,CAAA;EACzD,IAAIC,oBAAuBA,GAAA;AACzB,IAAA,OAAO,IAAI,CAACC,IAAI,CAACC,KAAK,KAAK,CAAA,IAAK,IAAI,CAACD,IAAI,CAACE,SAAS;AACrD;EAEA,IAAID,KAAQA,GAAA;IACV,IAAI,IAAI,CAACF,oBAAoB,EAAE;AAC7B,MAAA,OAAO,IAAI,CAACC,IAAI,CAACE,SAAS;AAC5B;AAEA,IAAA,IAAI,IAAI,CAACF,IAAI,CAACG,MAAM,EAAE;AACpB,MAAA,OAAO,CAAG,EAAA,IAAI,CAACH,IAAI,CAACC,KAAK,CAAI,CAAA,EAAA,IAAI,CAACD,IAAI,CAACG,MAAM,CAAE,CAAA;AACjD;AAEA,IAAA,OAAO,IAAI,CAACH,IAAI,CAACC,KAAK;AACxB;EAEA,IAAIG,IAAOA,GAAA;AACT,IAAA,OAAO,IAAI,CAACJ,IAAI,CAACI,IAAI,IAAI,MAAA;AAC3B;EAEA,IAAIC,IAAOA,GAAA;IACT,IAAI,IAAI,CAACN,oBAAoB,IAAI,IAAI,CAACC,IAAI,CAACM,SAAS,EAAE;AACpD,MAAA,OAAO,IAAI,CAACN,IAAI,CAACM,SAAS;AAC5B;AACA,IAAA,OAAO,IAAI,CAACN,IAAI,CAACK,IAAI;AACvB;AAEA,EAAA;IAAAE,oBAAA,CAAAC,kBAAA,CAqCA,o6BAAA,EAAA;MAAAC,UAAA,EAAA,IAAA;AAAAC,MAAAA,KAAA,EAAAA,OAAA;QAAAC,WAAA;QAAAC,gBAAA;QAAAC,OAAA;AAAAC,QAAAA;AAAA,OAAA;KAAU,CAAA,EAAV,IAAW,CAAA;AAAD;AACZ;;;;"}
{"version":3,"file":"counter.js","sources":["../../../src/components/vault-reporting/counter.gts"],"sourcesContent":["/**\n * Copyright (c) HashiCorp, Inc.\n * SPDX-License-Identifier: BUSL-1.1\n */\n\nimport Component from '@glimmer/component';\nimport {\n HdsTextBody,\n HdsIcon,\n HdsTooltipButton,\n HdsLinkInline,\n} from '@hashicorp/design-system-components/components';\nimport type { IconName } from '@hashicorp/flight-icons/svg';\n\nimport './counter.scss';\n\nexport interface SSUReportingCounterSignature {\n Args: {\n count: number;\n title: string;\n tooltipMessage?: string;\n icon?: IconName;\n suffix?: string;\n link?: string;\n emptyText?: string;\n emptyLink?: string;\n };\n\n Blocks: {\n default: [];\n };\n\n Element: HTMLElement;\n}\nexport default class SSUReportingCounter extends Component<SSUReportingCounterSignature> {\n get shouldShowEmptyState() {\n return this.args.count === 0 && this.args.emptyText;\n }\n\n get count() {\n if (this.shouldShowEmptyState) {\n return this.args.emptyText;\n }\n\n if (this.args.suffix) {\n return `${this.args.count} ${this.args.suffix}`;\n }\n\n return this.args.count;\n }\n\n get icon() {\n return this.args.icon || 'info';\n }\n\n get link() {\n if (this.shouldShowEmptyState && this.args.emptyLink) {\n return this.args.emptyLink;\n }\n return this.args.link;\n }\n\n <template>\n <div\n ...attributes\n data-test-vault-reporting-counter={{@title}}\n class=\"ssu-counter\"\n aria-label=\"{{@title}} {{this.count}}\"\n >\n <div class=\"ssu-counter__title-row\">\n <HdsTextBody @weight=\"semibold\" @size=\"200\" @color=\"primary\">{{@title}}\n {{#if @tooltipMessage}}\n <HdsTooltipButton\n data-test-vault-reporting-counter-tooltip-button\n class=\"ssu-counter__title-row__tooltip\"\n @text={{@tooltipMessage}}\n aria-label=\"Tooltip for {{@title}}\"\n @isInline={{true}}\n >\n <HdsIcon @name=\"help\" @isInline={{true}} />\n </HdsTooltipButton>\n {{/if}}\n </HdsTextBody>\n </div>\n\n <HdsTextBody>\n {{#if this.link}}\n <HdsLinkInline\n @href={{this.link}}\n @color=\"secondary\"\n class=\"ssu-counter__link\"\n target=\"_self\"\n >{{this.count}}\n </HdsLinkInline>\n {{else}}\n {{this.count}}\n {{/if}}\n </HdsTextBody>\n </div>\n </template>\n}\n"],"names":["SSUReportingCounter","Component","shouldShowEmptyState","args","count","emptyText","suffix","icon","link","emptyLink","setComponentTemplate","precompileTemplate","strictMode","scope","HdsTextBody","HdsTooltipButton","HdsIcon","HdsLinkInline"],"mappings":";;;;;AAAA;;;AAGC;AA+Bc,MAAMA,4BAA4BC,SAAU,CAAA;EACzD,IAAIC,oBAAuBA,GAAA;AACzB,IAAA,OAAO,IAAI,CAACC,IAAI,CAACC,KAAK,KAAK,CAAA,IAAK,IAAI,CAACD,IAAI,CAACE,SAAS;AACrD;EAEA,IAAID,KAAQA,GAAA;IACV,IAAI,IAAI,CAACF,oBAAoB,EAAE;AAC7B,MAAA,OAAO,IAAI,CAACC,IAAI,CAACE,SAAS;AAC5B;AAEA,IAAA,IAAI,IAAI,CAACF,IAAI,CAACG,MAAM,EAAE;AACpB,MAAA,OAAO,CAAG,EAAA,IAAI,CAACH,IAAI,CAACC,KAAK,CAAI,CAAA,EAAA,IAAI,CAACD,IAAI,CAACG,MAAM,CAAE,CAAA;AACjD;AAEA,IAAA,OAAO,IAAI,CAACH,IAAI,CAACC,KAAK;AACxB;EAEA,IAAIG,IAAOA,GAAA;AACT,IAAA,OAAO,IAAI,CAACJ,IAAI,CAACI,IAAI,IAAI,MAAA;AAC3B;EAEA,IAAIC,IAAOA,GAAA;IACT,IAAI,IAAI,CAACN,oBAAoB,IAAI,IAAI,CAACC,IAAI,CAACM,SAAS,EAAE;AACpD,MAAA,OAAO,IAAI,CAACN,IAAI,CAACM,SAAS;AAC5B;AACA,IAAA,OAAO,IAAI,CAACN,IAAI,CAACK,IAAI;AACvB;AAEA,EAAA;IAAAE,oBAAA,CAAAC,kBAAA,CAqCA,o8BAAA,EAAA;MAAAC,UAAA,EAAA,IAAA;AAAAC,MAAAA,KAAA,EAAAA,OAAA;QAAAC,WAAA;QAAAC,gBAAA;QAAAC,OAAA;AAAAC,QAAAA;AAAA,OAAA;KAAU,CAAA,EAAV,IAAW,CAAA;AAAD;AACZ;;;;"}

View File

@ -1,18 +1,34 @@
import Component from '@glimmer/component';
import { HdsDropdown } from '@hashicorp/design-system-components/components';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
import { service } from '@ember/service';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@ember/component';
import { g, i } from 'decorator-transforms/runtime';
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
class DashboardExport extends Component {
static {
g(this.prototype, "reportingAnalytics", [service]);
}
#reportingAnalytics = (i(this, "reportingAnalytics"), void 0);
#getNestedRows(records, prefix = '') {
return Object.entries(records).map(([key, value]) => {
return [`${prefix} ${key}`, value];
});
}
handleTrackExportToggle = () => {
this.reportingAnalytics.trackEvent('export_toggle');
};
handleTrackExportOption = option => {
this.reportingAnalytics.trackEvent(`export_option`, {
option
});
};
get dataAsDownloadableJSONString() {
const {
data
@ -37,10 +53,12 @@ class DashboardExport extends Component {
return URL.createObjectURL(blob);
}
static {
setComponentTemplate(precompileTemplate("\n {{#if @data}}\n <HdsDropdown @matchToggleWidth={{true}} as |D|>\n <D.ToggleButton data-test-export-toggle @text=\"Export\" />\n <D.Interactive data-test-export-json @href={{this.dataAsDownloadableJSONString}} download=\"vault-usage-dashboard.json\">JSON</D.Interactive>\n <D.Interactive data-test-export-csv @href={{this.dataAsDownloadableCSVString}} download=\"vault-usage-dashboard.csv\">CSV</D.Interactive>\n </HdsDropdown>\n {{/if}}\n ", {
setComponentTemplate(precompileTemplate("\n {{#if @data}}\n <HdsDropdown @matchToggleWidth={{true}} as |D|>\n <D.ToggleButton data-test-vault-reporting-export-toggle @text=\"Export\" {{on \"click\" this.handleTrackExportToggle}} />\n <D.Interactive data-test-vault-reporting-export-json @href={{this.dataAsDownloadableJSONString}} download=\"vault-usage-dashboard.json\" {{on \"click\" (fn this.handleTrackExportOption \"json\")}}>JSON</D.Interactive>\n <D.Interactive data-test-vault-reporting-export-csv @href={{this.dataAsDownloadableCSVString}} download=\"vault-usage-dashboard.csv\" {{on \"click\" (fn this.handleTrackExportOption \"csv\")}}>CSV</D.Interactive>\n </HdsDropdown>\n {{/if}}\n ", {
strictMode: true,
scope: () => ({
HdsDropdown
HdsDropdown,
on,
fn
})
}), this);
}

File diff suppressed because one or more lines are too long

View File

@ -51,7 +51,7 @@ class SSUReportingDonutChart extends Component {
return Math.max(computedRadius, 110);
}
static {
setComponentTemplate(precompileTemplate("\n <HdsCardContainer ...attributes class=\"ssu-donut-chart__container\" @hasBorder={{true}}>\n <div class=\"ssu-donut-chart__row\">\n {{!-- TODO: Figure out glint errors on lineal components --}}\n {{!-- @glint-expect-error --}}\n <LinealFluid class=\"ssu-donut-chart__fluid\" as |width height|>\n <svg width=\"100%\" height=\"100%\" class=\"ssu-donut-chart__chart\" tabindex=\"0\" role=\"img\" aria-label={{this.a11yLabel}}>\n <g transform={{this.getOffset width height}}>\n {{!-- @glint-expect-error --}}\n <LinealArcs @data={{this.data}} {{!-- @glint-expect-error --}} @theta=\"value\" @colorScale=\"nominal\" as |pie|>\n {{#each pie as |slice|}}\n {{!-- @glint-expect-error --}}\n <LinealArc data-test-slice={{slice.data.label}} @startAngle={{slice.startAngle}} @endAngle={{slice.endAngle}} @outerRadius={{this.getOuterRadius width height}} @innerRadius={{this.getInnerRadius width height}} stroke-width=\"2\" class={{slice.cssClass}} />\n {{/each}}\n </LinealArcs>\n <foreignObject transform=\"translate(-60 -30)\" width=\"120\" height=\"120\">\n <div class=\"ssu-donut-chart__total-summary\">\n <HdsTextDisplay @size=\"500\">\n {{this.total}}\n </HdsTextDisplay>\n <HdsTextDisplay @size=\"200\">\n {{@title}}\n </HdsTextDisplay>\n </div>\n </foreignObject>\n </g>\n </svg>\n </LinealFluid>\n <div class=\"ssu-donut-chart__legend\">\n {{#each this.data as |datum|}}\n <HdsTextDisplay data-test-legend-item={{datum.label}} class=\"ssu-donut-chart__legend-item\n {{concat \"ssu-donut-chart__legend-item-\" datum.scaleIndex}}\">{{datum.value}} {{datum.label}} </HdsTextDisplay>\n {{/each}}\n </div>\n </div>\n </HdsCardContainer>\n ", {
setComponentTemplate(precompileTemplate("\n <HdsCardContainer ...attributes class=\"ssu-donut-chart__container\" @hasBorder={{true}}>\n <div class=\"ssu-donut-chart__row\">\n {{!-- TODO: Figure out glint errors on lineal components --}}\n {{!-- @glint-expect-error --}}\n <LinealFluid class=\"ssu-donut-chart__fluid\" as |width height|>\n <svg width=\"100%\" height=\"100%\" class=\"ssu-donut-chart__chart\" tabindex=\"0\" role=\"img\" aria-label={{this.a11yLabel}}>\n <g transform={{this.getOffset width height}}>\n {{!-- @glint-expect-error --}}\n <LinealArcs @data={{this.data}} {{!-- @glint-expect-error --}} @theta=\"value\" @colorScale=\"nominal\" as |pie|>\n {{#each pie as |slice|}}\n {{!-- @glint-expect-error --}}\n <LinealArc data-test-vault-reporting-slice={{slice.data.label}} @startAngle={{slice.startAngle}} @endAngle={{slice.endAngle}} @outerRadius={{this.getOuterRadius width height}} @innerRadius={{this.getInnerRadius width height}} stroke-width=\"2\" class={{slice.cssClass}} />\n {{/each}}\n </LinealArcs>\n <foreignObject transform=\"translate(-60 -30)\" width=\"120\" height=\"120\">\n <div class=\"ssu-donut-chart__total-summary\">\n <HdsTextDisplay @size=\"500\">\n {{this.total}}\n </HdsTextDisplay>\n <HdsTextDisplay @size=\"200\">\n {{@title}}\n </HdsTextDisplay>\n </div>\n </foreignObject>\n </g>\n </svg>\n </LinealFluid>\n <div class=\"ssu-donut-chart__legend\">\n {{#each this.data as |datum|}}\n <HdsTextDisplay data-test-vault-reporting-legend-item={{datum.label}} class=\"ssu-donut-chart__legend-item\n {{concat \"ssu-donut-chart__legend-item-\" datum.scaleIndex}}\">{{datum.value}} {{datum.label}} </HdsTextDisplay>\n {{/each}}\n </div>\n </div>\n </HdsCardContainer>\n ", {
strictMode: true,
scope: () => ({
HdsCardContainer,

File diff suppressed because one or more lines are too long

View File

@ -45,7 +45,7 @@ class GlobalLease extends Component {
}
get description() {
if (this.hasData) {
return htmlSafe('Total number of active <a class="hds-link-inline--color-secondary" href="https://developer.hashicorp.com/vault/docs/concepts/lease" target="_blank" data-test-global-lease-description-link>leases</a> for this quota.');
return htmlSafe('Total number of active <a class="hds-link-inline--color-secondary" href="https://developer.hashicorp.com/vault/docs/concepts/lease" target="_blank" data-test-vault-reporting-global-lease-description-link>leases</a> for this quota.');
}
}
get linkUrl() {
@ -68,7 +68,7 @@ class GlobalLease extends Component {
}
}
static {
setComponentTemplate(precompileTemplate("\n <HdsCardContainer data-test-global-lease @hasBorder={{true}} class=\"ssu-global-lease\" {{cssCustomProperty \"--vault-reporting-global-lease-percentage\" this.percentageString}} ...attributes>\n <TitleRow @title=\"Global lease count quota\" @description={{this.description}} @linkText=\"Documentation\" @linkIcon=\"docs-link\" @linkUrl={{this.linkUrl}} @linkTarget=\"_blank\" />\n {{#if this.hasData}}\n <HdsTextDisplay @size=\"300\" @weight=\"medium\" data-test-global-lease-percentage-text>{{this.percentage}}%</HdsTextDisplay>\n\n {{#if this.alert}}\n <HdsAlert data-test-global-lease-alert class=\"ssu-global-lease__alert\" @type=\"compact\" @color={{this.alert.color}} as |A|>\n <A.Description>{{this.alert.description}}</A.Description>\n </HdsAlert>\n {{/if}}\n\n <div class=\"ssu-global-lease__progress-wrapper\">\n <div class=\"ssu-global-lease__progress-bar\">\n <div class=\"ssu-global-lease__progress-fill {{this.progressFillClass}}\" data-test-global-lease-fill></div>\n </div>\n <span>\n <HdsTextDisplay @size=\"200\" @weight=\"semibold\" data-test-global-lease-count-text>\n {{this.formattedCount}}\n </HdsTextDisplay>\n </span>\n </div>\n {{else}}\n\n <HdsApplicationState data-test-global-lease-empty-state class=\"ssu-global-lease__empty-state\" as |A|>\n {{#if (has-block \"empty\")}}\n {{yield A to=\"empty\"}}\n {{else}}\n <A.Body data-test-global-lease-empty-state-description @text=\"Lease quotas enforce limits on active secrets and tokens. It's recommended to enable this to protect stability for this Vault cluster.\" />\n\n <A.Footer as |F|>\n <F.LinkStandalone data-test-global-lease-empty-state-link @icon=\"docs-link\" @iconPosition=\"trailing\" @text=\"Global lease count quota\" @href=\"https://developer.hashicorp.com/vault/tutorials/operations/resource-quotas#global-default-lease-count-quota\" target=\"_blank\" />\n </A.Footer>\n {{/if}}\n </HdsApplicationState>\n {{/if}}\n\n </HdsCardContainer>\n ", {
setComponentTemplate(precompileTemplate("\n <HdsCardContainer data-test-vault-reporting-global-lease @hasBorder={{true}} class=\"ssu-global-lease\" {{cssCustomProperty \"--vault-reporting-global-lease-percentage\" this.percentageString}} ...attributes>\n <TitleRow @title=\"Global lease count quota\" @description={{this.description}} @linkText=\"Documentation\" @linkIcon=\"docs-link\" @linkUrl={{this.linkUrl}} @linkTarget=\"_blank\" />\n {{#if this.hasData}}\n <HdsTextDisplay @size=\"300\" @weight=\"medium\" data-test-vault-reporting-global-lease-percentage-text>{{this.percentage}}%</HdsTextDisplay>\n\n {{#if this.alert}}\n <HdsAlert data-test-vault-reporting-global-lease-alert class=\"ssu-global-lease__alert\" @type=\"compact\" @color={{this.alert.color}} as |A|>\n <A.Description>{{this.alert.description}}</A.Description>\n </HdsAlert>\n {{/if}}\n\n <div class=\"ssu-global-lease__progress-wrapper\">\n <div class=\"ssu-global-lease__progress-bar\">\n <div class=\"ssu-global-lease__progress-fill {{this.progressFillClass}}\" data-test-vault-reporting-global-lease-fill></div>\n </div>\n <span>\n <HdsTextDisplay @size=\"200\" @weight=\"semibold\" data-test-vault-reporting-global-lease-count-text>\n {{this.formattedCount}}\n </HdsTextDisplay>\n </span>\n </div>\n {{else}}\n\n <HdsApplicationState data-test-vault-reporting-global-lease-empty-state class=\"ssu-global-lease__empty-state\" as |A|>\n {{#if (has-block \"empty\")}}\n {{yield A to=\"empty\"}}\n {{else}}\n <A.Body data-test-vault-reporting-global-lease-empty-state-description @text=\"Lease quotas enforce limits on active secrets and tokens. It's recommended to enable this to protect stability for this Vault cluster.\" />\n\n <A.Footer as |F|>\n <F.LinkStandalone data-test-vault-reporting-global-lease-empty-state-link @icon=\"docs-link\" @iconPosition=\"trailing\" @text=\"Global lease count quota\" @href=\"https://developer.hashicorp.com/vault/tutorials/operations/resource-quotas#global-default-lease-count-quota\" target=\"_blank\" />\n </A.Footer>\n {{/if}}\n </HdsApplicationState>\n {{/if}}\n\n </HdsCardContainer>\n ", {
strictMode: true,
scope: () => ({
HdsCardContainer,

File diff suppressed because one or more lines are too long

View File

@ -99,7 +99,7 @@ class SSUReportingHorizontalBarChart extends Component {
this.xRangeOffsetWidth = offsetWidth;
};
static {
setComponentTemplate(precompileTemplate("\n <HdsCardContainer ...attributes class=\"ssu-horizontal-bar-chart__container\" @hasBorder={{true}}>\n <TitleRow @title={{@title}} @description={{this.description}} @linkUrl={{this.linkUrl}} @linkText={{@linkText}} @linkTarget={{@linkTarget}} @linkIcon={{@linkIcon}} />\n {{#if this.hasData}}\n {{!-- TODO: Figure out glint errors on lineal components --}}\n {{!-- @glint-expect-error --}}\n <LinealFluid class=\"ssu-horizontal-bar-chart__chart\" as |width|>\n <svg height={{this.rangeHeight}} width=\"100%\" {{axisOffset this.handleAxisOffset 8}} data-test-horizontal-bar-chart-svg>\n {{!-- We are using the stacked version of the HBars as there seems to be an issue in the non-stacked version for how the x position is calculated. --}}\n {{#let (scaleLinear range=(this.getXRange width) domain=this.xDomain) (scaleBand range=this.yRange domain=this.yDomain) (stackH data=this.data x=\"value\" y=\"label\" z=\"\") as |xScale yScale stacked|}}\n {{#if xScale.isValid}}\n <LinealAxis @scale={{yScale}} {{!-- @glint-expect-error --}} @orientation=\"left\" @includeDomain={{false}} />\n {{!-- TODO: Extra wrapper exists only for test attribute, figure out a better way --}}\n <g data-test-horizontal-bar-chart-bars>\n <LinealHBars @data={{stacked.data}} {{!-- @glint-expect-error --}} @x=\"x\" {{!-- @glint-expect-error --}} @y=\"y\" {{!-- @glint-expect-error --}} @height={{6}} @xScale={{xScale}} @yScale={{yScale}} />\n </g>\n <g>\n {{!-- @glint-expect-error --}}\n {{#each stacked.data as |dataset|}}\n {{#each dataset as |datum|}}\n <text class=\"ssu-horizontal-bar-chart__label\" {{!-- @glint-expect-error --}} y={{yScale.compute datum.y}} x={{xScale.compute datum.x}} dy=\"17.5px\" dx=\"8px\" data-test-horizontal-bar-chart-inline-count aria-label=\"{{datum.y}} {{datum.x}}\">\n {{datum.x}}\n </text>\n {{/each}}\n {{/each}}\n </g>\n {{/if}}\n {{/let}}\n </svg>\n </LinealFluid>\n <HdsSeparator class=\"ssu-horizontal-bar-chart__separator\" @spacing=\"0\" />\n <HdsTextBody class=\"ssu-horizontal-bar-chart__total\" @size=\"200\" @tag=\"p\" data-test-horizontal-bar-chart-total>\n Total:\n {{this.total}}\n </HdsTextBody>\n {{else}}\n\n <HdsApplicationState data-test-horizontal-bar-chart-empty-state class=\"ssu-horizontal-bar-chart__empty-state\" as |A|>\n {{#if (has-block \"empty\")}}\n {{yield A to=\"empty\"}}\n {{else}}\n <A.Header data-test-horizontal-bar-chart-empty-state-title @title={{this.emptyStateTitle}} />\n <A.Body data-test-horizontal-bar-chart-empty-state-description @text={{this.emptyStateDescription}} />\n {{#if @linkUrl}}\n <A.Footer as |F|>\n <F.LinkStandalone data-test-horizontal-bar-chart-empty-state-link @icon=\"plus\" @text={{this.emptyStateLinkText}} @href={{@linkUrl}} />\n </A.Footer>\n {{/if}}\n {{/if}}\n </HdsApplicationState>\n {{/if}}\n </HdsCardContainer>\n ", {
setComponentTemplate(precompileTemplate("\n <HdsCardContainer ...attributes class=\"ssu-horizontal-bar-chart__container\" @hasBorder={{true}}>\n <TitleRow @title={{@title}} @description={{this.description}} @linkUrl={{this.linkUrl}} @linkText={{@linkText}} @linkTarget={{@linkTarget}} @linkIcon={{@linkIcon}} />\n {{#if this.hasData}}\n {{!-- TODO: Figure out glint errors on lineal components --}}\n {{!-- @glint-expect-error --}}\n <LinealFluid class=\"ssu-horizontal-bar-chart__chart\" as |width|>\n <svg height={{this.rangeHeight}} width=\"100%\" {{axisOffset this.handleAxisOffset 8}} data-test-vault-reporting-horizontal-bar-chart-svg>\n {{!-- We are using the stacked version of the HBars as there seems to be an issue in the non-stacked version for how the x position is calculated. --}}\n {{#let (scaleLinear range=(this.getXRange width) domain=this.xDomain) (scaleBand range=this.yRange domain=this.yDomain) (stackH data=this.data x=\"value\" y=\"label\" z=\"\") as |xScale yScale stacked|}}\n {{#if xScale.isValid}}\n <LinealAxis @scale={{yScale}} {{!-- @glint-expect-error --}} @orientation=\"left\" @includeDomain={{false}} />\n {{!-- TODO: Extra wrapper exists only for test attribute, figure out a better way --}}\n <g data-test-vault-reporting-horizontal-bar-chart-bars>\n <LinealHBars @data={{stacked.data}} {{!-- @glint-expect-error --}} @x=\"x\" {{!-- @glint-expect-error --}} @y=\"y\" {{!-- @glint-expect-error --}} @height={{6}} @xScale={{xScale}} @yScale={{yScale}} />\n </g>\n <g>\n {{!-- @glint-expect-error --}}\n {{#each stacked.data as |dataset|}}\n {{#each dataset as |datum|}}\n <text class=\"ssu-horizontal-bar-chart__label\" {{!-- @glint-expect-error --}} y={{yScale.compute datum.y}} x={{xScale.compute datum.x}} dy=\"17.5px\" dx=\"8px\" data-test-vault-reporting-horizontal-bar-chart-inline-count aria-label=\"{{datum.y}} {{datum.x}}\">\n {{datum.x}}\n </text>\n {{/each}}\n {{/each}}\n </g>\n {{/if}}\n {{/let}}\n </svg>\n </LinealFluid>\n <HdsSeparator class=\"ssu-horizontal-bar-chart__separator\" @spacing=\"0\" />\n <HdsTextBody class=\"ssu-horizontal-bar-chart__total\" @size=\"200\" @tag=\"p\" data-test-vault-reporting-horizontal-bar-chart-total>\n Total:\n {{this.total}}\n </HdsTextBody>\n {{else}}\n\n <HdsApplicationState data-test-vault-reporting-horizontal-bar-chart-empty-state class=\"ssu-horizontal-bar-chart__empty-state\" as |A|>\n {{#if (has-block \"empty\")}}\n {{yield A to=\"empty\"}}\n {{else}}\n <A.Header data-test-vault-reporting-horizontal-bar-chart-empty-state-title @title={{this.emptyStateTitle}} />\n <A.Body data-test-vault-reporting-horizontal-bar-chart-empty-state-description @text={{this.emptyStateDescription}} />\n {{#if @linkUrl}}\n <A.Footer as |F|>\n <F.LinkStandalone data-test-vault-reporting-horizontal-bar-chart-empty-state-link @icon=\"plus\" @text={{this.emptyStateLinkText}} @href={{@linkUrl}} />\n </A.Footer>\n {{/if}}\n {{/if}}\n </HdsApplicationState>\n {{/if}}\n </HdsCardContainer>\n ", {
strictMode: true,
scope: () => ({
HdsCardContainer,

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,8 @@ import ClusterReplication from '../cluster-replication.js';
import DashboardExport from '../dashboard/export.js';
import { tracked } from '@glimmer/tracking';
import { HdsCardContainer, HdsAlert, HdsLinkInline, HdsTextBody, HdsBadge, HdsPageHeader } from '@hashicorp/design-system-components/components';
import { service } from '@ember/service';
import { on } from '@ember/modifier';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@ember/component';
import { g, i } from 'decorator-transforms/runtime';
@ -16,6 +18,10 @@ import { g, i } from 'decorator-transforms/runtime';
* SPDX-License-Identifier: BUSL-1.1
*/
class SSUViewDashboard extends Component {
static {
g(this.prototype, "reportingAnalytics", [service]);
}
#reportingAnalytics = (i(this, "reportingAnalytics"), void 0);
static {
g(this.prototype, "data", [tracked]);
}
@ -45,7 +51,17 @@ class SSUViewDashboard extends Component {
this.error = e;
}
};
getBarChartData = (map = {}, exclude = []) => {
handleTrackAnalyticsEvent = (eventName, properties, options) => {
this.reportingAnalytics.trackEvent(eventName, properties, options);
};
handleTrackSurveyLink = () => {
this.handleTrackAnalyticsEvent('survey_link');
};
handleRefresh = () => {
this.fetchAllData();
this.handleTrackAnalyticsEvent('refresh_button');
};
getBarChartData = (map = {}, exclude) => {
return Object.entries(map).map(([label, value]) => {
return {
label,
@ -110,13 +126,14 @@ class SSUViewDashboard extends Component {
return this.isVaultDedicated ? 'admin' : 'root';
}
static {
setComponentTemplate(precompileTemplate("\n <div class=\"dashboard\">\n <HdsPageHeader as |PH|>\n <PH.Title>\n Vault Usage\n <HdsBadge class=\"dashboard__badge\" @size=\"medium\" @icon=\"org\" @color=\"neutral\" @text={{this.namespace}} />\n </PH.Title>\n <PH.Description class=\"dashboard__description\">\n {{#if this.lastUpdatedTime}}\n <HdsTextBody @tag=\"p\" @size=\"200\" @color=\"faint\">\n Updated today at\n {{this.lastUpdatedTime}}.\n\n </HdsTextBody>\n {{/if}}\n <HdsTextBody @tag=\"p\" @size=\"200\" @color=\"primary\">\n View and export your Vault usage. Don't see what you're looking for?\n <HdsLinkInline data-test-dashboard-survey-link @icon=\"external-link\" @href=\"https://hashicorp.sjc1.qualtrics.com/jfe/form/SV_bqhLeB3deLd2caa\" target=\"_blank\">Share feedback</HdsLinkInline>\n </HdsTextBody>\n </PH.Description>\n <PH.Actions>\n <DashboardExport @data={{this.data}} />\n </PH.Actions>\n </HdsPageHeader>\n {{#if this.error}}\n <HdsAlert data-test-dashboard-error @type=\"inline\" @color=\"critical\" class=\"dashboard__error\" as |A|>\n <A.Title>Error</A.Title>\n <A.Description data-test-dashboard-error-description>An error\n occurred, please try again.</A.Description>\n </HdsAlert>\n {{/if}}\n {{#if this.data}}\n <HdsCardContainer @hasBorder={{true}} {{!-- @glint-expect-error --}} @background=\"neutral-secondary\" class=\"dashboard__counters\" data-test-dashboard-counters>\n {{#each this.counters as |counter|}}\n <ReportingCounter @title={{counter.title}} @tooltipMessage={{counter.tooltipMessage}} @count={{counter.data}} @icon={{counter.icon}} @suffix={{counter.suffix}} @link={{counter.link}} @emptyText={{counter.emptyText}} @emptyLink={{counter.emptyLink}} />\n {{/each}}\n </HdsCardContainer>\n <div data-test-dashboard-viz-blocks class=\"dashboard__viz-blocks\">\n <div>\n <ReportingHorizontalBarChart @data={{this.getBarChartData this.data.authMethods}} @title=\"Authentication methods\" @description=\"Enabled authentication methods for this cluster.\" @linkUrl=\"access\" class=\"dashboard__viz-block\" data-test-dashboard-auth-methods />\n\n <ReportingHorizontalBarChart @data={{this.getBarChartData this.data.secretEngines (array \"system\" \"identity\")}} @title=\"Secret engines\" @description=\"Enabled secret engines for this cluster.\" @linkUrl=\"secrets\" class=\"dashboard__viz-block\" data-test-dashboard-secret-engines />\n\n <ClusterReplication @disasterRecoveryState={{this.data.replicationStatus.drState}} @performanceState={{this.data.replicationStatus.prState}} @isVaultDedicated={{this.isVaultDedicated}} data-test-dashboard-cluster-replication />\n </div>\n\n <div>\n <ReportingHorizontalBarChart @data={{this.getBarChartData this.data.leasesByAuthMethod}} @title=\"Leases by authentication methods\" @description=\"Active leases issued per authentication method.\" @linkUrl=\"https://developer.hashicorp.com/vault/docs/concepts/auth#auth-leases\" @linkText=\"Documentation\" @linkIcon=\"docs-link\" @linkTarget=\"_blank\" class=\"dashboard__viz-block\" data-test-dashboard-leases-by-auth-method>\n <:empty as |A|>\n <A.Body @text=\"Lease are created when clients authenticate. Add an authentication method to monitor leases across this namespace.\" />\n <A.Footer as |F|>\n <F.LinkStandalone @icon=\"docs-link\" @text=\"Authentication leases\" @href=\"https://developer.hashicorp.com/vault/docs/concepts/auth#auth-leases\" />\n </A.Footer>\n </:empty>\n </ReportingHorizontalBarChart>\n <GlobalLease @count={{this.data.leaseCountQuotas.globalLeaseCountQuota.count}} @quota={{this.data.leaseCountQuotas.globalLeaseCountQuota.capacity}} class=\"dashboard__viz-block\" data-test-dashboard-lease-count />\n </div>\n\n </div>\n {{/if}}\n </div>\n ", {
setComponentTemplate(precompileTemplate("\n <div class=\"dashboard\" data-test-dashboard-container>\n <HdsPageHeader as |PH|>\n <PH.Title>\n Vault Usage\n <HdsBadge class=\"dashboard__badge\" @size=\"medium\" @icon=\"org\" @color=\"neutral\" @text={{this.namespace}} />\n </PH.Title>\n <PH.Description class=\"dashboard__description\">\n {{#if this.lastUpdatedTime}}\n <HdsTextBody @tag=\"p\" @size=\"200\" @color=\"faint\">\n Updated today at\n {{this.lastUpdatedTime}}.\n\n </HdsTextBody>\n {{/if}}\n <HdsTextBody @tag=\"p\" @size=\"200\" @color=\"primary\">\n View and export your Vault usage. Don't see what you're looking for?\n <HdsLinkInline data-test-vault-reporting-dashboard-survey-link @icon=\"external-link\" @href=\"https://hashicorp.sjc1.qualtrics.com/jfe/form/SV_bqhLeB3deLd2caa\" target=\"_blank\" {{on \"click\" this.handleTrackSurveyLink}}>Share feedback</HdsLinkInline>\n </HdsTextBody>\n </PH.Description>\n <PH.Actions>\n <DashboardExport @data={{this.data}} />\n </PH.Actions>\n </HdsPageHeader>\n {{#if this.error}}\n <HdsAlert data-test-vault-reporting-dashboard-error @type=\"inline\" @color=\"critical\" class=\"dashboard__error\" as |A|>\n <A.Title>Error</A.Title>\n <A.Description data-test-vault-reporting-dashboard-error-description>An error occurred, please try again.</A.Description>\n </HdsAlert>\n {{/if}}\n {{#if this.data}}\n <HdsCardContainer @hasBorder={{true}} {{!-- @glint-expect-error --}} @background=\"neutral-secondary\" class=\"dashboard__counters\" data-test-vault-reporting-dashboard-counters>\n {{#each this.counters as |counter|}}\n <ReportingCounter @title={{counter.title}} @tooltipMessage={{counter.tooltipMessage}} @count={{counter.data}} @icon={{counter.icon}} @suffix={{counter.suffix}} @link={{counter.link}} @emptyText={{counter.emptyText}} @emptyLink={{counter.emptyLink}} />\n {{/each}}\n </HdsCardContainer>\n <div data-test-vault-reporting-dashboard-viz-blocks class=\"dashboard__viz-blocks\">\n <div>\n <ReportingHorizontalBarChart @data={{this.getBarChartData this.data.authMethods}} @title=\"Authentication methods\" @description=\"Enabled authentication methods for this cluster.\" @linkUrl=\"access\" class=\"dashboard__viz-block\" data-test-vault-reporting-dashboard-auth-methods />\n\n <ReportingHorizontalBarChart @data={{this.getBarChartData this.data.secretEngines (array \"system\" \"identity\")}} @title=\"Secret engines\" @description=\"Enabled secret engines for this cluster.\" @linkUrl=\"secrets\" class=\"dashboard__viz-block\" data-test-vault-reporting-dashboard-secret-engines />\n\n <ClusterReplication @disasterRecoveryState={{this.data.replicationStatus.drState}} @performanceState={{this.data.replicationStatus.prState}} @isVaultDedicated={{this.isVaultDedicated}} data-test-vault-reporting-dashboard-cluster-replication />\n </div>\n\n <div>\n <ReportingHorizontalBarChart @data={{this.getBarChartData this.data.leasesByAuthMethod}} @title=\"Leases by authentication methods\" @description=\"Active leases issued per authentication method.\" @linkUrl=\"https://developer.hashicorp.com/vault/docs/concepts/auth#auth-leases\" @linkText=\"Documentation\" @linkIcon=\"docs-link\" @linkTarget=\"_blank\" class=\"dashboard__viz-block\" data-test-vault-reporting-dashboard-leases-by-auth-method>\n <:empty as |A|>\n <A.Body @text=\"Lease are created when clients authenticate. Add an authentication method to monitor leases across this namespace.\" />\n <A.Footer as |F|>\n <F.LinkStandalone @icon=\"docs-link\" @text=\"Authentication leases\" @href=\"https://developer.hashicorp.com/vault/docs/concepts/auth#auth-leases\" />\n </A.Footer>\n </:empty>\n </ReportingHorizontalBarChart>\n <GlobalLease @count={{this.data.leaseCountQuotas.globalLeaseCountQuota.count}} @quota={{this.data.leaseCountQuotas.globalLeaseCountQuota.capacity}} class=\"dashboard__viz-block\" data-test-vault-reporting-dashboard-lease-count />\n </div>\n\n </div>\n {{/if}}\n </div>\n ", {
strictMode: true,
scope: () => ({
HdsPageHeader,
HdsBadge,
HdsTextBody,
HdsLinkInline,
on,
DashboardExport,
HdsAlert,
HdsCardContainer,

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,37 @@
import { getOwner } from '@ember/owner';
import Service from '@ember/service';
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* This service is used to look up the `analytics` service in the host application and track events if it exists. If it doesn't exist
* or the implementation breaks it falls back gracefully to do nothing.
*/
class ReportingAnalytics extends Service {
// Using the `@service` decorator will throw an error if the service does not exist on the host application.
// This allows us to be defensive and have `trackEvent` be a no-op if the service is not present.
get analytics() {
return getOwner(this)?.lookup('service:analytics');
}
trackEvent(event, properties, options) {
if (!this.analytics?.trackEvent) {
return;
}
try {
const prefix = 'vault_reporting';
const prefixedEvent = `${prefix}_${event}`;
this.analytics.trackEvent(prefixedEvent, properties, options);
} catch (e) {
// no-op
console.warn('Error tracking event:', e);
}
}
}
// DO NOT DELETE: this is how TypeScript knows how to look up your services.
export { ReportingAnalytics as default };
//# sourceMappingURL=reporting-analytics.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"reporting-analytics.js","sources":["../../src/services/reporting-analytics.ts"],"sourcesContent":["/**\n * Copyright (c) HashiCorp, Inc.\n * SPDX-License-Identifier: BUSL-1.1\n */\n\nimport { getOwner } from '@ember/owner';\nimport Service from '@ember/service';\n/**\n * This service is used to look up the `analytics` service in the host application and track events if it exists. If it doesn't exist\n * or the implementation breaks it falls back gracefully to do nothing.\n */\nexport default class ReportingAnalytics extends Service {\n // Using the `@service` decorator will throw an error if the service does not exist on the host application.\n // This allows us to be defensive and have `trackEvent` be a no-op if the service is not present.\n get analytics() {\n return getOwner(this)?.lookup('service:analytics') as\n | {\n trackEvent: (\n event: string,\n properties?: object,\n options?: object,\n ) => void;\n }\n | undefined;\n }\n\n trackEvent(event: string, properties?: object, options?: object) {\n if (!this.analytics?.trackEvent) {\n return;\n }\n try {\n const prefix = 'vault_reporting';\n const prefixedEvent = `${prefix}_${event}`;\n this.analytics.trackEvent(prefixedEvent, properties, options);\n } catch (e) {\n // no-op\n console.warn('Error tracking event:', e);\n }\n }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n interface Registry {\n reportingAnalytics: ReportingAnalytics;\n }\n}\n"],"names":["ReportingAnalytics","Service","analytics","getOwner","lookup","trackEvent","event","properties","options","prefix","prefixedEvent","e","console","warn"],"mappings":";;;AAAA;AACA;AACA;AACA;;AAIA;AACA;AACA;AACA;AACe,MAAMA,kBAAkB,SAASC,OAAO,CAAC;AACtD;AACA;EACA,IAAIC,SAASA,GAAG;IACd,OAAOC,QAAQ,CAAC,IAAI,CAAC,EAAEC,MAAM,CAAC,mBAAmB,CAAC;AASpD;AAEAC,EAAAA,UAAUA,CAACC,KAAa,EAAEC,UAAmB,EAAEC,OAAgB,EAAE;AAC/D,IAAA,IAAI,CAAC,IAAI,CAACN,SAAS,EAAEG,UAAU,EAAE;AAC/B,MAAA;AACF;IACA,IAAI;MACF,MAAMI,MAAM,GAAG,iBAAiB;AAChC,MAAA,MAAMC,aAAa,GAAG,CAAA,EAAGD,MAAM,CAAA,CAAA,EAAIH,KAAK,CAAE,CAAA;MAC1C,IAAI,CAACJ,SAAS,CAACG,UAAU,CAACK,aAAa,EAAEH,UAAU,EAAEC,OAAO,CAAC;KAC9D,CAAC,OAAOG,CAAC,EAAE;AACV;AACAC,MAAAA,OAAO,CAACC,IAAI,CAAC,uBAAuB,EAAEF,CAAC,CAAC;AAC1C;AACF;AACF;;AAEA;;;;"}

View File

@ -2,4 +2,4 @@
* Copyright (c) HashiCorp, Inc.
*/
.ssu-title-row{margin-bottom:8px}.ssu-title-row__description{display:block;margin-top:.25rem;color:var(--token-color-foreground-faint)}.ssu-title-row__container{display:flex;justify-content:space-between}.ssu-title-row__container__link{padding:0}.ssu-counter{padding:16px}.ssu-counter__title-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.ssu-counter__title-row__tooltip{margin-left:3px;top:3px}.ssu-global-lease{padding:16px}.ssu-global-lease__progress-wrapper{display:flex;align-items:center;gap:1rem;height:15px}.ssu-global-lease__progress-bar{flex:1;height:100%;background-color:var(--token-color-palette-neutral-100);border:1.5px solid var(--token-color-palette-neutral-200);border-radius:4px;overflow:hidden;font-size:40px;display:inline-block}@keyframes initialWidth{0%{transform:scaleX(0)}100%{transform:scaleX(1)}}.ssu-global-lease__progress-fill{height:100%;width:var(--vault-reporting-global-lease-percentage);animation:1s ease-out initialWidth;transform-origin:left;transition:width 1s ease-out,background-color 1s ease-out;background-color:var(--token-color-palette-neutral-300)}.ssu-global-lease__progress-fill--exceeded{background-color:var(--token-color-palette-red-100)}.ssu-global-lease__empty-state{min-width:66%;margin:16px auto;color:var(--token-color-foreground-faint)}.ssu-global-lease__alert{margin-bottom:8px}.ssu-cluster-replication{padding:16px;display:flex;flex-direction:column;justify-content:center;gap:1rem}.ssu-cluster-replication__list-row__badge{margin:0 .25rem;text-transform:capitalize}.ssu-horizontal-bar-chart__container{padding:16px;overflow:hidden;font-size:13px}.ssu-horizontal-bar-chart__chart{box-sizing:border-box;margin-top:15px;color:var(--token-color-foreground-primary);fill:var(--token-color-foreground-primary)}.ssu-horizontal-bar-chart__chart svg{overflow:visible}.ssu-horizontal-bar-chart__chart rect{fill:var(--token-color-palette-blue-200);transform:translateY(10.5px);rx:3px;ry:3px}.ssu-horizontal-bar-chart__chart .axis line{display:none}.ssu-horizontal-bar-chart__separator{margin-bottom:10px}.ssu-horizontal-bar-chart__empty-state{min-width:66%;margin:16px auto}.ssu-horizontal-bar-chart__total{color:var(--token-color-foreground-primary)}.dashboard{padding:16px}.dashboard__error{margin-top:16px}.dashboard__counters{display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));gap:16px;margin:32px 0}.dashboard__viz-blocks{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:16px}.dashboard__viz-block{margin-bottom:16px}.dashboard__badge{margin-left:5px}.dashboard__description p{margin-bottom:8px}
.ssu-cluster-replication{padding:16px;display:flex;flex-direction:column;justify-content:center;gap:1rem}.ssu-cluster-replication__list-row__badge{margin:0 .25rem;text-transform:capitalize}.dashboard{padding:16px}.dashboard__error{margin-top:16px}.dashboard__counters{display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));gap:16px;margin:32px 0}.dashboard__viz-blocks{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:16px}.dashboard__viz-block{margin-bottom:16px}.dashboard__badge{margin-left:5px}.dashboard__description p{margin-bottom:8px}.ssu-counter{padding:16px}.ssu-counter__title-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.ssu-counter__title-row__tooltip{margin-left:3px;top:3px}.ssu-global-lease{padding:16px}.ssu-global-lease__progress-wrapper{display:flex;align-items:center;gap:1rem;height:15px}.ssu-global-lease__progress-bar{flex:1;height:100%;background-color:var(--token-color-palette-neutral-100);border:1.5px solid var(--token-color-palette-neutral-200);border-radius:4px;overflow:hidden;font-size:40px;display:inline-block}@keyframes initialWidth{0%{transform:scaleX(0)}100%{transform:scaleX(1)}}.ssu-global-lease__progress-fill{height:100%;width:var(--vault-reporting-global-lease-percentage);animation:1s ease-out initialWidth;transform-origin:left;transition:width 1s ease-out,background-color 1s ease-out;background-color:var(--token-color-palette-neutral-300)}.ssu-global-lease__progress-fill--exceeded{background-color:var(--token-color-palette-red-100)}.ssu-global-lease__empty-state{min-width:66%;margin:16px auto;color:var(--token-color-foreground-faint)}.ssu-global-lease__alert{margin-bottom:8px}.ssu-title-row{margin-bottom:8px}.ssu-title-row__description{display:block;margin-top:.25rem;color:var(--token-color-foreground-faint)}.ssu-title-row__container{display:flex;justify-content:space-between}.ssu-title-row__container__link{padding:0}.ssu-horizontal-bar-chart__container{padding:16px;overflow:hidden;font-size:13px}.ssu-horizontal-bar-chart__chart{box-sizing:border-box;margin-top:15px;color:var(--token-color-foreground-primary);fill:var(--token-color-foreground-primary)}.ssu-horizontal-bar-chart__chart svg{overflow:visible}.ssu-horizontal-bar-chart__chart rect{fill:var(--token-color-palette-blue-200);transform:translateY(10.5px);rx:3px;ry:3px}.ssu-horizontal-bar-chart__chart .axis line{display:none}.ssu-horizontal-bar-chart__separator{margin-bottom:10px}.ssu-horizontal-bar-chart__empty-state{min-width:66%;margin:16px auto}.ssu-horizontal-bar-chart__total{color:var(--token-color-foreground-primary)}

View File

@ -33,15 +33,15 @@
"scripts": {
"build": "rollup --config",
"format": "prettier . --cache --write",
"lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
"lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && run format",
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
"lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && run format",
"lint:format": "prettier . --cache --check",
"lint:hbs": "ember-template-lint . --no-error-on-unmatched-pattern",
"lint:hbs:fix": "ember-template-lint . --fix --no-error-on-unmatched-pattern",
"lint:js": "eslint . --cache",
"lint:js:fix": "eslint . --fix",
"lint:types": "glint",
"prepack": "rollup --config",
"prepack": "pnpm build",
"start": "rollup --config --watch",
"test": "echo 'A v2 addon does not have tests, run tests in test-app'",
"sync-to-vault": "node scripts/sync-to-vault.mjs"
@ -59,10 +59,13 @@
"@embroider/addon-dev": "^7.1.0",
"@embroider/addon-shim": "^1.8.9",
"@eslint/js": "^9.17.0",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
"@glint/core": "^1.4.0",
"@glint/environment-ember-loose": "^1.4.0",
"@glint/environment-ember-template-imports": "^1.4.0",
"@glint/template": "^1.4.0",
"@hashicorp/flight-icons": "^3.10.0",
"@rollup/plugin-babel": "^6.0.4",
"@tsconfig/ember": "^3.0.8",
"babel-plugin-ember-template-compilation": "^2.2.5",
@ -106,7 +109,8 @@
"./components/vault-reporting/horizontal-bar-chart.js": "./dist/_app_/components/vault-reporting/horizontal-bar-chart.js",
"./components/vault-reporting/views/dashboard.js": "./dist/_app_/components/vault-reporting/views/dashboard.js",
"./modifiers/axis-offset.js": "./dist/_app_/modifiers/axis-offset.js",
"./modifiers/css-custom-property.js": "./dist/_app_/modifiers/css-custom-property.js"
"./modifiers/css-custom-property.js": "./dist/_app_/modifiers/css-custom-property.js",
"./services/reporting-analytics.js": "./dist/_app_/services/reporting-analytics.js"
}
}
}

View File

@ -7202,7 +7202,7 @@ __metadata:
languageName: node
linkType: hard
"core-js@npm:^3.24.1":
"core-js@npm:^3.24.1, core-js@npm:^3.38.1":
version: 3.41.0
resolution: "core-js@npm:3.41.0"
checksum: 05331e92f354d3b92fa296fb3fc90c7b25d1a65230046c68651be29d67245bb156bdde7bf5c8cf8b3ecddfff93273a9ba6567f5e4ee6ceb70f39cee430693509
@ -10279,6 +10279,13 @@ __metadata:
languageName: node
linkType: hard
"fflate@npm:^0.4.8":
version: 0.4.8
resolution: "fflate@npm:0.4.8"
checksum: 29d8cbe44d5e7f53e7f5a160ac7f9cc025480c7b3bfd85c5f898cbe20dfa2dad4732daa534982664bf30b35896a90af44ea33ede5d94c5ffd1b8b0c0a0a56ca2
languageName: node
linkType: hard
"figures@npm:^2.0.0":
version: 2.0.0
resolution: "figures@npm:2.0.0"
@ -15235,6 +15242,33 @@ __metadata:
languageName: node
linkType: hard
"posthog-js@npm:^1.202.2":
version: 1.236.1
resolution: "posthog-js@npm:1.236.1"
dependencies:
core-js: ^3.38.1
fflate: ^0.4.8
preact: ^10.19.3
web-vitals: ^4.2.4
peerDependencies:
"@rrweb/types": 2.0.0-alpha.17
rrweb-snapshot: 2.0.0-alpha.17
peerDependenciesMeta:
"@rrweb/types":
optional: true
rrweb-snapshot:
optional: true
checksum: 114f30c71024c425ce31625f1de7ca154feb6bb18a42be439b9595bccc8e8e4c71ba9c075f3215a68d6dadf3168f69a3cc38ffcc1260dd9ec56efc1881c50ca4
languageName: node
linkType: hard
"preact@npm:^10.19.3":
version: 10.26.5
resolution: "preact@npm:10.26.5"
checksum: 3ef769e82bdb26314e51f02d09aea9ba566cad291504a5d79833055b1ff3ca7db647516315d2278498261881c61949917aac05cc56f172c32d013796378c304b
languageName: node
linkType: hard
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@ -18633,6 +18667,7 @@ __metadata:
miragejs: ~0.1.48
node-notifier: 8.0.2
pkijs: ~2.4.0
posthog-js: ^1.202.2
prettier: 3.0.3
prettier-eslint-cli: ~7.1.0
pvutils: ~1.1.3
@ -18820,6 +18855,13 @@ __metadata:
languageName: node
linkType: hard
"web-vitals@npm:^4.2.4":
version: 4.2.4
resolution: "web-vitals@npm:4.2.4"
checksum: 5b3ffe1db33f23aebf8cc8560ac574401a95939baafde5841835c1bb1c01f9a2478442f319f77aa0d7914739fc2f6b020c5d5b128c16c5c77ca6be2f9dfbbde6
languageName: node
linkType: hard
"webpack-sources@npm:^3.2.3":
version: 3.2.3
resolution: "webpack-sources@npm:3.2.3"