From c24c70af468cff2630a19cf6b394f552ec655c39 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Tue, 15 Dec 2020 15:34:54 +0000 Subject: [PATCH] ui: Dev/Test environment configurable metrics (#9345) In order to test certain setups for our metrics visualizations we need to be able to setup several different `ui_config` settings during development/testing. Generally in the UI, we use the Web Inspector to set various cookie values to configure the UI how we need to see it whilst developing, so this PR: 1. Routes `ui_config` through a dev time only `CONSUL_UI_CONFIG` env variable so we can change it via cookies vars. 2. Adds `CONSUL_METRICS_PROXY_ENABLE`, `CONSUL_METRICS_PROVIDER` and `CONSUL_SERVICE_DASHBOARD_URL` so it's easy to set/unset these only values during development. 3. Adds an acceptance testing step so we can setup `ui_config` to whatever we want during testing. 4. Adds an async 'repository-like' method to the `UiConfig` Service so it feels like a repository - incase we ever need to get this via an HTTP API+blocking query. 5. Vaguely unrelated: we allow cookie values to be set via the location.hash whilst in development only e.g. `/ui/services#CONSUL_METRICS_PROXY_ENABLE=1` so we can link to different setups if we ever need to. All values added here are empty/falsey by default, so in order to see how it was previously you'll need to set the appropriate cookies values, but you can now also easily preview/test the the metrics viz in different/disabled states (with differing `ui_config`) --- .../app/services/repository/metrics.js | 35 +++++++------ .../consul-ui/app/services/ui-config.js | 17 +++---- .../consul-ui/app/utils/get-environment.js | 51 +++++++++++++++++-- .../lib/startup/templates/head.html.js | 10 +--- .../tests/acceptance/dc/services/show.feature | 7 ++- .../tests/helpers/yadda-annotations.js | 17 +++++++ .../consul-ui/tests/steps/doubles/model.js | 7 ++- .../tests/unit/utils/get-environment-test.js | 29 +++++++---- 8 files changed, 122 insertions(+), 51 deletions(-) diff --git a/ui/packages/consul-ui/app/services/repository/metrics.js b/ui/packages/consul-ui/app/services/repository/metrics.js index 9f29137d3..e0138241d 100644 --- a/ui/packages/consul-ui/app/services/repository/metrics.js +++ b/ui/packages/consul-ui/app/services/repository/metrics.js @@ -1,20 +1,13 @@ import { inject as service } from '@ember/service'; import RepositoryService from 'consul-ui/services/repository'; -import { env } from 'consul-ui/env'; -// meta is used by DataSource to configure polling. The interval controls how -// long between each poll to the metrics provider. TODO - make this configurable -// in the UI settings. -const meta = { - interval: env('CONSUL_METRICS_POLL_INTERVAL') || 10000, -}; +// CONSUL_METRICS_POLL_INTERVAL controls how long between each poll to the +// metrics provider export default class MetricsService extends RepositoryService { - @service('ui-config') - cfg; - - @service('client/http') - client; + @service('ui-config') cfg; + @service('env') env; + @service('client/http') client; error = null; @@ -49,9 +42,11 @@ export default class MetricsService extends RepositoryService { this.provider.serviceRecentSummarySeries(slug, dc, nspace, protocol, {}), this.provider.serviceRecentSummaryStats(slug, dc, nspace, protocol, {}), ]; - return Promise.all(promises).then(function (results) { + return Promise.all(promises).then(results => { return { - meta: meta, + meta: { + interval: this.env.var('CONSUL_METRICS_POLL_INTERVAL') || 10000, + }, series: results[0], stats: results[1].stats, }; @@ -62,8 +57,10 @@ export default class MetricsService extends RepositoryService { if (this.error) { return Promise.reject(this.error); } - return this.provider.upstreamRecentSummaryStats(slug, dc, nspace, {}).then(function (result) { - result.meta = meta; + return this.provider.upstreamRecentSummaryStats(slug, dc, nspace, {}).then(result => { + result.meta = { + interval: this.env.var('CONSUL_METRICS_POLL_INTERVAL') || 10000, + }; return result; }); } @@ -72,8 +69,10 @@ export default class MetricsService extends RepositoryService { if (this.error) { return Promise.reject(this.error); } - return this.provider.downstreamRecentSummaryStats(slug, dc, nspace, {}).then(function (result) { - result.meta = meta; + return this.provider.downstreamRecentSummaryStats(slug, dc, nspace, {}).then(result => { + result.meta = { + interval: this.env.var('CONSUL_METRICS_POLL_INTERVAL') || 10000, + }; return result; }); } diff --git a/ui/packages/consul-ui/app/services/ui-config.js b/ui/packages/consul-ui/app/services/ui-config.js index cdd6b8593..afef0cfd9 100644 --- a/ui/packages/consul-ui/app/services/ui-config.js +++ b/ui/packages/consul-ui/app/services/ui-config.js @@ -1,15 +1,14 @@ -import Service from '@ember/service'; +import Service, { inject as service } from '@ember/service'; +import { get } from '@ember/object'; export default class UiConfigService extends Service { - config = undefined; + @service('env') env; + + async findByPath(path, configuration = {}) { + return get(this.get(), path); + } get() { - if (this.config === undefined) { - // Load config from our special meta tag for now. Later it might come from - // an API instead/as well. - var meta = unescape(document.getElementsByName('consul-ui/ui_config')[0].content); - this.config = JSON.parse(meta); - } - return this.config; + return this.env.var('CONSUL_UI_CONFIG'); } } diff --git a/ui/packages/consul-ui/app/utils/get-environment.js b/ui/packages/consul-ui/app/utils/get-environment.js index bfe93a2ea..0405ecf48 100644 --- a/ui/packages/consul-ui/app/utils/get-environment.js +++ b/ui/packages/consul-ui/app/utils/get-environment.js @@ -1,6 +1,26 @@ +import { runInDebug } from '@ember/debug'; +// 'environment' getter +// there are currently 3 levels of environment variables: +// 1. Those that can be set by the user by setting localStorage values +// 2. Those that can be set by the operator either via ui_config, or inferring +// from other server type properties (protocol) +// 3. Those that can be set only during development by adding cookie values +// via the browsers Web Inspector, or via the browsers hash (#COOKIE_NAME=1), +// which is useful for showing the UI with various settings enabled/disabled export default function(config = {}, win = window, doc = document) { - const dev = function() { - return doc.cookie + // look at the hash in the URL and transfer anything after the hash into + // cookies to enable linking of the UI with various settings enabled + runInDebug(() => { + if ( + typeof win.location !== 'undefined' && + typeof win.location.hash === 'string' && + win.location.hash.length > 0 + ) { + doc.cookie = win.location.hash.substr(1); + } + }); + const dev = function(str = doc.cookie) { + return str .split(';') .filter(item => item !== '') .map(item => item.trim().split('=')); @@ -20,6 +40,7 @@ export default function(config = {}, win = window, doc = document) { return {}; } }; + const ui_config = JSON.parse(unescape(doc.getElementsByName('consul-ui/ui_config')[0].content)); const scripts = doc.getElementsByTagName('script'); // we use the currently executing script as a reference // to figure out where we are for other things such as @@ -33,6 +54,21 @@ export default function(config = {}, win = window, doc = document) { const operator = function(str, env) { let protocol; switch (str) { + case 'CONSUL_UI_CONFIG': + const dashboards = {}; + const provider = env('CONSUL_METRICS_PROVIDER'); + const proxy = env('CONSUL_METRICS_PROXY_ENABLED'); + dashboards.service = env('CONSUL_SERVICE_DASHBOARD_URL'); + if (provider) { + ui_config.metrics_provider = provider; + } + if (proxy) { + ui_config.metrics_proxy_enabled = proxy; + } + if (dashboards.service) { + ui_config.dashboard_url_templates = dashboards; + } + return ui_config; case 'CONSUL_BASE_UI_URL': return currentSrc .split('/') @@ -85,6 +121,12 @@ export default function(config = {}, win = window, doc = document) { case 'CONSUL_SSO_ENABLE': prev['CONSUL_SSO_ENABLED'] = !!JSON.parse(String(value).toLowerCase()); break; + case 'CONSUL_METRICS_PROXY_ENABLE': + prev['CONSUL_METRICS_PROXY_ENABLED'] = !!JSON.parse(String(value).toLowerCase()); + break; + case 'CONSUL_UI_CONFIG': + prev['CONSUL_UI_CONFIG'] = JSON.parse(value); + break; default: prev[key] = value; } @@ -109,7 +151,10 @@ export default function(config = {}, win = window, doc = document) { case 'CONSUL_UI_REALTIME_RUNNER': // these are strings return user(str) || ui(str); - + case 'CONSUL_UI_CONFIG': + case 'CONSUL_METRICS_PROVIDER': + case 'CONSUL_METRICS_PROXY_ENABLE': + case 'CONSUL_SERVICE_DASHBOARD_URL': case 'CONSUL_BASE_UI_URL': case 'CONSUL_HTTP_PROTOCOL': case 'CONSUL_HTTP_MAX_CONNECTIONS': diff --git a/ui/packages/consul-ui/lib/startup/templates/head.html.js b/ui/packages/consul-ui/lib/startup/templates/head.html.js index 35c217e1f..702b6871b 100644 --- a/ui/packages/consul-ui/lib/startup/templates/head.html.js +++ b/ui/packages/consul-ui/lib/startup/templates/head.html.js @@ -3,15 +3,7 @@ module.exports = ({ appName, environment, rootURL, config }) => ` diff --git a/ui/packages/consul-ui/tests/acceptance/dc/services/show.feature b/ui/packages/consul-ui/tests/acceptance/dc/services/show.feature index 2b5b0399e..599475ed7 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/services/show.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/services/show.feature @@ -107,10 +107,15 @@ Feature: dc / services / show: Show Service --- Scenario: Given a dashboard template has been set Given 1 datacenter model with the value "dc1" + And ui_config from yaml + --- + dashboard_url_templates: + service: https://something.com?{{Service.Name}}&{{Datacenter}} + --- When I visit the service page for yaml --- dc: dc1 service: service-0 --- # The Metrics dashboard should use the Service.Name not the ID - And I see href on the metricsAnchor like "https://example.com?service-0&dc1" + And I see href on the metricsAnchor like "https://something.com?service-0&dc1" diff --git a/ui/packages/consul-ui/tests/helpers/yadda-annotations.js b/ui/packages/consul-ui/tests/helpers/yadda-annotations.js index 40056ca05..e629f8653 100644 --- a/ui/packages/consul-ui/tests/helpers/yadda-annotations.js +++ b/ui/packages/consul-ui/tests/helpers/yadda-annotations.js @@ -9,7 +9,23 @@ import dictionary from '../dictionary'; const getDictionary = dictionary(utils); const staticClassList = [...document.documentElement.classList]; +const getCookies = () => { + return Object.fromEntries(document.cookie.split(';').map(item => item.split('='))); +}; +const getResetCookies = function() { + const start = getCookies(); + return () => { + const startKeys = Object.keys(start); + const endKeys = Object.keys(getCookies()); + const diff = endKeys.filter(key => !startKeys.includes(key)); + diff.forEach(item => { + document.cookie = `${item}= ; expires=${new Date(0)}`; + }); + }; +}; +let resetCookies; const reset = function() { + resetCookies(); window.localStorage.clear(); api.server.reset(); const list = document.documentElement.classList; @@ -21,6 +37,7 @@ const reset = function() { }); }; const startup = function() { + resetCookies = getResetCookies(); api.server.setCookie('CONSUL_LATENCY', 0); }; diff --git a/ui/packages/consul-ui/tests/steps/doubles/model.js b/ui/packages/consul-ui/tests/steps/doubles/model.js index 75a0cb981..ca9e035c8 100644 --- a/ui/packages/consul-ui/tests/steps/doubles/model.js +++ b/ui/packages/consul-ui/tests/steps/doubles/model.js @@ -1,4 +1,4 @@ -export default function(scenario, create) { +export default function(scenario, create, win = window, doc = document) { scenario .given(['an external edit results in $number $model model[s]?'], function(number, model) { return create(number, model); @@ -17,7 +17,10 @@ export default function(scenario, create) { ) .given(['settings from yaml\n$yaml'], function(data) { return Object.keys(data).forEach(function(key) { - window.localStorage[key] = JSON.stringify(data[key]); + win.localStorage[key] = JSON.stringify(data[key]); }); + }) + .given(['ui_config from yaml\n$yaml'], function(data) { + doc.cookie = `CONSUL_UI_CONFIG=${JSON.stringify(data)}`; }); } diff --git a/ui/packages/consul-ui/tests/unit/utils/get-environment-test.js b/ui/packages/consul-ui/tests/unit/utils/get-environment-test.js index 94667ded0..bb91aa38e 100644 --- a/ui/packages/consul-ui/tests/unit/utils/get-environment-test.js +++ b/ui/packages/consul-ui/tests/unit/utils/get-environment-test.js @@ -9,11 +9,12 @@ const getEntriesByType = function(type) { }, ]; }; -const makeGetElementsByTagName = function(src) { +const makeGetElementsBy = function(str) { return function(name) { return [ { - src: src, + src: str, + content: str, }, ]; }; @@ -22,13 +23,17 @@ const win = { performance: { getEntriesByType: getEntriesByType, }, + location: { + hash: '', + }, localStorage: { getItem: function(key) {}, }, }; const doc = { cookie: '', - getElementsByTagName: makeGetElementsByTagName(''), + getElementsByTagName: makeGetElementsBy(''), + getElementsByName: makeGetElementsBy('{}'), }; module('Unit | Utility | getEnvironment', function() { test('it returns a function', function(assert) { @@ -55,14 +60,16 @@ module('Unit | Utility | getEnvironment', function() { let expected = 'http://localhost/ui'; let doc = { cookie: '', - getElementsByTagName: makeGetElementsByTagName(`${expected}/assets/consul-ui.js`), + getElementsByTagName: makeGetElementsBy(`${expected}/assets/consul-ui.js`), + getElementsByName: makeGetElementsBy('{}'), }; let env = getEnvironment(config, win, doc); assert.equal(env('CONSUL_BASE_UI_URL'), expected); expected = 'http://localhost/somewhere/else'; doc = { cookie: '', - getElementsByTagName: makeGetElementsByTagName(`${expected}/assets/consul-ui.js`), + getElementsByTagName: makeGetElementsBy(`${expected}/assets/consul-ui.js`), + getElementsByName: makeGetElementsBy('{}'), }; env = getEnvironment(config, win, doc); assert.equal(env('CONSUL_BASE_UI_URL'), expected); @@ -135,7 +142,8 @@ module('Unit | Utility | getEnvironment', function() { }; let doc = { cookie: 'CONSUL_NSPACES_ENABLE=1', - getElementsByTagName: makeGetElementsByTagName(''), + getElementsByTagName: makeGetElementsBy(''), + getElementsByName: makeGetElementsBy('{}'), }; let env = getEnvironment(config, win, doc); assert.ok(env('CONSUL_NSPACES_ENABLED')); @@ -145,7 +153,8 @@ module('Unit | Utility | getEnvironment', function() { }; doc = { cookie: 'CONSUL_NSPACES_ENABLE=0', - getElementsByTagName: makeGetElementsByTagName(''), + getElementsByTagName: makeGetElementsBy(''), + getElementsByName: makeGetElementsBy('{}'), }; env = getEnvironment(config, win, doc); assert.notOk(env('CONSUL_NSPACES_ENABLED')); @@ -169,7 +178,8 @@ module('Unit | Utility | getEnvironment', function() { }; let doc = { cookie: 'CONSUL_NSPACES_ENABLE=1', - getElementsByTagName: makeGetElementsByTagName(''), + getElementsByTagName: makeGetElementsBy(''), + getElementsByName: makeGetElementsBy('{}'), }; let env = getEnvironment(config, win, doc); assert.notOk(env('CONSUL_NSPACES_ENABLED')); @@ -179,7 +189,8 @@ module('Unit | Utility | getEnvironment', function() { }; doc = { cookie: 'CONSUL_NSPACES_ENABLE=0', - getElementsByTagName: makeGetElementsByTagName(''), + getElementsByTagName: makeGetElementsBy(''), + getElementsByName: makeGetElementsBy('{}'), }; env = getEnvironment(config, win, doc); assert.ok(env('CONSUL_NSPACES_ENABLED'));