mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
UI/client count 1.10 (#13574)
* UI/total client usage (#13359) * blah * setup * clean up * rename history to dashboard * clean up * Styling fixes (#13369) * styling * clean up * UI/ horizontal bar chart component (#13361) * horizontal bar chart component * adds horizontal chart to dashboard file * add export class * yarn install d3 array * yarn install d3 array * adds data subtext to chart * update naming to plural charts" * updates css grid to 6 columns" * UI/tooltip (#13397) * working state * stuff * adds month tick marks and sort of y-axis, but y scale still messed up * moves y scale so zero shows again * fixes translating constants * format numbers y axis * actually fixes viewbox * styling for x and y axis plus gridlines * clean up * separates grid types based on content Co-authored-by: Claire Bontempo <cbontempo@hashicorp.com> * Styling and legend component (#13430) * styling * cleanup * UI/ Double horizontal bar charts (#13398) * add descriptions and styling to side by side charts * add border below horizontal charts * starts legend styling * center legend * add to do * add hover actions/event listeners * UI/merge main (#13436) * Rename master key to root key (#13324) * See what it looks like to replace "master key" with "root key". There are two places that would require more challenging code changes: the storage path `core/master`, and its contents (the JSON-serialized EncodedKeyringtructure.) * Restore accidentally deleted line * Add changelog * Update root->recovery * Fix test Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * Fix typo (#13355) * Add kms_library configuration stanza (#13352) - Add the kms_library configuration stanza to Vault's command/server - Provide validation of keys and general configuration. - Add initial kms_library configuration documentation - Attempt at startup to verify we can read the configured HSM Library - Hook in KmsLibrary config into the Validate to detect typo/unused keys * modifed note (#13351) * Incorporate Ember Flight Icons (#12976) * adds ember-flight-icons dependecy * adds inline-json-import babel plugin * adds flight icon styling * updates Icon component to support flight icons * updates Icon component usages to new api and updates name values to flight icon set when available * fixes tests * updates icon story with flight mappings and fixes issue with flight icons not rendering in storybook * adds changelog * fixes typo in sign action glyph name in transit-key model * adds comments to icon-map * updates Icon component to use only supported flight icon sizes * adds icon transform codemod * updates icon transform formatting to handle edge case * runs icon transform on templates * updates Icon usage in toolbar-filter md and story * updates tests * docs: winsvc update recommendations (#13280) * docs: update custom database sample code (#13211) * clarify more sink options (#12586) * Update @hashicorp/react-hashi-stack-menu (#13354) * Docs to clarify k8s auth options with short-lived tokens (#13275) * Rework 1.21 content into one heading and add note at top * Add notes about extended k8s token duration * Add example of ClusterRoleBinding for using client JWTs * Adds support for SHA-3 to transit (#13367) * Adding support for SHA3 in the transit backend. * Adds SHA-3 tests for transit sign/verify path. Adds SHA-3 tests for logical system tools path hash functionality. Updates documentation to include SHA-3 algorithms in system tools path hashing. * Adds changelog entry. Co-authored-by: robison jacka <robison@packetized.io> * agent/cache: differentiate open log messages (#13362) Changes the error output for the second open of the persistent cache file, to differentiate it from the c.UI.Error message for the initial open of the cache file, just to make it easier to tell where a problem occurred. * Warn user supplying nonce values in FIPS mode for transit encryption requests (#13366) * Warn user supplying nonce values in FIPS mode for transit encryption requests - Send back a warning within the response if an end-user supplies nonce values that we use within the various transit encrypt apis. - We do not send a warning if an end-user supplies a nonce value but we don't use it. - Affected api methods are encrypt, rewrap and datakey - The warning is only sent when we are operating in FIPS mode. * [VAULT-3252] Add entity-alias behavior change to docs (#13370) * Add entity-alias behavior change to docs * Add upgrade note about entity-alias mapping change * Rename 1.7-9 upgrade pages, shuffle upgrade note position * Update website/content/partials/entity-alias-mapping.mdx Co-authored-by: Meggie <meggie@hashicorp.com> * Add incorrect policy issue to the docs * Add example about entity-alias restriction Co-authored-by: Meggie <meggie@hashicorp.com> * VAULT-1564 report in-flight requests (#13024) * VAULT-1564 report in-flight requests * adding a changelog * Changing some variable names and fixing comments * minor style change * adding unauthenticated support for in-flight-req * adding documentation for the listener.profiling stanza * adding an atomic counter for the inflight requests addressing comments * addressing comments * logging completed requests * fixing a test * providing log_requests_info as a config option to determine at which level requests should be logged * removing a member and a method from the StatusHeaderResponseWriter struct * adding api docks * revert changes in NewHTTPResponseWriter * Fix logging invalid log_requests_info value * Addressing comments * Fixing a test * use an tomic value for logRequestsInfo, and moving the CreateClientID function to Core * fixing go.sum * minor refactoring * protecting InFlightRequests from data race * another try on fixing a data race * another try to fix a data race * addressing comments * fixing couple of tests * changing log_requests_info to log_requests_level * minor style change * fixing a test * removing the lock in InFlightRequests * use single-argument form for interface assertion * adding doc for the new configuration paramter * adding the new doc to the nav data file * minor fix * auth/jwt: Update plugin to v0.11.3 (#13365) * auth/jwt: Update plugin to v0.11.3 * add changelog * changelog++ * Update alert banner (#13375) * Updating website for 1.9.1 (#13378) * Use os.Hostname instead of a dependency that doesn't work on OpenBSD. (#13389) * Remove another use gopsutil/host. (#13390) * CLI changes for new mount tune config parameter allowed_managed_keys (#13255) * CLI changes for new mount tune config parameter allowed_managed_keys * Correct allowed_managed_keys description in auth and secrets * Documentation update for secrets and removed changes for auth * Add changelog and remove documentation changes for auth * removed changelog * Correct the field description * auth/jwt: update changelog for pkce improvement (#13392) * Fix test validating convergent encryption behaviour across key types (#13371) - The test was attempting to test the convergent encryption behaviour with several key types but the common function never used the passed in key type. So we ran the test with the default aes256-gcm96 only. * Fix managed namespace test (#13394) * Fix managed namespace test * Remove log * Some changelog tidying for 1.10 preview (#13385) * Some changelog tidying for 1.10 preview * PR accounted for by different CL entry * changelog++ Working on a new workflow for generating the preview so I thought I'd leave a note that it's still coming. * UI/fix client count partial (#13396) * Initial fix * Add fallback zero values * Add changelog * Fix client count current test * Support clearing an identity alias' custom_metadata (#13395) * Support clearing an identity alias' custom_metadata Previously, an update to an entity alias supported updating the custom_metadata as long as the update was not empty, which makes it impossible to clear the metadata values completely. Fixes: - empty custom_metadata parameters are honoured on entity alias update - update related tests - drop dependency on mapstructure - reformat with gofumpt * Docs: fix invalid link in the kubernetes auth api doc. (#13399) * Clean up whitespace * auth/azure: add note about debug env (#13405) * auth/azure: add note about debug env * Update azure.mdx * Update azure.mdx * Add universal default key_bits value for PKI endpoints (#13080) * Allow universal default for key_bits This allows the key_bits field to take a universal default value, 0, which, depending on key_type, gets adjusted appropriately into a specific default value (rsa->2048, ec->256, ignored under ed25519). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Handle universal default key size in certutil Also move RSA < 2048 error message into certutil directly, instead of in ca_util/path_roles. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add missing RSA key sizes to pki/backend_test.go Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Switch to returning updated values When determining the default, don't pass in pointer types, but instead return the newly updated value. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add changelog entry Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Re-add fix for ed25519 from #13254 Ed25519 internally specifies a hash length; by changing the default from 256 to 0, we fail validation in ValidateSignatureLength(...) unless we specify the key algorithm. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix logging statement using formatting args (#13407) * Add docs about path param restrictions (#13413) * Add docs about path param restrictions * Update website/content/api-docs/auth/userpass.mdx Co-authored-by: Loann Le <84412881+taoism4504@users.noreply.github.com> * Update with review suggestion Co-authored-by: Loann Le <84412881+taoism4504@users.noreply.github.com> * Update raftautosnapshots.mdx (#13412) * Main go version bump (#13408) * Go 1.17.2 -> 1.17.5 * Switching to cimg * Bump yarn cache key version so that it uses the new disk layout we've adopted for using cimg/go. (#13420) * Add vault-api module (#13048) * crt main fix for ecr tag (#13425) * Add no-op method setupManagedKeyRegistry(). (#13433) * github auth: use org id to verify creds (#13332) * github auth: use org id to verify creds * add check for required org param; add test case * update UTs * add nil check for org * add changelog * fix typo in ut * set org ID if it is unset; add more ut coverage * add optional organization_id * move client instantiation * refactor parse URL; add UT for setting org ID * fix comment in UT * add nil check * don't update org name on change; return warning * refactor verifyCredentials * error when unable to fetch org ID on config write; add warnings * fix bug in log message * update UT and small refactor * update comments and log msg * use getter for org ID Co-authored-by: Jim Kalafut <jkalafut@hashicorp.com> Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> Co-authored-by: Harsimran Singh Maan <maan.harry@gmail.com> Co-authored-by: Steven Clark <steven.clark@hashicorp.com> Co-authored-by: Loann Le <84412881+taoism4504@users.noreply.github.com> Co-authored-by: Jordan Reimer <zofskeez@gmail.com> Co-authored-by: mickael-hc <86245626+mickael-hc@users.noreply.github.com> Co-authored-by: Calvin Leung Huang <1883212+calvn@users.noreply.github.com> Co-authored-by: Mike Green <772413+mikegreen@users.noreply.github.com> Co-authored-by: Noel Quiles <3746694+EnMod@users.noreply.github.com> Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com> Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com> Co-authored-by: robison jacka <robison@packetized.io> Co-authored-by: Theron Voran <tvoran@users.noreply.github.com> Co-authored-by: Pratyoy Mukhopadhyay <35388175+pmmukh@users.noreply.github.com> Co-authored-by: Meggie <meggie@hashicorp.com> Co-authored-by: hghaf099 <83242695+hghaf099@users.noreply.github.com> Co-authored-by: John-Michael Faircloth <fairclothjm@users.noreply.github.com> Co-authored-by: Brandon Romano <brandon@hashicorp.com> Co-authored-by: divyapola5 <87338962+divyapola5@users.noreply.github.com> Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Co-authored-by: Ben Ash <32777270+benashz@users.noreply.github.com> Co-authored-by: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com> Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com> Co-authored-by: Mark Lewis <56076038+ml4@users.noreply.github.com> Co-authored-by: Sai Hemanth Bheemreddy <35338241+SaiHemanthBR@users.noreply.github.com> Co-authored-by: Kyle Penfound <kpenfound11@gmail.com> Co-authored-by: Victor Rodriguez <vrizo@hashicorp.com> * UI/chart legend (#13437) * fixes axes lines blend * add pixel conversions to variable css file * reorganizes css file * adds legend * fixes scales and makes room for legend * fixes grid for dual charts * made grid responsive * fixes legend styling * fixes legend, removes ticks and fixes scale * adjusts tooltip target * un-comment mouse events * remove console log * UI/ Client counts range (running total component) (#13477) * grid for stacked charts * pass in data as arg from parent * pull out vertical bar chart component * refactor to use vertical bar chart component * remove any chart handling stuff from parent * rename variables * refactor horizontal bar chart into separate component * move descriptions to inside template (not passed in) * constructs attribution copy * add sample response to mirage config * change indenting * rename to MonthlyUsage * change name to running totals * rename variable * finishes line chart * pull constants to util * cleanup add todos * fix formatNumbers return" * comments and cleanup * adds tooltip to line chart * make cover area larger * fixes tooltip styling * adds tooltip styling" * adds tooltip modal to horizontal chart * finishes tooltip for horizontal chart * remove click event arg * merges main and fixes conflicts * bumps yarn.lock * linting fix * clean up go files and changelog * more clean up * remove changelog * fix * update component documentation for jsdocs * removing test to see if that helps with browserstack * remove new packages to test dep failure * add ember-modal-dialog * add ember-tether * add ember-tether * fixes mirage config file - merge conflict issue * remove general spacing variable Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> Co-authored-by: Claire Bontempo <cbontempo@hashicorp.com> Co-authored-by: Jim Kalafut <jkalafut@hashicorp.com> Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> Co-authored-by: Harsimran Singh Maan <maan.harry@gmail.com> Co-authored-by: Steven Clark <steven.clark@hashicorp.com> Co-authored-by: Loann Le <84412881+taoism4504@users.noreply.github.com> Co-authored-by: Jordan Reimer <zofskeez@gmail.com> Co-authored-by: mickael-hc <86245626+mickael-hc@users.noreply.github.com> Co-authored-by: Calvin Leung Huang <1883212+calvn@users.noreply.github.com> Co-authored-by: Mike Green <772413+mikegreen@users.noreply.github.com> Co-authored-by: Noel Quiles <3746694+EnMod@users.noreply.github.com> Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com> Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com> Co-authored-by: robison jacka <robison@packetized.io> Co-authored-by: Theron Voran <tvoran@users.noreply.github.com> Co-authored-by: Pratyoy Mukhopadhyay <35388175+pmmukh@users.noreply.github.com> Co-authored-by: Meggie <meggie@hashicorp.com> Co-authored-by: hghaf099 <83242695+hghaf099@users.noreply.github.com> Co-authored-by: John-Michael Faircloth <fairclothjm@users.noreply.github.com> Co-authored-by: Brandon Romano <brandon@hashicorp.com> Co-authored-by: divyapola5 <87338962+divyapola5@users.noreply.github.com> Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Co-authored-by: Ben Ash <32777270+benashz@users.noreply.github.com> Co-authored-by: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com> Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com> Co-authored-by: Mark Lewis <56076038+ml4@users.noreply.github.com> Co-authored-by: Sai Hemanth Bheemreddy <35338241+SaiHemanthBR@users.noreply.github.com> Co-authored-by: Kyle Penfound <kpenfound11@gmail.com> Co-authored-by: Victor Rodriguez <vrizo@hashicorp.com>
This commit is contained in:
parent
cab32414df
commit
7b3b455a7b
22 changed files with 1691 additions and 83 deletions
45
ui/app/components/clients/attribution.js
Normal file
45
ui/app/components/clients/attribution.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import Component from '@glimmer/component';
|
||||
|
||||
// TODO: fill out below!!
|
||||
/**
|
||||
* @module Attribution
|
||||
* Attribution components are used to...
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <Attribution @requiredParam={requiredParam} @optionalParam={optionalParam} @param1={{param1}}>
|
||||
* Pass in export button
|
||||
* </Attribution>
|
||||
* ```
|
||||
* @param {object} requiredParam - requiredParam is...
|
||||
* @param {string} [optionalParam] - optionalParam is...
|
||||
* @param {string} [param1=defaultValue] - param1 is...
|
||||
*/
|
||||
|
||||
export default class Attribution extends Component {
|
||||
get dateRange() {
|
||||
// some conditional that returns "date range" or "month" depending on what the params are
|
||||
return 'date range';
|
||||
}
|
||||
|
||||
get chartText() {
|
||||
// something that determines if data is by namespace or by auth method
|
||||
// and returns text
|
||||
// if byNamespace
|
||||
return {
|
||||
description:
|
||||
'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.',
|
||||
newCopy: `The new clients in the namespace for this ${this.dateRange}.
|
||||
This aids in understanding which namespaces create and use new clients
|
||||
${this.dateRange === 'date range' ? ' over time.' : '.'}`,
|
||||
totalCopy: `The total clients in the namespace for this ${this.dateRange}. This number is useful for identifying overall usage volume.`,
|
||||
};
|
||||
// if byAuthMethod
|
||||
// return
|
||||
// byAuthMethod = {
|
||||
// description: "This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where new clients and total clients are originating. Authentication methods are organized by path.",
|
||||
// newCopy: `The new clients used by the auth method for this {{@range}}. This aids in understanding which auth methods create and use new clients ${this.dateRange === "date range" ? " over time." : "."}`,
|
||||
// totalCopy: `The total clients used by the auth method for this ${this.dateRange}. This number is useful for identifying overall usage volume. `
|
||||
// }
|
||||
}
|
||||
}
|
||||
182
ui/app/components/clients/dashboard.js
Normal file
182
ui/app/components/clients/dashboard.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export default class Dashboard extends Component {
|
||||
maxNamespaces = 10;
|
||||
chartLegend = [
|
||||
{ key: 'distinct_entities', label: 'unique entities' },
|
||||
{ key: 'non_entity_tokens', label: 'non-entity tokens' },
|
||||
];
|
||||
@tracked selectedNamespace = null;
|
||||
|
||||
@tracked barChartSelection = false;
|
||||
|
||||
// Determine if we have client count data based on the current tab
|
||||
get hasClientData() {
|
||||
if (this.args.tab === 'current') {
|
||||
// Show the current numbers as long as config is on
|
||||
return this.args.model.config?.enabled !== 'Off';
|
||||
}
|
||||
return this.args.model.activity && this.args.model.activity.total;
|
||||
}
|
||||
|
||||
// Show namespace graph only if we have more than 1
|
||||
get showGraphs() {
|
||||
return (
|
||||
this.args.model.activity &&
|
||||
this.args.model.activity.byNamespace &&
|
||||
this.args.model.activity.byNamespace.length > 1
|
||||
);
|
||||
}
|
||||
|
||||
// Construct the namespace model for the search select component
|
||||
get searchDataset() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
let dataList = this.args.model.activity.byNamespace;
|
||||
return dataList.map((d) => {
|
||||
return {
|
||||
name: d['namespace_id'],
|
||||
id: d['namespace_path'] === '' ? 'root' : d['namespace_path'],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Construct the namespace model for the bar chart component
|
||||
get barChartDataset() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
let dataset = this.args.model.activity.byNamespace.slice(0, this.maxNamespaces);
|
||||
return dataset.map((d) => {
|
||||
return {
|
||||
label: d['namespace_path'] === '' ? 'root' : d['namespace_path'],
|
||||
// the order here determines which data is the left bar and which is the right
|
||||
distinct_entities: d['counts']['distinct_entities'],
|
||||
non_entity_tokens: d['counts']['non_entity_tokens'],
|
||||
total: d['counts']['clients'],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: dataset for line chart
|
||||
get lineChartData() {
|
||||
return [
|
||||
{ month: '1/21', clients: 100, new: 100 },
|
||||
{ month: '2/21', clients: 300, new: 200 },
|
||||
{ month: '3/21', clients: 300, new: 0 },
|
||||
{ month: '4/21', clients: 300, new: 0 },
|
||||
{ month: '5/21', clients: 300, new: 0 },
|
||||
{ month: '6/21', clients: 300, new: 0 },
|
||||
{ month: '7/21', clients: 300, new: 0 },
|
||||
{ month: '8/21', clients: 350, new: 50 },
|
||||
{ month: '9/21', clients: 400, new: 50 },
|
||||
{ month: '10/21', clients: 450, new: 50 },
|
||||
{ month: '11/21', clients: 500, new: 50 },
|
||||
{ month: '12/21', clients: 1000, new: 1000 },
|
||||
];
|
||||
}
|
||||
|
||||
// TODO: dataset for new monthly clients vertical bar chart (manage in serializer?)
|
||||
get newMonthlyClients() {
|
||||
return [
|
||||
{ month: 'January', distinct_entities: 1000, non_entity_tokens: 322, total: 1322 },
|
||||
{ month: 'February', distinct_entities: 1500, non_entity_tokens: 122, total: 1622 },
|
||||
{ month: 'March', distinct_entities: 4300, non_entity_tokens: 700, total: 5000 },
|
||||
{ month: 'April', distinct_entities: 1550, non_entity_tokens: 229, total: 1779 },
|
||||
{ month: 'May', distinct_entities: 5560, non_entity_tokens: 124, total: 5684 },
|
||||
{ month: 'June', distinct_entities: 1570, non_entity_tokens: 142, total: 1712 },
|
||||
{ month: 'July', distinct_entities: 300, non_entity_tokens: 112, total: 412 },
|
||||
{ month: 'August', distinct_entities: 1610, non_entity_tokens: 130, total: 1740 },
|
||||
{ month: 'September', distinct_entities: 1900, non_entity_tokens: 222, total: 2122 },
|
||||
{ month: 'October', distinct_entities: 500, non_entity_tokens: 166, total: 666 },
|
||||
{ month: 'November', distinct_entities: 480, non_entity_tokens: 132, total: 612 },
|
||||
{ month: 'December', distinct_entities: 980, non_entity_tokens: 202, total: 1182 },
|
||||
];
|
||||
}
|
||||
|
||||
// TODO: dataset for vault usage vertical bar chart (manage in serializer?)
|
||||
get monthlyUsage() {
|
||||
return [
|
||||
{ month: 'January', distinct_entities: 1000, non_entity_tokens: 322, total: 1322 },
|
||||
{ month: 'February', distinct_entities: 1500, non_entity_tokens: 122, total: 1622 },
|
||||
{ month: 'March', distinct_entities: 4300, non_entity_tokens: 700, total: 5000 },
|
||||
{ month: 'April', distinct_entities: 1550, non_entity_tokens: 229, total: 1779 },
|
||||
{ month: 'May', distinct_entities: 5560, non_entity_tokens: 124, total: 5684 },
|
||||
{ month: 'June', distinct_entities: 1570, non_entity_tokens: 142, total: 1712 },
|
||||
{ month: 'July', distinct_entities: 300, non_entity_tokens: 112, total: 412 },
|
||||
{ month: 'August', distinct_entities: 1610, non_entity_tokens: 130, total: 1740 },
|
||||
{ month: 'September', distinct_entities: 1900, non_entity_tokens: 222, total: 2122 },
|
||||
{ month: 'October', distinct_entities: 500, non_entity_tokens: 166, total: 666 },
|
||||
{ month: 'November', distinct_entities: 480, non_entity_tokens: 132, total: 612 },
|
||||
{ month: 'December', distinct_entities: 980, non_entity_tokens: 202, total: 1182 },
|
||||
];
|
||||
}
|
||||
|
||||
// Create namespaces data for csv format
|
||||
get getCsvData() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
let results = '',
|
||||
namespaces = this.args.model.activity.byNamespace,
|
||||
fields = ['Namespace path', 'Active clients', 'Unique entities', 'Non-entity tokens'];
|
||||
|
||||
results = fields.join(',') + '\n';
|
||||
|
||||
namespaces.forEach(function (item) {
|
||||
let path = item.namespace_path !== '' ? item.namespace_path : 'root',
|
||||
total = item.counts.clients,
|
||||
unique = item.counts.distinct_entities,
|
||||
non_entity = item.counts.non_entity_tokens;
|
||||
|
||||
results += path + ',' + total + ',' + unique + ',' + non_entity + '\n';
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
// Return csv filename with start and end dates
|
||||
get getCsvFileName() {
|
||||
let defaultFileName = `clients-by-namespace`,
|
||||
startDate =
|
||||
this.args.model.queryStart || `${format(new Date(this.args.model.activity.startTime), 'MM-yyyy')}`,
|
||||
endDate =
|
||||
this.args.model.queryEnd || `${format(new Date(this.args.model.activity.endTime), 'MM-yyyy')}`;
|
||||
if (startDate && endDate) {
|
||||
defaultFileName += `-${startDate}-${endDate}`;
|
||||
}
|
||||
return defaultFileName;
|
||||
}
|
||||
|
||||
// Get the namespace by matching the path from the namespace list
|
||||
getNamespace(path) {
|
||||
return this.args.model.activity.byNamespace.find((ns) => {
|
||||
if (path === 'root') {
|
||||
return ns.namespace_path === '';
|
||||
}
|
||||
return ns.namespace_path === path;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
selectNamespace(value) {
|
||||
// In case of search select component, value returned is an array
|
||||
if (Array.isArray(value)) {
|
||||
this.selectedNamespace = this.getNamespace(value[0]);
|
||||
this.barChartSelection = false;
|
||||
} else if (typeof value === 'object') {
|
||||
// While D3 bar selection returns an object
|
||||
this.selectedNamespace = this.getNamespace(value.label);
|
||||
this.barChartSelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
resetData() {
|
||||
this.barChartSelection = false;
|
||||
this.selectedNamespace = null;
|
||||
}
|
||||
}
|
||||
225
ui/app/components/clients/horizontal-bar-chart.js
Normal file
225
ui/app/components/clients/horizontal-bar-chart.js
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { stack } from 'd3-shape';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { select, event, selectAll } from 'd3-selection';
|
||||
import { scaleLinear, scaleBand } from 'd3-scale';
|
||||
import { axisLeft } from 'd3-axis';
|
||||
import { max, maxIndex } from 'd3-array';
|
||||
import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE } from '../../utils/chart-helpers';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
/**
|
||||
* @module HorizontalBarChart
|
||||
* HorizontalBarChart components are used to display data in the form of a horizontal, stacked bar chart with accompanying tooltip.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <HorizontalBarChart @dataset={{@dataset}} @chartLegend={{@chartLegend}}/>
|
||||
* ```
|
||||
* @param {array} dataset - dataset for the chart, must be an array of flattened objects
|
||||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||
*/
|
||||
|
||||
// TODO: delete original bar chart component
|
||||
|
||||
// SIZING CONSTANTS
|
||||
const CHART_MARGIN = { top: 10, left: 95 }; // makes space for y-axis legend
|
||||
const TRANSLATE = { down: 13 };
|
||||
const CHAR_LIMIT = 15; // character count limit for y-axis labels to trigger truncating
|
||||
const LINE_HEIGHT = 24; // each bar w/ padding is 24 pixels thick
|
||||
|
||||
export default class HorizontalBarChart extends Component {
|
||||
@tracked tooltipTarget = '';
|
||||
@tracked tooltipText = '';
|
||||
|
||||
get labelKey() {
|
||||
return this.args.labelKey || 'label';
|
||||
}
|
||||
|
||||
get chartLegend() {
|
||||
return this.args.chartLegend;
|
||||
}
|
||||
|
||||
get topNamespace() {
|
||||
return this.args.dataset[maxIndex(this.args.dataset, (d) => d.total)];
|
||||
}
|
||||
|
||||
@action removeTooltip() {
|
||||
this.tooltipTarget = null;
|
||||
}
|
||||
|
||||
@action
|
||||
renderChart(element, args) {
|
||||
// chart legend tells stackFunction how to stack/organize data
|
||||
// creates an array of data for each key name
|
||||
// each array contains coordinates for each data bar
|
||||
let stackFunction = stack().keys(this.chartLegend.map((l) => l.key));
|
||||
let dataset = args[0];
|
||||
let stackedData = stackFunction(dataset);
|
||||
let labelKey = this.labelKey;
|
||||
|
||||
let xScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d.total))])
|
||||
.range([0, 75]); // 25% reserved for margins
|
||||
|
||||
let yScale = scaleBand()
|
||||
.domain(dataset.map((d) => d[labelKey]))
|
||||
.range([0, dataset.length * LINE_HEIGHT])
|
||||
.paddingInner(0.765); // percent of the total width to reserve for padding between bars
|
||||
|
||||
let chartSvg = select(element);
|
||||
chartSvg.attr('width', '100%').attr('viewBox', `0 0 564 ${(dataset.length + 1) * LINE_HEIGHT}`);
|
||||
// chartSvg.attr('viewBox', `0 0 700 300`);
|
||||
|
||||
let groups = chartSvg
|
||||
.selectAll('g')
|
||||
.remove()
|
||||
.exit()
|
||||
.data(stackedData)
|
||||
.enter()
|
||||
.append('g')
|
||||
// shifts chart to accommodate y-axis legend
|
||||
.attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top})`)
|
||||
.style('fill', (d, i) => LIGHT_AND_DARK_BLUE[i]);
|
||||
|
||||
let yAxis = axisLeft(yScale).tickSize(0);
|
||||
yAxis(chartSvg.append('g').attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top})`));
|
||||
|
||||
chartSvg.select('.domain').remove();
|
||||
|
||||
let truncate = (selection) =>
|
||||
selection.text((string) =>
|
||||
string.length < CHAR_LIMIT ? string : string.slice(0, CHAR_LIMIT - 3) + '...'
|
||||
);
|
||||
|
||||
chartSvg.selectAll('.tick text').call(truncate);
|
||||
|
||||
groups
|
||||
.selectAll('rect')
|
||||
// iterate through the stacked data and chart respectively
|
||||
.data((stackedData) => stackedData)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('class', 'data-bar')
|
||||
.style('cursor', 'pointer')
|
||||
.attr('width', (chartData) => `${xScale(chartData[1] - chartData[0]) - 0.25}%`)
|
||||
.attr('height', yScale.bandwidth())
|
||||
.attr('x', (chartData) => `${xScale(chartData[0])}%`)
|
||||
.attr('y', ({ data }) => yScale(data[labelKey]))
|
||||
.attr('rx', 3)
|
||||
.attr('ry', 3);
|
||||
|
||||
let actionBars = chartSvg
|
||||
.selectAll('.action-bar')
|
||||
.data(dataset)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.style('cursor', 'pointer')
|
||||
.attr('class', 'action-bar')
|
||||
.attr('width', '100%')
|
||||
.attr('height', `${LINE_HEIGHT}px`)
|
||||
.attr('x', '0')
|
||||
.attr('y', (chartData) => yScale(chartData[labelKey]))
|
||||
.style('fill', `${GREY}`)
|
||||
.style('opacity', '0')
|
||||
.style('mix-blend-mode', 'multiply');
|
||||
|
||||
let yLegendBars = chartSvg
|
||||
.selectAll('.label-bar')
|
||||
.data(dataset)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.style('cursor', 'pointer')
|
||||
.attr('class', 'label-action-bar')
|
||||
.attr('width', CHART_MARGIN.left)
|
||||
.attr('height', `${LINE_HEIGHT}px`)
|
||||
.attr('x', '0')
|
||||
.attr('y', (chartData) => yScale(chartData[labelKey]))
|
||||
.style('opacity', '0')
|
||||
.style('mix-blend-mode', 'multiply');
|
||||
|
||||
let dataBars = chartSvg.selectAll('rect.data-bar');
|
||||
let actionBarSelection = chartSvg.selectAll('rect.action-bar');
|
||||
|
||||
let compareAttributes = (elementA, elementB, attr) =>
|
||||
select(elementA).attr(`${attr}`) === select(elementB).attr(`${attr}`);
|
||||
|
||||
// MOUSE EVENTS FOR DATA BARS
|
||||
actionBars
|
||||
.on('mouseover', (data) => {
|
||||
let hoveredElement = actionBars.filter((bar) => bar.label === data.label).node();
|
||||
this.tooltipTarget = hoveredElement;
|
||||
this.tooltipText = `${Math.round((data.total * 100) / 19000)}% of total client counts:
|
||||
${data.non_entity_tokens} non-entity tokens, ${data.distinct_entities} unique entities.`;
|
||||
|
||||
select(hoveredElement).style('opacity', 1);
|
||||
|
||||
dataBars
|
||||
.filter(function () {
|
||||
return compareAttributes(this, hoveredElement, 'y');
|
||||
})
|
||||
.style('fill', (b, i) => `${BAR_COLOR_HOVER[i]}`);
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
select(this).style('opacity', 0);
|
||||
dataBars
|
||||
.filter(function () {
|
||||
return compareAttributes(this, event.target, 'y');
|
||||
})
|
||||
.style('fill', (b, i) => `${LIGHT_AND_DARK_BLUE[i]}`);
|
||||
});
|
||||
|
||||
// MOUSE EVENTS FOR Y-AXIS LABELS
|
||||
yLegendBars
|
||||
.on('mouseover', (data) => {
|
||||
if (data.label.length >= CHAR_LIMIT) {
|
||||
let hoveredElement = yLegendBars.filter((bar) => bar.label === data.label).node();
|
||||
this.tooltipTarget = hoveredElement;
|
||||
this.tooltipText = data.label;
|
||||
} else {
|
||||
this.tooltipTarget = null;
|
||||
}
|
||||
dataBars
|
||||
.filter(function () {
|
||||
return compareAttributes(this, event.target, 'y');
|
||||
})
|
||||
.style('fill', (b, i) => `${BAR_COLOR_HOVER[i]}`);
|
||||
actionBarSelection
|
||||
.filter(function () {
|
||||
return compareAttributes(this, event.target, 'y');
|
||||
})
|
||||
.style('opacity', '1');
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
this.tooltipTarget = null;
|
||||
dataBars
|
||||
.filter(function () {
|
||||
return compareAttributes(this, event.target, 'y');
|
||||
})
|
||||
.style('fill', (b, i) => `${LIGHT_AND_DARK_BLUE[i]}`);
|
||||
actionBarSelection
|
||||
.filter(function () {
|
||||
return compareAttributes(this, event.target, 'y');
|
||||
})
|
||||
.style('opacity', '0');
|
||||
});
|
||||
|
||||
// add client count total values to the right
|
||||
chartSvg
|
||||
.append('g')
|
||||
.attr('transform', `translate(${CHART_MARGIN.left}, ${TRANSLATE.down})`)
|
||||
.selectAll('text')
|
||||
.data(dataset)
|
||||
.enter()
|
||||
.append('text')
|
||||
.text((d) => d.total)
|
||||
.attr('fill', '#000')
|
||||
.attr('class', 'total-value')
|
||||
.style('font-size', '.8rem')
|
||||
.attr('text-anchor', 'start')
|
||||
.attr('alignment-baseline', 'middle')
|
||||
.attr('x', (chartData) => `${xScale(chartData.total)}%`)
|
||||
.attr('y', (chartData) => yScale(chartData.label));
|
||||
}
|
||||
}
|
||||
121
ui/app/components/clients/line-chart.js
Normal file
121
ui/app/components/clients/line-chart.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { max } from 'd3-array';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { select, selectAll, node } from 'd3-selection';
|
||||
import { axisLeft, axisBottom } from 'd3-axis';
|
||||
import { scaleLinear, scalePoint } from 'd3-scale';
|
||||
import { line } from 'd3-shape';
|
||||
import { LIGHT_AND_DARK_BLUE, SVG_DIMENSIONS, formatNumbers } from '../../utils/chart-helpers';
|
||||
|
||||
/**
|
||||
* @module LineChart
|
||||
* LineChart components are used to display data in a line plot with accompanying tooltip
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <LineChart @dataset={dataset} />
|
||||
* ```
|
||||
* @param {array} dataset - dataset is an array of objects
|
||||
*/
|
||||
|
||||
export default class LineChart extends Component {
|
||||
// TODO make just one tracked variable tooltipText?
|
||||
@tracked tooltipTarget = '';
|
||||
@tracked tooltipMonth = '';
|
||||
@tracked tooltipTotal = '';
|
||||
@tracked tooltipNew = '';
|
||||
|
||||
@action removeTooltip() {
|
||||
this.tooltipTarget = null;
|
||||
}
|
||||
|
||||
@action
|
||||
renderChart(element, args) {
|
||||
let dataset = args[0];
|
||||
let chartSvg = select(element);
|
||||
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
|
||||
|
||||
// DEFINE AXES SCALES
|
||||
let yScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d.clients))])
|
||||
.range([0, 100]);
|
||||
|
||||
let yAxisScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d.clients))]) // TODO will need to recalculate when you get the data
|
||||
.range([SVG_DIMENSIONS.height, 0]);
|
||||
|
||||
let xScale = scalePoint() // use scaleTime()?
|
||||
.domain(dataset.map((d) => d.month))
|
||||
.range([0, SVG_DIMENSIONS.width])
|
||||
.padding(0.2);
|
||||
|
||||
// CUSTOMIZE AND APPEND AXES
|
||||
let yAxis = axisLeft(yAxisScale)
|
||||
.ticks(7)
|
||||
.tickPadding(10)
|
||||
.tickSizeInner(-SVG_DIMENSIONS.width) // makes grid lines length of svg
|
||||
.tickFormat(formatNumbers);
|
||||
|
||||
let xAxis = axisBottom(xScale).tickSize(0);
|
||||
|
||||
yAxis(chartSvg.append('g'));
|
||||
xAxis(chartSvg.append('g').attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`));
|
||||
|
||||
chartSvg.selectAll('.domain').remove();
|
||||
|
||||
// PATH BETWEEN PLOT POINTS
|
||||
let lineGenerator = line()
|
||||
.x((d) => xScale(d.month))
|
||||
.y((d) => yAxisScale(d.clients));
|
||||
|
||||
chartSvg
|
||||
.append('g')
|
||||
.append('path')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', LIGHT_AND_DARK_BLUE[1])
|
||||
.attr('stroke-width', 0.5)
|
||||
.attr('d', lineGenerator(dataset));
|
||||
|
||||
// LINE PLOTS (CIRCLES)
|
||||
chartSvg
|
||||
.append('g')
|
||||
.selectAll('circle')
|
||||
.data(dataset)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'data-plot')
|
||||
.attr('cy', (d) => `${100 - yScale(d.clients)}%`)
|
||||
.attr('cx', (d) => xScale(d.month))
|
||||
.attr('r', 3.5)
|
||||
.attr('fill', LIGHT_AND_DARK_BLUE[0])
|
||||
.attr('stroke', LIGHT_AND_DARK_BLUE[1])
|
||||
.attr('stroke-width', 1.5);
|
||||
|
||||
// LARGER HOVER CIRCLES
|
||||
chartSvg
|
||||
.append('g')
|
||||
.selectAll('circle')
|
||||
.data(dataset)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'hover-circle')
|
||||
.style('cursor', 'pointer')
|
||||
.style('opacity', '0')
|
||||
.attr('cy', (d) => `${100 - yScale(d.clients)}%`)
|
||||
.attr('cx', (d) => xScale(d.month))
|
||||
.attr('r', 10);
|
||||
|
||||
let hoverCircles = chartSvg.selectAll('.hover-circle');
|
||||
|
||||
// MOUSE EVENT FOR TOOLTIP
|
||||
hoverCircles.on('mouseover', (data) => {
|
||||
this.tooltipMonth = data.month;
|
||||
this.tooltipTotal = `${data.clients} total clients`;
|
||||
this.tooltipNew = `${data.new} new clients`;
|
||||
let node = hoverCircles.filter((plot) => plot.month === data.month).node();
|
||||
this.tooltipTarget = node;
|
||||
});
|
||||
}
|
||||
}
|
||||
139
ui/app/components/clients/vertical-bar-chart.js
Normal file
139
ui/app/components/clients/vertical-bar-chart.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { max } from 'd3-array';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { select, selectAll, node } from 'd3-selection';
|
||||
import { axisLeft, axisBottom } from 'd3-axis';
|
||||
import { scaleLinear, scaleBand } from 'd3-scale';
|
||||
import { stack } from 'd3-shape';
|
||||
import {
|
||||
GREY,
|
||||
LIGHT_AND_DARK_BLUE,
|
||||
SVG_DIMENSIONS,
|
||||
TRANSLATE,
|
||||
formatNumbers,
|
||||
} from '../../utils/chart-helpers';
|
||||
|
||||
/**
|
||||
* @module VerticalBarChart
|
||||
* VerticalBarChart components are used to display stacked data in a vertical bar chart with accompanying tooltip
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <VerticalBarChart @dataset={dataset} @chartLegend={chartLegend} />
|
||||
* ```
|
||||
* @param {array} dataset - dataset for the chart, must be an array of flattened objects
|
||||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||
*/
|
||||
|
||||
export default class VerticalBarChart extends Component {
|
||||
@tracked tooltipTarget = '';
|
||||
@tracked tooltipTotal = '';
|
||||
@tracked uniqueEntities = '';
|
||||
@tracked nonEntityTokens = '';
|
||||
|
||||
get chartLegend() {
|
||||
return this.args.chartLegend;
|
||||
}
|
||||
|
||||
@action
|
||||
registerListener(element, args) {
|
||||
let dataset = args[0];
|
||||
// TODO pull out lines 44 - scales into helper? b/c same as line chart?
|
||||
let stackFunction = stack().keys(this.chartLegend.map((l) => l.key));
|
||||
let stackedData = stackFunction(dataset);
|
||||
let chartSvg = select(element);
|
||||
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
|
||||
|
||||
// DEFINE DATA BAR SCALES
|
||||
let yScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d.total))]) // TODO will need to recalculate when you get the data
|
||||
.range([0, 100])
|
||||
.nice();
|
||||
|
||||
let xScale = scaleBand()
|
||||
.domain(dataset.map((d) => d.month))
|
||||
.range([0, SVG_DIMENSIONS.width]) // set width to fix number of pixels
|
||||
.paddingInner(0.85);
|
||||
|
||||
let dataBars = chartSvg
|
||||
.selectAll('g')
|
||||
.data(stackedData)
|
||||
.enter()
|
||||
.append('g')
|
||||
.style('fill', (d, i) => LIGHT_AND_DARK_BLUE[i]);
|
||||
|
||||
dataBars
|
||||
.selectAll('rect')
|
||||
.data((stackedData) => stackedData)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('width', '7px')
|
||||
.attr('class', 'data-bar')
|
||||
.attr('height', (stackedData) => `${yScale(stackedData[1] - stackedData[0])}%`)
|
||||
.attr('x', ({ data }) => xScale(data.month)) // uses destructuring because was data.data.month
|
||||
.attr('y', (data) => `${100 - yScale(data[1])}%`); // subtract higher than 100% to give space for x axis ticks
|
||||
|
||||
// MAKE AXES //
|
||||
let yAxisScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d.total))]) // TODO will need to recalculate when you get the data
|
||||
.range([`${SVG_DIMENSIONS.height}`, 0])
|
||||
.nice();
|
||||
|
||||
let yAxis = axisLeft(yAxisScale)
|
||||
.ticks(7)
|
||||
.tickPadding(10)
|
||||
.tickSizeInner(-SVG_DIMENSIONS.width)
|
||||
.tickFormat(formatNumbers);
|
||||
|
||||
let xAxis = axisBottom(xScale).tickSize(0);
|
||||
|
||||
yAxis(chartSvg.append('g'));
|
||||
xAxis(chartSvg.append('g').attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`));
|
||||
|
||||
chartSvg.selectAll('.domain').remove(); // remove domain lines
|
||||
|
||||
// WIDER SELECTION AREA FOR TOOLTIP HOVER
|
||||
let greyBars = chartSvg
|
||||
.append('g')
|
||||
.attr('transform', `translate(${TRANSLATE.left})`)
|
||||
.style('fill', `${GREY}`)
|
||||
.style('opacity', '0')
|
||||
.style('mix-blend-mode', 'multiply');
|
||||
|
||||
let tooltipRect = greyBars
|
||||
.selectAll('rect')
|
||||
.data(dataset)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.style('cursor', 'pointer')
|
||||
.attr('class', 'tooltip-rect')
|
||||
.attr('height', '100%')
|
||||
.attr('width', '30px') // three times width
|
||||
.attr('y', '0') // start at bottom
|
||||
.attr('x', (data) => xScale(data.month)); // not data.data because this is not stacked data
|
||||
|
||||
// MOUSE EVENT FOR TOOLTIP
|
||||
tooltipRect.on('mouseover', (data) => {
|
||||
let hoveredMonth = data.month;
|
||||
this.tooltipTotal = `${data.total} total clients`;
|
||||
this.uniqueEntities = `${data.distinct_entities} unique entities`;
|
||||
this.nonEntityTokens = `${data.non_entity_tokens} non-entity tokens`;
|
||||
// let node = chartSvg
|
||||
// .selectAll('rect.tooltip-rect')
|
||||
// .filter(data => data.month === this.hoveredLabel)
|
||||
// .node();
|
||||
let node = chartSvg
|
||||
.selectAll('rect.data-bar')
|
||||
// filter for the top data bar (so y-coord !== 0) with matching month
|
||||
.filter((data) => data[0] !== 0 && data.data.month === hoveredMonth)
|
||||
.node();
|
||||
this.tooltipTarget = node; // grab the node from the list of rects
|
||||
});
|
||||
}
|
||||
|
||||
@action removeTooltip() {
|
||||
this.tooltipTarget = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
.bar-chart-wrapper {
|
||||
border: $light-border;
|
||||
border-radius: $radius-large;
|
||||
padding: $spacing-l $spacing-l $spacing-s $spacing-l;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
> div.is-border {
|
||||
border: 0.3px solid $ui-gray-200;
|
||||
margin-bottom: $spacing-xxs;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
|
||||
.header-left {
|
||||
.chart-title {
|
||||
font-size: $size-5;
|
||||
font-weight: $font-weight-bold;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.chart-description {
|
||||
font-size: $size-8;
|
||||
font-weight: $font-weight-normal;
|
||||
color: $ui-gray-700;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
text-align: right;
|
||||
|
||||
> button {
|
||||
font-size: $size-8;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-chart-container {
|
||||
padding: $spacing-m 0;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
.tick > text {
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: $size-8;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-container {
|
||||
height: $spacing-l;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.legend {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
@import './core/buttons';
|
||||
@import './core/footer';
|
||||
@import './core/forms';
|
||||
@import './core/charts';
|
||||
@import './core/helpers';
|
||||
@import './core/hero';
|
||||
@import './core/level';
|
||||
|
|
@ -45,7 +46,7 @@
|
|||
@import './components/auth-buttons';
|
||||
@import './components/auth-form';
|
||||
@import './components/b64-toggle';
|
||||
@import './components/bar-chart';
|
||||
|
||||
@import './components/box-label';
|
||||
@import './components/box-radio';
|
||||
@import './components/codemirror';
|
||||
|
|
|
|||
299
ui/app/styles/core/charts.scss
Normal file
299
ui/app/styles/core/charts.scss
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
.chart-wrapper {
|
||||
border: $light-border;
|
||||
border-radius: $radius-large;
|
||||
padding: $spacing-l $spacing-l $spacing-s $spacing-l;
|
||||
margin-bottom: $spacing-m;
|
||||
}
|
||||
|
||||
// GRID LAYOUT //
|
||||
.stacked-charts {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
// grid-template-columns: 1fr;
|
||||
// grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.single-chart-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 0.3fr 3.7fr;
|
||||
grid-template-rows: 0.5fr 1fr 1fr 1fr 0.25fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dual-chart-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-rows: 0.7fr 1fr 1fr 1fr 0.3fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: span col4-end;
|
||||
grid-row-start: 1;
|
||||
box-shadow: inset 0 -1px 0 $vault-gray-200;
|
||||
margin-bottom: $spacing-xl;
|
||||
}
|
||||
|
||||
.has-export {
|
||||
display: grid;
|
||||
grid-template-columns: 4fr 1fr;
|
||||
|
||||
.header-right {
|
||||
text-align: right;
|
||||
> button {
|
||||
font-size: $size-8;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container-wide {
|
||||
grid-column-start: 3;
|
||||
grid-column-end: 4;
|
||||
grid-row-start: 2;
|
||||
grid-row-end: span 3;
|
||||
justify-self: center;
|
||||
height: 341px;
|
||||
max-width: 730px;
|
||||
|
||||
svg.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container-left {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
grid-row-start: 2;
|
||||
grid-row-end: 5;
|
||||
padding-bottom: $spacing-xl;
|
||||
margin-bottom: $spacing-s;
|
||||
box-shadow: inset 0 -1px 0 $vault-gray-200;
|
||||
|
||||
> h2 {
|
||||
padding-left: 18px;
|
||||
}
|
||||
> p {
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container-right {
|
||||
grid-column-start: 4;
|
||||
grid-column-end: 8;
|
||||
grid-row-start: 2;
|
||||
grid-row-end: 5;
|
||||
padding-bottom: $spacing-xl;
|
||||
margin-bottom: $spacing-s;
|
||||
box-shadow: inset 0 -1px 0 $vault-gray-200;
|
||||
|
||||
> h2 {
|
||||
padding-left: 18px;
|
||||
}
|
||||
> p {
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-subTitle {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.data-details-top {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
grid-row-start: 3;
|
||||
}
|
||||
|
||||
.data-details-bottom {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
grid-row-start: 4;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 5;
|
||||
color: $ui-gray-500;
|
||||
font-size: $size-9;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.legend-center {
|
||||
grid-row-start: 5;
|
||||
grid-column-start: 3;
|
||||
grid-column-end: 5;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
font-size: $size-9;
|
||||
}
|
||||
|
||||
.legend-right {
|
||||
grid-row-start: 4;
|
||||
grid-column-start: 3;
|
||||
grid-column-end: 3;
|
||||
align-self: end;
|
||||
justify-self: center;
|
||||
font-size: $size-9;
|
||||
}
|
||||
|
||||
// FONT STYLES //
|
||||
|
||||
h2.chart-title {
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: $size-5;
|
||||
line-height: $spacing-l;
|
||||
}
|
||||
|
||||
p.chart-description {
|
||||
color: $ui-gray-700;
|
||||
font-size: $body-size;
|
||||
line-height: 18px;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
p.chart-subtext {
|
||||
color: $ui-gray-500;
|
||||
font-size: $size-9;
|
||||
line-height: $body-size;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
h3.data-details {
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: $size-9;
|
||||
line-height: $body-size;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
p.data-details {
|
||||
font-weight: $font-weight-normal;
|
||||
font-size: $size-4;
|
||||
}
|
||||
|
||||
// MISC STYLES
|
||||
|
||||
.light-dot {
|
||||
background-color: #bfd4ff;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dark-dot {
|
||||
background-color: #1563ff;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
padding-left: $spacing-xs;
|
||||
padding-right: $spacing-xl;
|
||||
}
|
||||
|
||||
.chart-tooltip {
|
||||
background-color: $ui-gray-700;
|
||||
color: white;
|
||||
font-size: $size-9;
|
||||
padding: 6px;
|
||||
border-radius: $radius-large;
|
||||
|
||||
.bold {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
.line-chart {
|
||||
width: 117px;
|
||||
}
|
||||
|
||||
.vertical-chart {
|
||||
text-align: center;
|
||||
flex-wrap: nowrap;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.horizontal-chart {
|
||||
width: 200px;
|
||||
padding: $spacing-s;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-tooltip-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 9px solid $ui-gray-700;
|
||||
position: absolute;
|
||||
opacity: 0.8;
|
||||
bottom: -9px;
|
||||
left: calc(50% - 5px);
|
||||
}
|
||||
|
||||
.has-grid {
|
||||
g > text {
|
||||
color: $ui-gray-500;
|
||||
font-size: $size-9;
|
||||
}
|
||||
|
||||
g > line {
|
||||
// TODO: mix-blend doesn't work in firefox browser?
|
||||
mix-blend-mode: darken;
|
||||
color: $ui-gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.is-horizontal {
|
||||
.tick > text {
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: $size-9;
|
||||
}
|
||||
}
|
||||
|
||||
// RESPONSIVE STYLING //
|
||||
|
||||
@media only screen and (max-width: 950px) {
|
||||
.dual-chart-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: 0.2fr 0.75fr 0.75fr 0.2fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-container-left {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
grid-row-start: 2;
|
||||
grid-row-end: 3;
|
||||
margin-left: $spacing-xxl;
|
||||
margin-right: $spacing-xxl;
|
||||
}
|
||||
.chart-container-right {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
grid-row-start: 3;
|
||||
grid-row-end: 4;
|
||||
margin-left: $spacing-xxl;
|
||||
margin-right: $spacing-xxl;
|
||||
}
|
||||
|
||||
.legend-center {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 4;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 4;
|
||||
}
|
||||
}
|
||||
|
|
@ -28,14 +28,15 @@ $family-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto'
|
|||
$family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
$family-primary: $family-sans;
|
||||
$body-size: 14px;
|
||||
$size-3: (24/14) + 0rem; // ~1.714rem
|
||||
$size-5: 1.25rem;
|
||||
$size-7: (13/14) + 0rem; // ~.929rem
|
||||
$size-8: (12/14) + 0rem; // ~.857rem
|
||||
$size-9: 0.75rem;
|
||||
$size-10: 0.5rem;
|
||||
$size-11: 0.25rem;
|
||||
$console-spacing: 1.5rem;
|
||||
$size-3: (24/14) + 0rem; // ~1.714rem ~27px
|
||||
$size-4: 1.5rem; // 24px
|
||||
$size-5: 1.25rem; // 20px
|
||||
$size-7: (13/14) + 0rem; // ~.929rem ~15px
|
||||
$size-8: (12/14) + 0rem; // ~.857rem ~13.7px
|
||||
$size-9: 0.75rem; // 12px
|
||||
$size-10: 0.5rem; // 8px
|
||||
$size-11: 0.25rem; // 4px
|
||||
$console-spacing: 1.5rem; // 24px
|
||||
$size-small: $size-8;
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-semibold: 600;
|
||||
|
|
|
|||
34
ui/app/templates/components/clients/attribution.hbs
Normal file
34
ui/app/templates/components/clients/attribution.hbs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<div class="chart-wrapper dual-chart-grid">
|
||||
<div class="chart-header has-export">
|
||||
<div class="header-left">
|
||||
<h2 class="chart-title">{{@title}}</h2>
|
||||
<p class="chart-description">{{this.chartText.description}}</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
{{#if @totalClientsData}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container-left">
|
||||
<h2 class="chart-title">New clients</h2>
|
||||
<p class="chart-description">{{this.chartText.newCopy}}</p>
|
||||
<Clients::HorizontalBarChart @dataset={{@newClientsData}} @chartLegend={{@chartLegend}} />
|
||||
</div>
|
||||
|
||||
<div class="chart-container-right">
|
||||
<h2 class="chart-title">Total clients</h2>
|
||||
<p class="chart-description">{{this.chartText.totalCopy}}</p>
|
||||
<Clients::HorizontalBarChart @dataset={{@totalClientsData}} @chartLegend={{@chartLegend}} />
|
||||
</div>
|
||||
|
||||
<div class="timestamp">
|
||||
Updated Nov 15 2021, 4:07:32 pm
|
||||
</div>
|
||||
|
||||
<div class="legend-center">
|
||||
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
||||
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
||||
</div>
|
||||
</div>
|
||||
123
ui/app/templates/components/clients/dashboard.hbs
Normal file
123
ui/app/templates/components/clients/dashboard.hbs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
{{#if (and (eq @tab "history") (eq @model.config.queriesAvailable false))}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title="No monthly history"
|
||||
@message="There is no data in the monthly history yet. We collect it at the end of each month, so your data will be available on the first of next month."
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Data tracking is disabled"
|
||||
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<p>
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="config"}}>
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
</p>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
{{! ARG TODO change current to Dashboard }}
|
||||
{{#if (eq @tab "current")}}
|
||||
<p class="has-bottom-margin-s">
|
||||
{{! ARG TODO Add link for documentation "here" }}
|
||||
This dashboard will surface Vault client usage over time. Clients represent anything that has authenticated to or
|
||||
communicated with Vault. Documentation is available here.
|
||||
</p>
|
||||
{{#if (eq @model.config.enabled "Off")}}
|
||||
<EmptyState
|
||||
@title="Tracking is disabled"
|
||||
@message="Tracking is disabled and data is not being collected. To turn it on edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (eq @model.config.enabled "Off")}}
|
||||
<AlertBanner data-test-tracking-disabled @type="warning" @title="Tracking is disabled">
|
||||
Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need
|
||||
to
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
edit the configuration
|
||||
</LinkTo>
|
||||
to enable tracking again.
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
<h1 data-test-client-count-title class="title is-4 has-bottom-margin-s">
|
||||
Monthly history
|
||||
{{! ARG TODO change }}
|
||||
</h1>
|
||||
<p class="has-bottom-margin-s">
|
||||
This data is presented by full month. If there is data missing, it's possible that tracking was turned off at the
|
||||
time. Vault will only show data for contiguous blocks of time during which tracking was on.
|
||||
</p>
|
||||
{{! ARG TODO replace with calendar widget }}
|
||||
<PricingMetricsDates
|
||||
@queryStart={{@model.queryStart}}
|
||||
@queryEnd={{@model.queryEnd}}
|
||||
@resultStart={{@model.activity.startTime}}
|
||||
@resultEnd={{@model.activity.endTime}}
|
||||
@defaultSpan={{or @model.config.defaultReportMonths 12}}
|
||||
@retentionMonths={{@model.config.retentionMonths}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @isLoading}}
|
||||
<LayoutLoading />
|
||||
{{else}}
|
||||
{{#if this.hasClientData}}
|
||||
{{! ARG TODO end of part that goes to Running Client }}
|
||||
{{#if this.showGraphs}}
|
||||
{{! ARG TODO chart playground }}
|
||||
<Clients::RunningTotal
|
||||
@title="Vault client counts"
|
||||
@description="An active client is any user or service that interacts with Vault. They are made up of unique entities and non-entity tokens. The total client count number is an important consideration for Vault billing."
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@lineChartData={{this.lineChartData}}
|
||||
@barChartData={{this.newMonthlyClients}}
|
||||
/>
|
||||
|
||||
<Clients::Attribution
|
||||
@title="Attribution"
|
||||
@newClientsData={{this.barChartDataset}}
|
||||
@totalClientsData={{this.barChartDataset}}
|
||||
@chartLegend={{this.chartLegend}}
|
||||
>
|
||||
<button type="button"> Export attribution data </button>
|
||||
</Clients::Attribution>
|
||||
|
||||
<Clients::MonthlyUsage
|
||||
@title="Vault usage"
|
||||
@description="This data can be used to understand how many total clients are using Vault each month for the time period selected above."
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@verticalBarChartData={{this.monthlyUsage}}
|
||||
@subTitle="Total monthly clients"
|
||||
@subText="Each unique client is counted once per month. This can help with capacity planning."
|
||||
@dataOne="Average total clients per month"
|
||||
@dataOneData="100"
|
||||
@dataTwo="Average new clients per month"
|
||||
@dataTwoData="4"
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{! ARG TODO change current to dashboard }}
|
||||
{{#if (eq @tab "current")}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title="No data received"
|
||||
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<EmptyState @title="No data received" @message="No data exists for that query period. Try searching again." />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
25
ui/app/templates/components/clients/horizontal-bar-chart.hbs
Normal file
25
ui/app/templates/components/clients/horizontal-bar-chart.hbs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<svg
|
||||
class="chart is-horizontal"
|
||||
{{on "mouseleave" this.removeTooltip}}
|
||||
{{did-insert this.renderChart @dataset}}
|
||||
{{did-update this.renderChart @dataset}}
|
||||
>
|
||||
</svg>
|
||||
|
||||
{{#if this.tooltipTarget}}
|
||||
{{! Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 }}
|
||||
{{! Component must be in curly bracket notation }}
|
||||
{{! template-lint-disable no-curly-component-invocation }}
|
||||
{{#modal-dialog
|
||||
tagName="div"
|
||||
tetherTarget=this.tooltipTarget
|
||||
targetAttachment="bottom middle"
|
||||
attachment="bottom middle"
|
||||
offset="35px 0"
|
||||
}}
|
||||
<div class="chart-tooltip horizontal-chart">
|
||||
<p>{{this.tooltipText}}</p>
|
||||
</div>
|
||||
<div class="chart-tooltip-arrow"></div>
|
||||
{{/modal-dialog}}
|
||||
{{/if}}
|
||||
|
After Width: | Height: | Size: 741 B |
24
ui/app/templates/components/clients/line-chart.hbs
Normal file
24
ui/app/templates/components/clients/line-chart.hbs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<svg class="chart has-grid" {{on "mouseleave" this.removeTooltip}} {{did-insert this.renderChart @dataset}}>
|
||||
</svg>
|
||||
|
||||
{{! TOOLTIP }}
|
||||
|
||||
{{#if this.tooltipTarget}}
|
||||
{{! Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 }}
|
||||
{{! Component must be in curly bracket notation }}
|
||||
{{! template-lint-disable no-curly-component-invocation }}
|
||||
{{#modal-dialog
|
||||
tagName="div"
|
||||
tetherTarget=this.tooltipTarget
|
||||
targetAttachment="bottom middle"
|
||||
attachment="bottom middle"
|
||||
offset="35px 0"
|
||||
}}
|
||||
<div class="chart-tooltip line-chart">
|
||||
<p class="bold">{{this.tooltipMonth}}</p>
|
||||
<p>{{this.tooltipTotal}}</p>
|
||||
<p>{{this.tooltipNew}}</p>
|
||||
</div>
|
||||
<div class="chart-tooltip-arrow"></div>
|
||||
{{/modal-dialog}}
|
||||
{{/if}}
|
||||
|
After Width: | Height: | Size: 778 B |
37
ui/app/templates/components/clients/monthly-usage.hbs
Normal file
37
ui/app/templates/components/clients/monthly-usage.hbs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<div class="chart-wrapper single-chart-grid">
|
||||
<div class="chart-header">
|
||||
<h2 class="chart-title">{{@title}}</h2>
|
||||
{{#if @description}}
|
||||
<p class="chart-description">{{@description}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="chart-container-wide">
|
||||
<Clients::VerticalBarChart @dataset={{@verticalBarChartData}} @chartLegend={{@chartLegend}} />
|
||||
</div>
|
||||
|
||||
<div class="chart-subTitle">
|
||||
<h2 class="chart-title">{{@subTitle}}</h2>
|
||||
<p class="chart-subtext">{{@subText}}</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-top">
|
||||
<h3 class="data-details">{{@dataOne}}</h3>
|
||||
<p class="data-details">{{@dataOneData}}</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-bottom">
|
||||
<h3 class="data-details">{{@dataTwo}}</h3>
|
||||
<p class="data-details">{{@dataTwoData}}</p>
|
||||
</div>
|
||||
|
||||
<div class="timestamp">
|
||||
Updated Nov 15 2021, 4:07:32 pm
|
||||
</div>
|
||||
|
||||
<div class="legend-right">
|
||||
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
||||
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
64
ui/app/templates/components/clients/running-total.hbs
Normal file
64
ui/app/templates/components/clients/running-total.hbs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<div class="chart-wrapper stacked-charts">
|
||||
<div class="single-chart-grid">
|
||||
<div class="chart-header">
|
||||
<h2 class="chart-title">{{@title}}</h2>
|
||||
<p class="chart-description">{{@description}}</p>
|
||||
</div>
|
||||
|
||||
<div class="chart-container-wide">
|
||||
<Clients::LineChart @dataset={{@lineChartData}} />
|
||||
</div>
|
||||
|
||||
<div class="chart-subTitle">
|
||||
<h2 class="chart-title">Running client total</h2>
|
||||
<p class="chart-subtext">The number of clients which interacted with Vault during this date range. </p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-top">
|
||||
<h3 class="data-details">{{capitalize @chartLegend.0.label}}</h3>
|
||||
<p class="data-details">1,307</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-bottom">
|
||||
<h3 class="data-details">{{capitalize @chartLegend.1.label}}</h3>
|
||||
<p class="data-details">8,005</p>
|
||||
</div>
|
||||
|
||||
<div class="legend-right">
|
||||
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
||||
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="single-chart-grid">
|
||||
<div class="chart-container-wide">
|
||||
<Clients::VerticalBarChart @dataset={{@barChartData}} @chartLegend={{@chartLegend}} />
|
||||
</div>
|
||||
|
||||
<div class="chart-subTitle">
|
||||
<h2 class="chart-title">New monthly clients</h2>
|
||||
<p class="chart-subtext">
|
||||
Clients which interacted with Vault for the first time during this date range, displayed per month.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-top">
|
||||
<h3 class="data-details">Average new {{@chartLegend.0.label}} per month</h3>
|
||||
<p class="data-details">{{@dataOneData}}</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-bottom">
|
||||
<h3 class="data-details">Average new {{@chartLegend.1.label}} per month</h3>
|
||||
<p class="data-details">{{@dataTwoData}}</p>
|
||||
</div>
|
||||
|
||||
<div class="timestamp">
|
||||
Updated Nov 15 2021, 4:07:32 pm
|
||||
</div>
|
||||
|
||||
<div class="legend-right">
|
||||
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
||||
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
24
ui/app/templates/components/clients/vertical-bar-chart.hbs
Normal file
24
ui/app/templates/components/clients/vertical-bar-chart.hbs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<svg class="chart has-grid" {{on "mouseleave" this.removeTooltip}} {{did-insert this.registerListener @dataset}}>
|
||||
</svg>
|
||||
|
||||
{{! TOOLTIP }}
|
||||
|
||||
{{#if this.tooltipTarget}}
|
||||
{{! Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 }}
|
||||
{{! Component must be in curly bracket notation }}
|
||||
{{! template-lint-disable no-curly-component-invocation }}
|
||||
{{#modal-dialog
|
||||
tagName="div"
|
||||
tetherTarget=this.tooltipTarget
|
||||
targetAttachment="bottom middle"
|
||||
attachment="bottom middle"
|
||||
offset="40px 0"
|
||||
}}
|
||||
<div class="chart-tooltip vertical-chart">
|
||||
<p>{{this.tooltipTotal}}</p>
|
||||
<p>{{this.uniqueEntities}}</p>
|
||||
<p>{{this.nonEntityTokens}}</p>
|
||||
</div>
|
||||
<div class="chart-tooltip-arrow"></div>
|
||||
{{/modal-dialog}}
|
||||
{{/if}}
|
||||
|
After Width: | Height: | Size: 781 B |
|
|
@ -7,6 +7,7 @@
|
|||
</PageHeader>
|
||||
|
||||
<div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless" data-test-pricing-metrics>
|
||||
{{! template-lint-configure no-unknown-arguments-for-builtin-components "warn" }}
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{{! template-lint-configure no-unknown-arguments-for-builtin-components "warn" }}
|
||||
|
|
@ -62,5 +63,6 @@
|
|||
</Toolbar>
|
||||
<Clients::Config @model={{this.model.config}} @isLoading={{this.currentlyLoading}} />
|
||||
{{else}}
|
||||
{{!-- <Clients::Dashboard @tab={{this.tab}} @model={{this.model}} @isLoading={{this.currentlyLoading}} /> --}}
|
||||
<Clients::History @tab={{this.tab}} @model={{this.model}} @isLoading={{this.currentlyLoading}} />
|
||||
{{/if}}
|
||||
16
ui/app/utils/chart-helpers.js
Normal file
16
ui/app/utils/chart-helpers.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { format } from 'd3-format';
|
||||
|
||||
// COLOR THEME:
|
||||
export const LIGHT_AND_DARK_BLUE = ['#BFD4FF', '#1563FF'];
|
||||
export const BAR_COLOR_HOVER = ['#1563FF', '#0F4FD1'];
|
||||
export const GREY = '#EBEEF2';
|
||||
|
||||
// TRANSLATIONS:
|
||||
export const TRANSLATE = { left: -11 };
|
||||
export const SVG_DIMENSIONS = { height: 190, width: 500 };
|
||||
|
||||
// Reference for tickFormat https://www.youtube.com/watch?v=c3MCROTNN8g
|
||||
export function formatNumbers(number) {
|
||||
// replace SI prefix of 'G' for billions to 'B'
|
||||
return format('.1s')(number).replace('G', 'B');
|
||||
}
|
||||
|
|
@ -47,11 +47,10 @@ const CHAR_LIMIT = 18; // character count limit for y-axis labels to trigger tru
|
|||
const LINE_HEIGHT = 24; // each bar w/ padding is 24 pixels thick
|
||||
|
||||
// COLOR THEME:
|
||||
const BAR_COLOR_DEFAULT = ['#BFD4FF', '#8AB1FF'];
|
||||
const LIGHT_AND_DARK_BLUE = ['#BFD4FF', '#8AB1FF'];
|
||||
const BAR_COLOR_HOVER = ['#1563FF', '#0F4FD1'];
|
||||
const BACKGROUND_BAR_COLOR = '#EBEEF2';
|
||||
const TOOLTIP_BACKGROUND = '#525761';
|
||||
|
||||
const GREY = '#EBEEF2';
|
||||
class BarChartComponent extends Component {
|
||||
get labelKey() {
|
||||
return this.args.labelKey || 'label';
|
||||
|
|
@ -121,7 +120,7 @@ class BarChartComponent extends Component {
|
|||
.append('g')
|
||||
// shifts chart to accommodate y-axis legend
|
||||
.attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top})`)
|
||||
.style('fill', (d, i) => BAR_COLOR_DEFAULT[i]);
|
||||
.style('fill', (d, i) => LIGHT_AND_DARK_BLUE[i]);
|
||||
|
||||
let yAxis = axisLeft(yScale).tickSize(0);
|
||||
yAxis(chartSvg.append('g').attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top})`));
|
||||
|
|
@ -159,7 +158,7 @@ class BarChartComponent extends Component {
|
|||
.attr('height', `${LINE_HEIGHT}px`)
|
||||
.attr('x', '0')
|
||||
.attr('y', (chartData) => yScale(chartData[labelKey]))
|
||||
.style('fill', `${BACKGROUND_BAR_COLOR}`)
|
||||
.style('fill', `${GREY}`)
|
||||
.style('opacity', '0')
|
||||
.style('mix-blend-mode', 'multiply');
|
||||
|
||||
|
|
@ -205,7 +204,7 @@ class BarChartComponent extends Component {
|
|||
.filter(function () {
|
||||
return compareAttributes(this, event.target, 'y');
|
||||
})
|
||||
.style('fill', (b, i) => `${BAR_COLOR_DEFAULT[i]}`);
|
||||
.style('fill', (b, i) => `${LIGHT_AND_DARK_BLUE[i]}`);
|
||||
})
|
||||
.on('mousemove', function (chartData) {
|
||||
select('.chart-tooltip')
|
||||
|
|
@ -248,7 +247,7 @@ class BarChartComponent extends Component {
|
|||
.filter(function () {
|
||||
return compareAttributes(this, event.target, 'y');
|
||||
})
|
||||
.style('fill', (b, i) => `${BAR_COLOR_DEFAULT[i]}`);
|
||||
.style('fill', (b, i) => `${LIGHT_AND_DARK_BLUE[i]}`);
|
||||
actionBarSelection
|
||||
.filter(function () {
|
||||
return compareAttributes(this, event.target, 'y');
|
||||
|
|
@ -296,7 +295,7 @@ class BarChartComponent extends Component {
|
|||
.attr('cx', `${xCoordinate}%`)
|
||||
.attr('cy', '50%')
|
||||
.attr('r', 6)
|
||||
.style('fill', `${BAR_COLOR_DEFAULT[i]}`);
|
||||
.style('fill', `${LIGHT_AND_DARK_BLUE[i]}`);
|
||||
legendSvg
|
||||
.append('text')
|
||||
.attr('x', `${xCoordinate + 2}%`)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default function () {
|
|||
};
|
||||
});
|
||||
|
||||
this.get('/sys/internal/ui/feature-flags', (db) => {
|
||||
this.get('/sys/internal/ui/feature-flags', function (db) {
|
||||
const featuresResponse = db.features.first();
|
||||
return {
|
||||
data: {
|
||||
|
|
@ -31,6 +31,284 @@ export default function () {
|
|||
};
|
||||
});
|
||||
|
||||
this.get('/sys/internal/counters/activity', function () {
|
||||
return {
|
||||
data: {
|
||||
start_time: '2019-11-01T00:00:00Z',
|
||||
end_time: '2020-10-31T23:59:59Z',
|
||||
total: {
|
||||
distinct_entities: 200,
|
||||
non_entity_tokens: 100,
|
||||
clients: 300,
|
||||
},
|
||||
by_namespace: [
|
||||
{
|
||||
_comment: 'by_namespace will remain as it is',
|
||||
},
|
||||
],
|
||||
months: [
|
||||
{
|
||||
'jan/2022': {
|
||||
counts: {
|
||||
distinct_entities: 100,
|
||||
non_entity_tokens: 50,
|
||||
clients: 150,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
id: 'root',
|
||||
path: '',
|
||||
counts: {
|
||||
distinct_entities: 50,
|
||||
non_entity_tokens: 25,
|
||||
clients: 75,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 25,
|
||||
non_entity_tokens: 12,
|
||||
clients: 37,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'auth/approle/login',
|
||||
counts: {
|
||||
distinct_entities: 25,
|
||||
non_entity_tokens: 13,
|
||||
clients: 38,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 'ns1',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 50,
|
||||
non_entity_tokens: 25,
|
||||
clients: 75,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 20,
|
||||
non_entity_tokens: 10,
|
||||
clients: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/approle/login',
|
||||
counts: {
|
||||
distinct_entities: 30,
|
||||
non_entity_tokens: 15,
|
||||
clients: 45,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
new: {
|
||||
counts: {
|
||||
distinct_entities: 30,
|
||||
non_entity_tokens: 10,
|
||||
clients: 40,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 15,
|
||||
non_entity_tokens: 5,
|
||||
clients: 20,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
non_entity_tokens: 2,
|
||||
clients: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 10,
|
||||
non_entity_tokens: 3,
|
||||
clients: 13,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 'ns1',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 15,
|
||||
non_entity_tokens: 5,
|
||||
clients: 20,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 10,
|
||||
non_entity_tokens: 1,
|
||||
clients: 11,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
non_entity_tokens: 4,
|
||||
clients: 9,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
'feb/2022': {
|
||||
counts: {
|
||||
_comment: 'total monthly clients',
|
||||
distinct_entities: 100,
|
||||
non_entity_tokens: 50,
|
||||
clients: 150,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 60,
|
||||
non_entity_tokens: 10,
|
||||
clients: 70,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 30,
|
||||
non_entity_tokens: 5,
|
||||
clients: 35,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/approle/login',
|
||||
counts: {
|
||||
distinct_entities: 30,
|
||||
non_entity_tokens: 5,
|
||||
clients: 35,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 'ns1',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 40,
|
||||
non_entity_tokens: 40,
|
||||
clients: 80,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 20,
|
||||
non_entity_tokens: 20,
|
||||
clients: 40,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/approle/login',
|
||||
counts: {
|
||||
distinct_entities: 20,
|
||||
non_entity_tokens: 20,
|
||||
clients: 40,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
new: {
|
||||
counts: {
|
||||
distinct_entities: 20,
|
||||
non_entity_tokens: 5,
|
||||
clients: 25,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 10,
|
||||
non_entity_tokens: 3,
|
||||
clients: 13,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
non_entity_tokens: 1,
|
||||
clients: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
non_entity_tokens: 2,
|
||||
clients: 7,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 'ns1',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 10,
|
||||
non_entity_tokens: 2,
|
||||
clients: 12,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
non_entity_tokens: 2,
|
||||
clients: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/aws/login',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
non_entity_tokens: 0,
|
||||
clients: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.get('/sys/internal/counters/activity/monthly', function () {
|
||||
return {
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@
|
|||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-maybe-import-regenerator": "^0.1.6",
|
||||
"ember-maybe-in-element": "^2.0.3",
|
||||
"ember-modal-dialog": "^3.0.3",
|
||||
"ember-page-title": "^6.0.3",
|
||||
"ember-power-select-with-create": "0.9.0",
|
||||
"ember-promise-helpers": "^1.0.9",
|
||||
|
|
@ -138,6 +139,7 @@
|
|||
"ember-svg-jar": "^2.1.0",
|
||||
"ember-template-lint": "^3.14.0",
|
||||
"ember-test-selectors": "^2.1.0",
|
||||
"ember-tether": "^2.0.1",
|
||||
"ember-truth-helpers": "^2.1.0",
|
||||
"ember-wormhole": "^0.5.5",
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
|
|
|
|||
32
ui/yarn.lock
32
ui/yarn.lock
|
|
@ -10512,6 +10512,13 @@ ember-get-config@^0.2.2:
|
|||
broccoli-file-creator "^1.1.1"
|
||||
ember-cli-babel "^6.3.0"
|
||||
|
||||
ember-ignore-children-helper@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-ignore-children-helper/-/ember-ignore-children-helper-1.0.1.tgz#f7c4aa17afb9c5685e1d4dcdb61c7b138ca7cdc3"
|
||||
integrity sha512-AgKkrvd1/hIBWdLn42gITlweVsALUGPYF9fMpQ2IDqp7QnRmtO8ocRbZEmMddPDWY9Xu7W5qO2f35rbD7OSpYw==
|
||||
dependencies:
|
||||
ember-cli-babel "^6.8.2"
|
||||
|
||||
ember-in-element-polyfill@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-in-element-polyfill/-/ember-in-element-polyfill-1.0.1.tgz#143504445bb4301656a2eaad42644d684f5164dd"
|
||||
|
|
@ -10564,6 +10571,17 @@ ember-maybe-in-element@^2.0.2, ember-maybe-in-element@^2.0.3:
|
|||
ember-cli-version-checker "^5.1.1"
|
||||
ember-in-element-polyfill "^1.0.1"
|
||||
|
||||
ember-modal-dialog@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/ember-modal-dialog/-/ember-modal-dialog-3.0.3.tgz#4dd1c699fef02953f21e2b2b290cca380a6317dd"
|
||||
integrity sha512-a3WHPIZMbnM4GDZdyNseClUIKWcI6AZuhXdNpw0XbfKzpocW2vAqhwLmQ17Fo+W5mCwQXU131DAROFWeOP68xQ==
|
||||
dependencies:
|
||||
ember-cli-babel "^7.23.0"
|
||||
ember-cli-htmlbars "^3.0.0"
|
||||
ember-cli-version-checker "^2.1.0"
|
||||
ember-ignore-children-helper "^1.0.1"
|
||||
ember-wormhole "^0.5.5"
|
||||
|
||||
ember-modifier-manager-polyfill@^1.1.0, ember-modifier-manager-polyfill@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-modifier-manager-polyfill/-/ember-modifier-manager-polyfill-1.2.0.tgz#cf4444e11a42ac84f5c8badd85e635df57565dda"
|
||||
|
|
@ -10850,6 +10868,15 @@ ember-test-selectors@^2.1.0:
|
|||
ember-cli-babel "^6.8.2"
|
||||
ember-cli-version-checker "^3.1.2"
|
||||
|
||||
ember-tether@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-tether/-/ember-tether-2.0.1.tgz#29cab1463c43ebc22c836887b25d9b5106231366"
|
||||
integrity sha512-ECjmFVqo58YK6SmrBW77hmp1wE76n9azl3H6oMvYOXLA4FraOB4xhuk/VCV/XPbG0p1bArKepGjxM8Qbu8g0Lw==
|
||||
dependencies:
|
||||
ember-auto-import "^1.2.19"
|
||||
ember-cli-babel "^7.1.2"
|
||||
tether "^1.4.0"
|
||||
|
||||
ember-text-measurer@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-text-measurer/-/ember-text-measurer-0.6.0.tgz#140eda044fd7d4d7f60f654dd30da79c06922b2e"
|
||||
|
|
@ -20163,6 +20190,11 @@ testem@^3.2.0:
|
|||
tmp "0.0.33"
|
||||
xmldom "^0.6.0"
|
||||
|
||||
tether@^1.4.0:
|
||||
version "1.4.7"
|
||||
resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.7.tgz#d56a818590d8fe72e387f77a67f93ab96d8e1fb2"
|
||||
integrity sha512-Z0J1aExjoFU8pybVkQAo/vD2wfSO63r+XOPfWQMC5qtf1bI7IWqNk4MiyBcgvvnY8kqnY06dVdvwTK2S3PU/Fw==
|
||||
|
||||
text-encoder-lite@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/text-encoder-lite/-/text-encoder-lite-2.0.0.tgz#3c865dd6f3720b279c9e370f8f36c831d2cee175"
|
||||
|
|
|
|||
Loading…
Reference in a new issue