Skip to content

Commit

Permalink
Add ability to use JS file for UI configuration (#123 from jaeger-ui)
Browse files Browse the repository at this point in the history
Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
  • Loading branch information
th3M1ke committed Dec 29, 2020
1 parent 1b20947 commit 85cc44b
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 55 deletions.
9 changes: 9 additions & 0 deletions cmd/query/app/fixture/ui-config-hotreload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function UIConfig () {
return {
menu: [
{
label: "About Jaeger"
}
]
}
}
10 changes: 10 additions & 0 deletions cmd/query/app/fixture/ui-config-malformed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
() => {
return {
menu: [
{
label: "GitHub",
url: "https://github.com/jaegertracing/jaeger"
}
]
}
}
10 changes: 10 additions & 0 deletions cmd/query/app/fixture/ui-config-menu.js
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"
}
]
}
}
5 changes: 5 additions & 0 deletions cmd/query/app/fixture/ui-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function UIConfig() {
return {
x: "y"
}
}
36 changes: 24 additions & 12 deletions cmd/query/app/static_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package app

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
120 changes: 77 additions & 43 deletions cmd/query/app/static_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package app

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -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
}

Expand All @@ -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",
})
Expand All @@ -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 {
Expand Down

0 comments on commit 85cc44b

Please sign in to comment.