-
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
Changes from 2 commits
67f35b3
b1ea51d
35039ed
02a6292
85c1274
a7565c0
12394b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
function UIConfig(){ | ||
return { | ||
menu: [ | ||
{ | ||
label: "About Jaeger" | ||
} | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
() => { | ||
return { | ||
menu: [ | ||
{ | ||
label: "GitHub", | ||
url: "https://github.com/jaegertracing/jaeger" | ||
} | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
function UIConfig(){ | ||
return { | ||
menu: [ | ||
{ | ||
label: "GitHub", | ||
url: "https://github.com/jaegertracing/jaeger" | ||
} | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
function UIConfig(){ | ||
return { | ||
x: "y" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -16,6 +16,7 @@ | |||||
package app | ||||||
|
||||||
import ( | ||||||
"bytes" | ||||||
"encoding/json" | ||||||
"fmt" | ||||||
"io/ioutil" | ||||||
|
@@ -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.*`) | ||||||
versionPattern = regexp.MustCompile("JAEGER_VERSION *= *DEFAULT_VERSION;") | ||||||
basePathPattern = regexp.MustCompile(`<base href="/"`) // Note: tag is not closed | ||||||
) | ||||||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
regexp *regexp.Regexp | ||||||
config []byte | ||||||
} | ||||||
|
||||||
// NewStaticAssetsHandler returns a StaticAssetsHandler | ||||||
func NewStaticAssetsHandler(staticAssetsRoot string, options StaticAssetsHandlerOptions) (*StaticAssetsHandler, error) { | ||||||
assetsFS := ui.StaticFiles | ||||||
|
@@ -105,17 +113,11 @@ func loadAndEnrichIndexHTML(open func(string) (http.File, error), options Static | |||||
return nil, fmt.Errorf("cannot load index.html: %w", err) | ||||||
} | ||||||
// replace UI config | ||||||
configString := "JAEGER_CONFIG = DEFAULT_CONFIG" | ||||||
if config, err := loadUIConfig(options.UIConfigPath); err != nil { | ||||||
if configObject, err := loadUIConfig(options.UIConfigPath); err != nil { | ||||||
return nil, err | ||||||
} else if config != nil { | ||||||
// 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. | ||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more. I am confused by There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
} | ||||||
indexBytes = configPattern.ReplaceAll(indexBytes, []byte(configString+";")) | ||||||
// replace Jaeger version | ||||||
versionJSON, _ := json.Marshal(version.Get()) | ||||||
versionString := fmt.Sprintf("JAEGER_VERSION = %s;", string(versionJSON)) | ||||||
|
@@ -210,30 +212,49 @@ 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) (*UIConfigOptions, error) { | ||||||
if uiConfig == "" { | ||||||
return nil, nil | ||||||
} | ||||||
ext := filepath.Ext(uiConfig) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for better readability, I would move this next to the ext := filepath.Ext(uiConfig)
switch strings.ToLower(ext) { |
||||||
bytes, err := ioutil.ReadFile(filepath.Clean(uiConfig)) | ||||||
bytesConfig, err := ioutil.ReadFile(filepath.Clean(uiConfig)) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("cannot read UI config file %v: %w", uiConfig, err) | ||||||
} | ||||||
|
||||||
var c map[string]interface{} | ||||||
var r []byte | ||||||
var re *regexp.Regexp | ||||||
var unmarshal func([]byte, interface{}) error | ||||||
|
||||||
switch strings.ToLower(ext) { | ||||||
case ".json": | ||||||
unmarshal = json.Unmarshal | ||||||
re = configPattern | ||||||
case ".js": | ||||||
r = bytes.TrimSpace(bytesConfig) | ||||||
re = configJsPattern | ||||||
if !bytes.Contains(r, []byte("function UIConfig(){")) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps it's better to use regex here? Especially the part |
||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more. more self-explanatory error:
Suggested change
|
||||||
} | ||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
if err := unmarshal(bytes, &c); err != nil { | ||||||
return nil, fmt.Errorf("cannot parse UI config file %v: %w", uiConfig, err) | ||||||
if unmarshal != nil { | ||||||
if err := unmarshal(bytesConfig, &c); err != nil { | ||||||
return nil, fmt.Errorf("cannot parse UI config file %v: %w", uiConfig, err) | ||||||
} | ||||||
// 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 commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can also return directly from switch branches instead of maintaining global-like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||||||
if r, err = json.Marshal(c); err != nil { | ||||||
return nil, fmt.Errorf("cannot encode UI config file %v: %w", uiConfig, err) | ||||||
} | ||||||
r = append([]byte("JAEGER_CONFIG = "), append(r, byte(';'))...) | ||||||
} | ||||||
return c, nil | ||||||
|
||||||
return &UIConfigOptions{regexp: re, config: r}, nil | ||||||
} | ||||||
|
||||||
// RegisterRoutes registers routes for this handler on the given router | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,11 +16,13 @@ | |
package app | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"os" | ||
"regexp" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
@@ -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 commentThe 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 commentThe 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 |
||
tmpfile, err := ioutil.TempFile("", "ui-config-hotreload.*.json") | ||
assert.NoError(t, err) | ||
run := func(description string, extension string) { | ||
t.Run(description, func(t *testing.T) { | ||
tmpfile, err := ioutil.TempFile("", "ui-config-hotreload.*."+extension) | ||
assert.NoError(t, err) | ||
|
||
tmpFileName := tmpfile.Name() | ||
defer os.Remove(tmpFileName) | ||
tmpFileName := tmpfile.Name() | ||
defer os.Remove(tmpFileName) | ||
|
||
content, err := ioutil.ReadFile("fixture/ui-config-hotreload.json") | ||
assert.NoError(t, err) | ||
content, err := ioutil.ReadFile("fixture/ui-config-hotreload." + extension) | ||
assert.NoError(t, err) | ||
|
||
err = ioutil.WriteFile(tmpFileName, content, 0644) | ||
assert.NoError(t, err) | ||
err = ioutil.WriteFile(tmpFileName, content, 0644) | ||
assert.NoError(t, err) | ||
|
||
h, err := NewStaticAssetsHandler("fixture", StaticAssetsHandlerOptions{ | ||
UIConfigPath: tmpFileName, | ||
}) | ||
assert.NoError(t, err) | ||
h, err := NewStaticAssetsHandler("fixture", StaticAssetsHandlerOptions{ | ||
UIConfigPath: tmpFileName, | ||
}) | ||
assert.NoError(t, err) | ||
|
||
c := string(h.indexHTML.Load().([]byte)) | ||
assert.Contains(t, c, "About Jaeger") | ||
|
||
c := string(h.indexHTML.Load().([]byte)) | ||
assert.Contains(t, c, "About Jaeger") | ||
newContent := strings.Replace(string(content), "About Jaeger", "About a new Jaeger", 1) | ||
err = ioutil.WriteFile(tmpFileName, []byte(newContent), 0644) | ||
assert.NoError(t, err) | ||
|
||
newContent := strings.Replace(string(content), "About Jaeger", "About a new Jaeger", 1) | ||
err = ioutil.WriteFile(tmpFileName, []byte(newContent), 0644) | ||
assert.NoError(t, err) | ||
done := make(chan bool) | ||
go func() { | ||
for { | ||
i := string(h.indexHTML.Load().([]byte)) | ||
|
||
done := make(chan bool) | ||
go func() { | ||
for { | ||
i := string(h.indexHTML.Load().([]byte)) | ||
if strings.Contains(i, "About a new Jaeger") { | ||
done <- true | ||
} | ||
time.Sleep(10 * time.Millisecond) | ||
} | ||
}() | ||
|
||
if strings.Contains(i, "About a new Jaeger") { | ||
done <- true | ||
select { | ||
case <-done: | ||
assert.Contains(t, string(h.indexHTML.Load().([]byte)), "About a new Jaeger") | ||
case <-time.After(time.Second): | ||
assert.Fail(t, "timed out waiting for the hot reload to kick in") | ||
} | ||
time.Sleep(10 * time.Millisecond) | ||
} | ||
}() | ||
|
||
select { | ||
case <-done: | ||
assert.Contains(t, string(h.indexHTML.Load().([]byte)), "About a new Jaeger") | ||
case <-time.After(time.Second): | ||
assert.Fail(t, "timed out waiting for the hot reload to kick in") | ||
}) | ||
} | ||
|
||
run("json hot reload", "json") | ||
run("js hot reload", "js") | ||
} | ||
|
||
func TestLoadUIConfig(t *testing.T) { | ||
type testCase struct { | ||
configFile string | ||
expected map[string]interface{} | ||
expected *UIConfigOptions | ||
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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more.
Sure
This is unit test, I believe, and
regexp is part of it...
Will update |
||
|
||
run := func(description string, testCase testCase) { | ||
t.Run(description, func(t *testing.T) { | ||
config, err := loadUIConfig(testCase.configFile) | ||
|
@@ -181,7 +193,7 @@ func TestLoadUIConfig(t *testing.T) { | |
} | ||
|
||
run("no config", testCase{}) | ||
run("invalid config", testCase{ | ||
run("invalid json config", testCase{ | ||
configFile: "invalid", | ||
expectedError: "cannot read UI config file invalid: open invalid: no such file or directory", | ||
}) | ||
|
@@ -195,19 +207,55 @@ func TestLoadUIConfig(t *testing.T) { | |
}) | ||
run("json", testCase{ | ||
configFile: "fixture/ui-config.json", | ||
expected: map[string]interface{}{"x": "y"}, | ||
expected: &UIConfigOptions{ | ||
config: []byte(`JAEGER_CONFIG = {"x":"y"};`), | ||
regexp: jsonRegExpPattern, | ||
}, | ||
}) | ||
c, _ := json.Marshal(map[string]interface{}{ | ||
"menu": []interface{}{ | ||
map[string]interface{}{ | ||
"label": "GitHub", | ||
"url": "https://github.com/jaegertracing/jaeger", | ||
}, | ||
}, | ||
}) | ||
run("json-menu", testCase{ | ||
configFile: "fixture/ui-config-menu.json", | ||
expected: map[string]interface{}{ | ||
"menu": []interface{}{ | ||
map[string]interface{}{ | ||
"label": "GitHub", | ||
"url": "https://github.com/jaegertracing/jaeger", | ||
}, | ||
}, | ||
expected: &UIConfigOptions{ | ||
config: append([]byte("JAEGER_CONFIG = "), append(c, byte(';'))...), | ||
regexp: jsonRegExpPattern, | ||
}, | ||
}) | ||
run("malformed js config", testCase{ | ||
configFile: "fixture/ui-config-malformed.js", | ||
expectedError: "wrong JS function format in UI config file format fixture/ui-config-malformed.js", | ||
}) | ||
run("js", testCase{ | ||
configFile: "fixture/ui-config.js", | ||
expected: &UIConfigOptions{ | ||
regexp: jsRegExpPattern, | ||
config: []byte(`function UIConfig(){ | ||
return { | ||
x: "y" | ||
} | ||
}`)}, | ||
}) | ||
run("js-menu", testCase{ | ||
configFile: "fixture/ui-config-menu.js", | ||
expected: &UIConfigOptions{ | ||
regexp: jsRegExpPattern, | ||
config: []byte(`function UIConfig(){ | ||
return { | ||
menu: [ | ||
{ | ||
label: "GitHub", | ||
url: "https://github.com/jaegertracing/jaeger" | ||
} | ||
] | ||
} | ||
}`)}, | ||
}) | ||
} | ||
|
||
type fakeFile 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.
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 them
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 withJAEGER_CONFIG_JS
and following comment on the next line