-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ability to use JS file for UI configuration (#123 from jaeger-ui) #2707
Add ability to use JS file for UI configuration (#123 from jaeger-ui) #2707
Conversation
…om jaeger-ui) Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
85cc44b
to
67f35b3
Compare
Codecov Report
@@ Coverage Diff @@
## master #2707 +/- ##
==========================================
+ Coverage 95.73% 95.77% +0.03%
==========================================
Files 216 217 +1
Lines 9599 9628 +29
==========================================
+ Hits 9190 9221 +31
+ Misses 336 335 -1
+ Partials 73 72 -1
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good start!
cmd/query/app/static_handler.go
Outdated
bytes, _ := json.Marshal(config) | ||
configString = fmt.Sprintf("JAEGER_CONFIG = %v", string(bytes)) | ||
} else if configBytes != nil { | ||
configString = fmt.Sprintf("JAEGER_CONFIG = %v", string(configBytes)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't look right. In case of JS file that contains a function there needs to be a part of the template that invokes that function and assigns its result to JAEGER_CONFIG ( or something similar).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest taking a look at this line from the perspective of invoking the JS config file itself.
My idea is next:
- we add a more strict JS file format validation like
/^function\s[a-zA-Z$_][a-zA-Z0-9$_]+(\s)?\(\)(\s)?{(.*\n)+}$/gi
- in this case we can rely on JS file and assume, that its plain JS function like
function LoadCodnfig(){
console.log('Hello World!');
return {
callMe: () => {
return Promise.resolve(setTimeout(() => {
console.log('CALL ME!!!', 1000);
}))
},
asd: 'sdf'
}
}
- next, we add
()
at the moment of injecting config insideindex.html
and we have IIFE insideindex.html
- in
jaeger-ui
we will add a cache forgetConfig()
like
diff --git a/packages/jaeger-ui/src/utils/config/get-config.tsx b/packages/jaeger-ui/src/utils/config/get-config.tsx
index 458ba0c..68dfd47 100644
--- a/packages/jaeger-ui/src/utils/config/get-config.tsx
+++ b/packages/jaeger-ui/src/utils/config/get-config.tsx
@@ -16,15 +16,21 @@ import _get from 'lodash/get';
import processDeprecation from './process-deprecation';
import defaultConfig, { deprecations } from '../../constants/default-config';
+import { Config } from '../../types/config';
let haveWarnedFactoryFn = false;
let haveWarnedDeprecations = false;
+let cachedConfig: null | Config = null;
/**
* Merge the embedded config from the query service (if present) with the
* default config from `../../constants/default-config`.
*/
export default function getConfig() {
+ if(cachedConfig) {
+ return cachedConfig;
+ }
+
const getJaegerUiConfig = window.getJaegerUiConfig;
if (typeof getJaegerUiConfig !== 'function') {
if (!haveWarnedFactoryFn) {
@@ -52,6 +58,7 @@ export default function getConfig() {
rv[key] = { ...defaultConfig[key], ...embedded[key] };
}
}
+ cachedConfig = rv;
return rv;
}
From a usage perspective, we can say, that JS config file is Function and we invoke it once the page loaded
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you show how index.html would be changed to handle JS file? Right now it looks like this, and the middle line is being replaced with the content of JSON file.
function getJaegerUiConfig() {
const DEFAULT_CONFIG = null;
const JAEGER_CONFIG = DEFAULT_CONFIG;
return JAEGER_CONFIG;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For JSON:
function getJaegerUiConfig() {
const DEFAULT_CONFIG = null;
const JAEGER_CONFIG = {"a":1, "b":2};
return JAEGER_CONFIG;
}
For JS:
function getJaegerUiConfig() {
const DEFAULT_CONFIG = null;
const JAEGER_CONFIG = function UIConfig() {
return {"a":1, "b":2};
}();
return JAEGER_CONFIG;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, you're suggesting to piggy back on the string replacement that currently happens for const JAEGER_CONFIG = DEFAULT_CONFIG;
line. While this is doable, I wonder if it puts a lot of limitations on what the actual config function can do. If all it does is return a static dictionary, then sure, but we may want to use it as a mechanism for loading plugins, etc. Wouldn't it be better if it was embedded at the top level, not inlined inside an expression?
What if we do this instead:
// JAEGER_CONFIG_JS
// the line above may be replaced by user-provided JS file that should define a UIConfig function.
function getJaegerUiConfig() {
if "UIConfig is not null and is a function" {
return UIConfig();
}
const DEFAULT_CONFIG = null;
const JAEGER_CONFIG = DEFAULT_CONFIG;
return JAEGER_CONFIG;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
regarding this:
let cachedConfig: null | Config = null;
I think there is a memoize module that we're using, could it be used instead of explicit caching?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I understand you correctly?
JSON:
// JAEGER_CONFIG_JS
// the line above may be replaced by user-provided JS file that should define a UIConfig function.
function getJaegerUiConfig() {
if(typeof window.UIConfig === 'function') {
return UIConfig();
}
const DEFAULT_CONFIG = null;
const JAEGER_CONFIG = {a:1, b:2};
return JAEGER_CONFIG;
}
JS:
function UIConfig() {
return {a:1, b:2}
}
// the line above may be replaced by user-provided JS file that should define a UIConfig function.
function getJaegerUiConfig() {
if(typeof window.UIConfig === 'function') {
return UIConfig();
}
const DEFAULT_CONFIG = null;
const JAEGER_CONFIG = {a:1, b:2};
return JAEGER_CONFIG;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm. any concerns?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No concerns. Thanks for clarifying! 🤝 WIll update PR shortly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bonus points if we can get rid of the comment during substitution (e.g. put it all on one line)
cmd/query/app/static_handler.go
Outdated
var unmarshal func([]byte, interface{}) error | ||
|
||
switch strings.ToLower(ext) { | ||
case ".json": | ||
unmarshal = json.Unmarshal | ||
case ".js": | ||
r = bytes.TrimSpace(bytesConfig) | ||
if !bytes.HasPrefix(r, []byte("function")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest to check for string "function UIConfig()" if you want some kind of validation, not the prefix, because there may be comments etc.
Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
Have update the PR. @yurishkuro can you take a look? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm overall, some clean-up comments
cmd/query/app/static_handler.go
Outdated
@@ -39,6 +40,7 @@ var ( | |||
|
|||
// The following patterns are searched and replaced in the index.html as a way of customizing the UI. | |||
configPattern = regexp.MustCompile("JAEGER_CONFIG *= *DEFAULT_CONFIG;") | |||
configJsPattern = regexp.MustCompile(`(?im)(^\s+)\/\/.*JAEGER_CONFIG_JS.*\n.*`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
configJsPattern = regexp.MustCompile(`(?im)(^\s+)\/\/.*JAEGER_CONFIG_JS.*\n.*`) | |
configJsPattern = regexp.MustCompile(`(?im)(^\s+)\/\/\s*JAEGER_CONFIG_JS.*\n.*`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the purpose of trailing .*
? Also, with the m
mode, does .*\n
guarantee single-line match?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.*
- my bad, you are correct \s+
is best fits.
m
is for multiline search. I assume that we must replace 2 lines: line with JAEGER_CONFIG_JS
and following comment on the next line
// JAEGER_CONFIG_JS
// the line above may be replaced by user-provided JS file that should define a UIConfig function.
cmd/query/app/static_handler.go
Outdated
@@ -72,6 +74,12 @@ type StaticAssetsHandlerOptions struct { | |||
Logger *zap.Logger | |||
} | |||
|
|||
// UIConfigOptions define options for injecting config in index.html in loadAndEnrichIndexHTML | |||
type UIConfigOptions struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- "options" typically refers to inputs, this is used as output
- does not need to be public
type UIConfigOptions struct { | |
type loadedConfig struct { |
cmd/query/app/static_handler.go
Outdated
bytes, _ := json.Marshal(config) | ||
configString = fmt.Sprintf("JAEGER_CONFIG = %v", string(bytes)) | ||
} else if configObject != nil { | ||
indexBytes = configObject.regexp.ReplaceAll(indexBytes, append([]byte(`${1}`), configObject.config...)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am confused by ${1}
- is it just repeating whitespace? If we match on // JAEGER...
and replace just that, the whitespace becomes irrelevant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are correct, it's for white spaces for function UIConfig(). To save origin indent 😶 . Will remove it.
cmd/query/app/static_handler.go
Outdated
} | ||
// TODO if we want to support other config formats like YAML, we need to normalize `config` to be | ||
// suitable for json.Marshal(). For example, YAML parser may return a map that has keys of type | ||
// interface{}, and json.Marshal() is unable to serialize it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest removing this. We don't have any plans to support anything other than JSON and JS. I would move the unmarshal code directly under case ".json":
to simplify the logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can also return directly from switch branches instead of maintaining global-like r
, re
, c
variables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
cmd/query/app/static_handler_test.go
Outdated
@@ -117,57 +119,67 @@ func TestNewStaticAssetsHandlerErrors(t *testing.T) { | |||
|
|||
// This test is potentially intermittent | |||
func TestHotReloadUIConfigTempFile(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does reload functionality actually depend on the type of file being loaded? It seems it only complicates the test without any real benefits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably no, but to keep consistency I did these changes. Will revert this back
cmd/query/app/static_handler_test.go
Outdated
expectedError string | ||
} | ||
|
||
jsonRegExpPattern := regexp.MustCompile("JAEGER_CONFIG *= *DEFAULT_CONFIG;") | ||
jsRegExpPattern := regexp.MustCompile(`(?im)(^\s+)\/\/.*JAEGER_CONFIG_JS.*\n.*`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just use the constants here? Also, do we even need to be comparing which regex is returned? I would rather express the test in terms of what the resulting HTML looks like, not the implementation details of how that HTML is produced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just use the constants here?
Sure
Also, do we even need to be comparing which regex is returned?
This is unit test, I believe, and LoadUIConfig
is return
type UIConfigOptions struct {
regexp *regexp.Regexp
config []byte
}
regexp is part of it...
I would rather express the test in terms of what the resulting HTML looks like, not the implementation details of how that HTML is produced.
Will update TestRegisterStaticHandler
to cover JS file config case
Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
Have updated the PR. @yurishkuro can you take a look, please? 🙏 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, a couple of minor nits
cmd/query/app/static_handler.go
Outdated
}, nil | ||
case ".js": | ||
r = bytes.TrimSpace(bytesConfig) | ||
if !bytes.Contains(r, []byte("function UIConfig(){")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it's better to use regex here? Especially the part (){
, without space before {
, is a matter of style/formatting.
cmd/query/app/static_handler.go
Outdated
case ".js": | ||
r = bytes.TrimSpace(bytesConfig) | ||
if !bytes.Contains(r, []byte("function UIConfig(){")) { | ||
return nil, fmt.Errorf("wrong JS function format in UI config file format %v", uiConfig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
more self-explanatory error:
return nil, fmt.Errorf("wrong JS function format in UI config file format %v", uiConfig) | |
return nil, fmt.Errorf("UI config file must define function UIConfig(): %v", uiConfig) |
cmd/query/app/static_handler.go
Outdated
@@ -210,30 +211,43 @@ func loadIndexHTML(open func(string) (http.File, error)) ([]byte, error) { | |||
return indexBytes, nil | |||
} | |||
|
|||
func loadUIConfig(uiConfig string) (map[string]interface{}, error) { | |||
func loadUIConfig(uiConfig string) (*loadedConfig, error) { | |||
if uiConfig == "" { | |||
return nil, nil | |||
} | |||
ext := filepath.Ext(uiConfig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for better readability, I would move this next to the switch
condition:
ext := filepath.Ext(uiConfig)
switch strings.ToLower(ext) {
cmd/query/app/static_handler.go
Outdated
return &loadedConfig{ | ||
regexp: configJsPattern, | ||
config: r, | ||
}, nil | ||
default: | ||
return nil, fmt.Errorf("unrecognized UI config file format %v", uiConfig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nil, fmt.Errorf("unrecognized UI config file format %v", uiConfig) | |
return nil, fmt.Errorf("unrecognized UI config file format, expecting .js or .json file: %v", uiConfig) |
and sorry for delay, did not get the notification of the last change |
Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 🎉 🎉
@th3M1ke this will also require some change in the jaeger-ui, to the html template and the function that retrieves the config. Did you already implement it? It may be worth trying to build query-service with the updated UI as an end-to-end test of JS configs (before we merge this PR, in case of any issues).
Pull request has been modified.
@yurishkuro I have added a PR with required changes in the |
I suggest we bump the UI git submodule to pick up jaegertracing/jaeger-ui#677 (which I just merged). Then once this PR is merged, the latest Docker image for all-in-one can be used to test JS config. |
Would this require a new release tag of Jaeger-UI? |
No, submodule links by a commit. |
Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
…gertracing#2707) * Add ability to use JS file for UI configuration (#123 from jaeger-ui) Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io> * Update UI config file injection Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io> * Update update test, refactor loadUIConfig function Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io> * Update errors text, minor refactor Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io> * Remove debug code Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io> * Update jaeger-ui to latest master with index.html changes Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
Which problem is this PR solving?
Short description of the changes
javascript
file for UI configuration