From 85cc44bdd20be4d41cef157f7956527a1d4efc9e Mon Sep 17 00:00:00 2001 From: Mykhailo Semenchenko Date: Tue, 29 Dec 2020 15:59:27 +0200 Subject: [PATCH] Add ability to use JS file for UI configuration (#123 from jaeger-ui) Signed-off-by: Mykhailo Semenchenko --- cmd/query/app/fixture/ui-config-hotreload.js | 9 ++ cmd/query/app/fixture/ui-config-malformed.js | 10 ++ cmd/query/app/fixture/ui-config-menu.js | 10 ++ cmd/query/app/fixture/ui-config.js | 5 + cmd/query/app/static_handler.go | 36 ++++-- cmd/query/app/static_handler_test.go | 120 ++++++++++++------- 6 files changed, 135 insertions(+), 55 deletions(-) create mode 100644 cmd/query/app/fixture/ui-config-hotreload.js create mode 100644 cmd/query/app/fixture/ui-config-malformed.js create mode 100644 cmd/query/app/fixture/ui-config-menu.js create mode 100644 cmd/query/app/fixture/ui-config.js diff --git a/cmd/query/app/fixture/ui-config-hotreload.js b/cmd/query/app/fixture/ui-config-hotreload.js new file mode 100644 index 00000000000..ed9d221a8d9 --- /dev/null +++ b/cmd/query/app/fixture/ui-config-hotreload.js @@ -0,0 +1,9 @@ +function UIConfig () { + return { + menu: [ + { + label: "About Jaeger" + } + ] + } +} diff --git a/cmd/query/app/fixture/ui-config-malformed.js b/cmd/query/app/fixture/ui-config-malformed.js new file mode 100644 index 00000000000..1956f1b31e6 --- /dev/null +++ b/cmd/query/app/fixture/ui-config-malformed.js @@ -0,0 +1,10 @@ +() => { + return { + menu: [ + { + label: "GitHub", + url: "https://github.com/jaegertracing/jaeger" + } + ] + } +} diff --git a/cmd/query/app/fixture/ui-config-menu.js b/cmd/query/app/fixture/ui-config-menu.js new file mode 100644 index 00000000000..0312c7bc5fa --- /dev/null +++ b/cmd/query/app/fixture/ui-config-menu.js @@ -0,0 +1,10 @@ +function UIConfig() { + return { + menu: [ + { + label: "GitHub", + url: "https://github.com/jaegertracing/jaeger" + } + ] + } +} diff --git a/cmd/query/app/fixture/ui-config.js b/cmd/query/app/fixture/ui-config.js new file mode 100644 index 00000000000..0200133563b --- /dev/null +++ b/cmd/query/app/fixture/ui-config.js @@ -0,0 +1,5 @@ +function UIConfig() { + return { + x: "y" + } +} diff --git a/cmd/query/app/static_handler.go b/cmd/query/app/static_handler.go index 5f8e9709bd6..beb62fb8473 100644 --- a/cmd/query/app/static_handler.go +++ b/cmd/query/app/static_handler.go @@ -16,6 +16,7 @@ package app import ( + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -106,14 +107,10 @@ func loadAndEnrichIndexHTML(open func(string) (http.File, error), options Static } // replace UI config configString := "JAEGER_CONFIG = DEFAULT_CONFIG" - if config, err := loadUIConfig(options.UIConfigPath); err != nil { + if configBytes, 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 configBytes != nil { + configString = fmt.Sprintf("JAEGER_CONFIG = %v", string(configBytes)) } indexBytes = configPattern.ReplaceAll(indexBytes, []byte(configString+";")) // replace Jaeger version @@ -210,30 +207,45 @@ 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) ([]byte, error) { if uiConfig == "" { return nil, nil } ext := filepath.Ext(uiConfig) - 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 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")) { + return nil, fmt.Errorf("wrong JS function format in UI config file format %v", uiConfig) + } default: return nil, fmt.Errorf("unrecognized UI config file format %v", uiConfig) } - 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. + if r, err = json.Marshal(c); err != nil { + return nil, fmt.Errorf("cannot encode UI config file %v: %w", uiConfig, err) + } } - return c, nil + + return r, nil } // RegisterRoutes registers routes for this handler on the given router diff --git a/cmd/query/app/static_handler_test.go b/cmd/query/app/static_handler_test.go index 97bdc50cd15..3d319f8ec64 100644 --- a/cmd/query/app/static_handler_test.go +++ b/cmd/query/app/static_handler_test.go @@ -16,6 +16,7 @@ package app import ( + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -117,54 +118,61 @@ func TestNewStaticAssetsHandlerErrors(t *testing.T) { // This test is potentially intermittent func TestHotReloadUIConfigTempFile(t *testing.T) { - 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("json hot reload", "js") } func TestLoadUIConfig(t *testing.T) { type testCase struct { configFile string - expected map[string]interface{} + expected []byte expectedError string } @@ -181,7 +189,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 +203,45 @@ func TestLoadUIConfig(t *testing.T) { }) run("json", testCase{ configFile: "fixture/ui-config.json", - expected: map[string]interface{}{"x": "y"}, + expected: []byte(`{"x":"y"}`), }) - 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", - }, + 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: c, + }) + 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: []byte(`function UIConfig() { + return { + x: "y" + } +}`), + }) + run("js-menu", testCase{ + configFile: "fixture/ui-config-menu.js", + expected: []byte(`function UIConfig() { + return { + menu: [ + { + label: "GitHub", + url: "https://github.com/jaegertracing/jaeger" + } + ] + } +}`), + }) } type fakeFile struct {