Skip to content
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

Conversation

th3M1ke
Copy link
Contributor

@th3M1ke th3M1ke commented Dec 29, 2020

Which problem is this PR solving?

Short description of the changes

  • Added ability to use javascript file for UI configuration

…om jaeger-ui)

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
@th3M1ke th3M1ke force-pushed the Add-ability-to-use-JS-file-for-UI-configuration branch from 85cc44b to 67f35b3 Compare December 29, 2020 15:49
@codecov
Copy link

codecov bot commented Dec 29, 2020

Codecov Report

Merging #2707 (12394b3) into master (2f8ffa9) will increase coverage by 0.03%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            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     
Impacted Files Coverage Δ
cmd/query/app/static_handler.go 96.77% <100.00%> (+1.99%) ⬆️
pkg/discovery/grpcresolver/grpc_resolver.go 100.00% <0.00%> (ø)
cmd/agent/app/reporter/connect_metrics.go 100.00% <0.00%> (ø)
cmd/agent/app/reporter/grpc/builder.go 95.83% <0.00%> (+0.83%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update dc091c5...12394b3. Read the comment docs.

Copy link
Member

@yurishkuro yurishkuro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start!

bytes, _ := json.Marshal(config)
configString = fmt.Sprintf("JAEGER_CONFIG = %v", string(bytes))
} else if configBytes != nil {
configString = fmt.Sprintf("JAEGER_CONFIG = %v", string(configBytes))
Copy link
Member

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

Copy link
Contributor Author

@th3M1ke th3M1ke Dec 29, 2020

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 inside index.html and we have IIFE inside index.html
  • in jaeger-ui we will add a cache for getConfig() 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

Copy link
Member

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;
      }

Copy link
Contributor Author

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;
      }

Copy link
Member

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;
}

Copy link
Member

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?

Copy link
Contributor Author

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;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. any concerns?

Copy link
Contributor Author

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

Copy link
Member

@yurishkuro yurishkuro Dec 30, 2020

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)

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")) {
Copy link
Member

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>
@th3M1ke
Copy link
Contributor Author

th3M1ke commented Jan 4, 2021

Have update the PR. @yurishkuro can you take a look?

Copy link
Member

@yurishkuro yurishkuro left a 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

@@ -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.*`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
configJsPattern = regexp.MustCompile(`(?im)(^\s+)\/\/.*JAEGER_CONFIG_JS.*\n.*`)
configJsPattern = regexp.MustCompile(`(?im)(^\s+)\/\/\s*JAEGER_CONFIG_JS.*\n.*`)

Copy link
Member

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?

Copy link
Contributor Author

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.

@@ -72,6 +74,12 @@ type StaticAssetsHandlerOptions struct {
Logger *zap.Logger
}

// UIConfigOptions define options for injecting config in index.html in loadAndEnrichIndexHTML
type UIConfigOptions struct {
Copy link
Member

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
Suggested change
type UIConfigOptions struct {
type loadedConfig struct {

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...))
Copy link
Member

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.

Copy link
Contributor Author

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.

}
// 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.
Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -117,57 +119,67 @@ func TestNewStaticAssetsHandlerErrors(t *testing.T) {

// This test is potentially intermittent
func TestHotReloadUIConfigTempFile(t *testing.T) {
Copy link
Member

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.

Copy link
Contributor Author

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

expectedError string
}

jsonRegExpPattern := regexp.MustCompile("JAEGER_CONFIG *= *DEFAULT_CONFIG;")
jsRegExpPattern := regexp.MustCompile(`(?im)(^\s+)\/\/.*JAEGER_CONFIG_JS.*\n.*`)
Copy link
Member

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.

Copy link
Contributor Author

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>
@th3M1ke
Copy link
Contributor Author

th3M1ke commented Jan 5, 2021

Have updated the PR. @yurishkuro can you take a look, please? 🙏

Copy link
Member

@yurishkuro yurishkuro left a 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

}, nil
case ".js":
r = bytes.TrimSpace(bytesConfig)
if !bytes.Contains(r, []byte("function UIConfig(){")) {
Copy link
Member

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.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more self-explanatory error:

Suggested change
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)

@@ -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)
Copy link
Member

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) {

return &loadedConfig{
regexp: configJsPattern,
config: r,
}, nil
default:
return nil, fmt.Errorf("unrecognized UI config file format %v", uiConfig)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)

@yurishkuro
Copy link
Member

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>
yurishkuro
yurishkuro previously approved these changes Jan 12, 2021
Copy link
Member

@yurishkuro yurishkuro left a 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).

@th3M1ke
Copy link
Contributor Author

th3M1ke commented Jan 13, 2021

@yurishkuro I have added a PR with required changes in the jaeger-ui repo: jaegertracing/jaeger-ui#677

@yurishkuro
Copy link
Member

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.

@albertteoh
Copy link
Contributor

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?

@yurishkuro
Copy link
Member

No, submodule links by a commit.

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
Copy link
Member

@yurishkuro yurishkuro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@yurishkuro yurishkuro merged commit b73b0c8 into jaegertracing:master Jan 14, 2021
bhiravabhatla pushed a commit to bhiravabhatla/jaeger that referenced this pull request Jan 25, 2021
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Change UI configuration to a Javascript file
4 participants