From 7b3b455a7bd703c1a47c2c3f40bed0ef229f70f5 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 6 Jan 2022 13:59:57 -0700 Subject: [PATCH] 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 * 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 * 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 * 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 * Add incorrect policy issue to the docs * Add example about entity-alias restriction Co-authored-by: Meggie * 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 * 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 * Add missing RSA key sizes to pki/backend_test.go Signed-off-by: Alexander Scheel * 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 * Add changelog entry Signed-off-by: Alexander Scheel * 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 * 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 Co-authored-by: Nick Cabatoff Co-authored-by: Harsimran Singh Maan Co-authored-by: Steven Clark Co-authored-by: Loann Le <84412881+taoism4504@users.noreply.github.com> Co-authored-by: Jordan Reimer 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 Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com> Co-authored-by: robison jacka Co-authored-by: Theron Voran Co-authored-by: Pratyoy Mukhopadhyay <35388175+pmmukh@users.noreply.github.com> Co-authored-by: Meggie Co-authored-by: hghaf099 <83242695+hghaf099@users.noreply.github.com> Co-authored-by: John-Michael Faircloth Co-authored-by: Brandon Romano 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 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 Co-authored-by: Victor Rodriguez * 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 Co-authored-by: Jim Kalafut Co-authored-by: Nick Cabatoff Co-authored-by: Harsimran Singh Maan Co-authored-by: Steven Clark Co-authored-by: Loann Le <84412881+taoism4504@users.noreply.github.com> Co-authored-by: Jordan Reimer 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 Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com> Co-authored-by: robison jacka Co-authored-by: Theron Voran Co-authored-by: Pratyoy Mukhopadhyay <35388175+pmmukh@users.noreply.github.com> Co-authored-by: Meggie Co-authored-by: hghaf099 <83242695+hghaf099@users.noreply.github.com> Co-authored-by: John-Michael Faircloth Co-authored-by: Brandon Romano 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 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 Co-authored-by: Victor Rodriguez --- ui/app/components/clients/attribution.js | 45 +++ ui/app/components/clients/dashboard.js | 182 +++++++++++ .../clients/horizontal-bar-chart.js | 225 +++++++++++++ ui/app/components/clients/line-chart.js | 121 +++++++ .../components/clients/vertical-bar-chart.js | 139 ++++++++ ui/app/styles/components/bar-chart.scss | 65 ---- ui/app/styles/core.scss | 3 +- ui/app/styles/core/charts.scss | 299 ++++++++++++++++++ ui/app/styles/utils/_bulma_variables.scss | 17 +- .../components/clients/attribution.hbs | 34 ++ .../components/clients/dashboard.hbs | 123 +++++++ .../clients/horizontal-bar-chart.hbs | 25 ++ .../components/clients/line-chart.hbs | 24 ++ .../components/clients/monthly-usage.hbs | 37 +++ .../components/clients/running-total.hbs | 64 ++++ .../components/clients/vertical-bar-chart.hbs | 24 ++ .../templates/vault/cluster/clients/index.hbs | 2 + ui/app/utils/chart-helpers.js | 16 + ui/lib/core/addon/components/bar-chart.js | 15 +- ui/mirage/config.js | 280 +++++++++++++++- ui/package.json | 2 + ui/yarn.lock | 32 ++ 22 files changed, 1691 insertions(+), 83 deletions(-) create mode 100644 ui/app/components/clients/attribution.js create mode 100644 ui/app/components/clients/dashboard.js create mode 100644 ui/app/components/clients/horizontal-bar-chart.js create mode 100644 ui/app/components/clients/line-chart.js create mode 100644 ui/app/components/clients/vertical-bar-chart.js delete mode 100644 ui/app/styles/components/bar-chart.scss create mode 100644 ui/app/styles/core/charts.scss create mode 100644 ui/app/templates/components/clients/attribution.hbs create mode 100644 ui/app/templates/components/clients/dashboard.hbs create mode 100644 ui/app/templates/components/clients/horizontal-bar-chart.hbs create mode 100644 ui/app/templates/components/clients/line-chart.hbs create mode 100644 ui/app/templates/components/clients/monthly-usage.hbs create mode 100644 ui/app/templates/components/clients/running-total.hbs create mode 100644 ui/app/templates/components/clients/vertical-bar-chart.hbs create mode 100644 ui/app/utils/chart-helpers.js diff --git a/ui/app/components/clients/attribution.js b/ui/app/components/clients/attribution.js new file mode 100644 index 0000000000..7df0575cad --- /dev/null +++ b/ui/app/components/clients/attribution.js @@ -0,0 +1,45 @@ +import Component from '@glimmer/component'; + +// TODO: fill out below!! +/** + * @module Attribution + * Attribution components are used to... + * + * @example + * ```js + * + * Pass in export button + * + * ``` + * @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. ` + // } + } +} diff --git a/ui/app/components/clients/dashboard.js b/ui/app/components/clients/dashboard.js new file mode 100644 index 0000000000..8d464f32fa --- /dev/null +++ b/ui/app/components/clients/dashboard.js @@ -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; + } +} diff --git a/ui/app/components/clients/horizontal-bar-chart.js b/ui/app/components/clients/horizontal-bar-chart.js new file mode 100644 index 0000000000..125caccf4a --- /dev/null +++ b/ui/app/components/clients/horizontal-bar-chart.js @@ -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 + * + * ``` + * @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)); + } +} diff --git a/ui/app/components/clients/line-chart.js b/ui/app/components/clients/line-chart.js new file mode 100644 index 0000000000..78a703efa0 --- /dev/null +++ b/ui/app/components/clients/line-chart.js @@ -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 + * + * ``` + * @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; + }); + } +} diff --git a/ui/app/components/clients/vertical-bar-chart.js b/ui/app/components/clients/vertical-bar-chart.js new file mode 100644 index 0000000000..8d5aa45c06 --- /dev/null +++ b/ui/app/components/clients/vertical-bar-chart.js @@ -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 + * + * ``` + * @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; + } +} diff --git a/ui/app/styles/components/bar-chart.scss b/ui/app/styles/components/bar-chart.scss deleted file mode 100644 index 1b9484ee23..0000000000 --- a/ui/app/styles/components/bar-chart.scss +++ /dev/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%; -} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index 7e07524dfe..7d91d8efd9 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -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'; diff --git a/ui/app/styles/core/charts.scss b/ui/app/styles/core/charts.scss new file mode 100644 index 0000000000..4de34306cb --- /dev/null +++ b/ui/app/styles/core/charts.scss @@ -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; + } +} diff --git a/ui/app/styles/utils/_bulma_variables.scss b/ui/app/styles/utils/_bulma_variables.scss index ff5aa2ca50..930c645739 100644 --- a/ui/app/styles/utils/_bulma_variables.scss +++ b/ui/app/styles/utils/_bulma_variables.scss @@ -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; diff --git a/ui/app/templates/components/clients/attribution.hbs b/ui/app/templates/components/clients/attribution.hbs new file mode 100644 index 0000000000..3bb5040c5d --- /dev/null +++ b/ui/app/templates/components/clients/attribution.hbs @@ -0,0 +1,34 @@ +
+
+
+

{{@title}}

+

{{this.chartText.description}}

+
+
+ {{#if @totalClientsData}} + {{yield}} + {{/if}} +
+
+ +
+

New clients

+

{{this.chartText.newCopy}}

+ +
+ +
+

Total clients

+

{{this.chartText.totalCopy}}

+ +
+ +
+ Updated Nov 15 2021, 4:07:32 pm +
+ +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+
\ No newline at end of file diff --git a/ui/app/templates/components/clients/dashboard.hbs b/ui/app/templates/components/clients/dashboard.hbs new file mode 100644 index 0000000000..bc349a4e56 --- /dev/null +++ b/ui/app/templates/components/clients/dashboard.hbs @@ -0,0 +1,123 @@ +{{#if (and (eq @tab "history") (eq @model.config.queriesAvailable false))}} + {{#if (eq @model.config.enabled "On")}} + + {{else}} + + {{#if @model.config.configPath.canUpdate}} +

+ + Go to configuration + +

+ {{/if}} +
+ {{/if}} +{{else}} +
+ {{! ARG TODO change current to Dashboard }} + {{#if (eq @tab "current")}} +

+ {{! 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. +

+ {{#if (eq @model.config.enabled "Off")}} + + {{#if @model.config.configPath.canUpdate}} + + Go to configuration + + {{/if}} + + {{/if}} + {{else}} + {{#if (eq @model.config.enabled "Off")}} + + Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need + to + + edit the configuration + + to enable tracking again. + + {{/if}} +

+ Monthly history + {{! ARG TODO change }} +

+

+ 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. +

+ {{! ARG TODO replace with calendar widget }} + + {{/if}} + {{#if @isLoading}} + + {{else}} + {{#if this.hasClientData}} + {{! ARG TODO end of part that goes to Running Client }} + {{#if this.showGraphs}} + {{! ARG TODO chart playground }} + + + + + + + + {{/if}} + {{else}} + {{! ARG TODO change current to dashboard }} + {{#if (eq @tab "current")}} + {{#if (eq @model.config.enabled "On")}} + + {{/if}} + {{else}} + + {{/if}} + {{/if}} + {{/if}} +
+{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/clients/horizontal-bar-chart.hbs b/ui/app/templates/components/clients/horizontal-bar-chart.hbs new file mode 100644 index 0000000000..7d86a85327 --- /dev/null +++ b/ui/app/templates/components/clients/horizontal-bar-chart.hbs @@ -0,0 +1,25 @@ + + + +{{#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" + }} +
+

{{this.tooltipText}}

+
+
+ {{/modal-dialog}} +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/clients/line-chart.hbs b/ui/app/templates/components/clients/line-chart.hbs new file mode 100644 index 0000000000..75973949db --- /dev/null +++ b/ui/app/templates/components/clients/line-chart.hbs @@ -0,0 +1,24 @@ + + + +{{! 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" + }} +
+

{{this.tooltipMonth}}

+

{{this.tooltipTotal}}

+

{{this.tooltipNew}}

+
+
+ {{/modal-dialog}} +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/clients/monthly-usage.hbs b/ui/app/templates/components/clients/monthly-usage.hbs new file mode 100644 index 0000000000..6ae22a513c --- /dev/null +++ b/ui/app/templates/components/clients/monthly-usage.hbs @@ -0,0 +1,37 @@ +
+
+

{{@title}}

+ {{#if @description}} +

{{@description}}

+ {{/if}} +
+ +
+ +
+ +
+

{{@subTitle}}

+

{{@subText}}

+
+ +
+

{{@dataOne}}

+

{{@dataOneData}}

+
+ +
+

{{@dataTwo}}

+

{{@dataTwoData}}

+
+ +
+ Updated Nov 15 2021, 4:07:32 pm +
+ +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+ +
\ No newline at end of file diff --git a/ui/app/templates/components/clients/running-total.hbs b/ui/app/templates/components/clients/running-total.hbs new file mode 100644 index 0000000000..c41814e1bb --- /dev/null +++ b/ui/app/templates/components/clients/running-total.hbs @@ -0,0 +1,64 @@ +
+
+
+

{{@title}}

+

{{@description}}

+
+ +
+ +
+ +
+

Running client total

+

The number of clients which interacted with Vault during this date range.

+
+ +
+

{{capitalize @chartLegend.0.label}}

+

1,307

+
+ +
+

{{capitalize @chartLegend.1.label}}

+

8,005

+
+ +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+
+ +
+
+ +
+ +
+

New monthly clients

+

+ Clients which interacted with Vault for the first time during this date range, displayed per month. +

+
+ +
+

Average new {{@chartLegend.0.label}} per month

+

{{@dataOneData}}

+
+ +
+

Average new {{@chartLegend.1.label}} per month

+

{{@dataTwoData}}

+
+ +
+ Updated Nov 15 2021, 4:07:32 pm +
+ +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+
+
\ No newline at end of file diff --git a/ui/app/templates/components/clients/vertical-bar-chart.hbs b/ui/app/templates/components/clients/vertical-bar-chart.hbs new file mode 100644 index 0000000000..b110479452 --- /dev/null +++ b/ui/app/templates/components/clients/vertical-bar-chart.hbs @@ -0,0 +1,24 @@ + + + +{{! 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" + }} +
+

{{this.tooltipTotal}}

+

{{this.uniqueEntities}}

+

{{this.nonEntityTokens}}

+
+
+ {{/modal-dialog}} +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/clients/index.hbs b/ui/app/templates/vault/cluster/clients/index.hbs index 930fbf0a14..62ef43cae8 100644 --- a/ui/app/templates/vault/cluster/clients/index.hbs +++ b/ui/app/templates/vault/cluster/clients/index.hbs @@ -7,6 +7,7 @@
+ {{! template-lint-configure no-unknown-arguments-for-builtin-components "warn" }}