From 49a51b0a012adc1336c10d3a637d23e21d6cac60 Mon Sep 17 00:00:00 2001 From: Jack Dockerty Date: Tue, 14 May 2024 16:51:31 +0100 Subject: [PATCH 1/7] feat: add format functionality --- internal/server/state/state.go | 10 ++++++++++ internal/server/state/state_test.go | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/internal/server/state/state.go b/internal/server/state/state.go index b72d184..6e8b2c1 100644 --- a/internal/server/state/state.go +++ b/internal/server/state/state.go @@ -6,6 +6,7 @@ import ( "hash" "github.com/google/go-jsonnet" + "github.com/google/go-jsonnet/formatter" "github.com/kubecfg/kubecfg/pkg/kubecfg" ) @@ -44,6 +45,15 @@ func (s *State) EvaluateSnippet(snippet string) (string, error) { return evaluated, nil } +func (s *State) FormatSnippet(snippet string) (string, error) { + opts := formatter.DefaultOptions() + output, err := formatter.Format(PlaygroundFile, snippet, opts) + if err != nil { + return "", nil + } + return output, nil +} + // Config contains server configuration type Config struct { ShareDomain string diff --git a/internal/server/state/state_test.go b/internal/server/state/state_test.go index 2877c64..bc539fc 100644 --- a/internal/server/state/state_test.go +++ b/internal/server/state/state_test.go @@ -28,3 +28,10 @@ func TestEvaluateKubecfg(t *testing.T) { eval, _ := s.EvaluateSnippet(string(f)) assert.Equal(t, string(expected), eval) } + +func TestFormat(t *testing.T) { + s := state.New("") + + eval, _ := s.FormatSnippet(`{hello:"world"}`) + assert.Equal(t, eval, "{ hello: 'world' }\n") +} From fe6d828d9095190c1d36df30fc0335d7b770cce1 Mon Sep 17 00:00:00 2001 From: Jack Dockerty Date: Tue, 14 May 2024 17:28:43 +0100 Subject: [PATCH 2/7] feat: add format route --- internal/server/routes/backend.go | 28 ++++++++++++++++++++++++++++ internal/server/server.go | 1 + 2 files changed, 29 insertions(+) diff --git a/internal/server/routes/backend.go b/internal/server/routes/backend.go index e9b70cb..415e4c3 100644 --- a/internal/server/routes/backend.go +++ b/internal/server/routes/backend.go @@ -103,3 +103,31 @@ func HandleGetShare(state *state.State) http.HandlerFunc { w.Write([]byte(snippet)) } } + +// Format the input Jsonnet according to the standard jsonnetfmt rules. +func HandleFormat(state *state.State) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "must be GET", 400) + return + } + log.Println("Formatting snippet") + + err := r.ParseForm() + if err != nil { + http.Error(w, "unable to parse form", 400) + return + } + + incomingJsonnet := r.FormValue("jsonnet-input") + log.Println("Incoming:", incomingJsonnet) + formattedJsonnet, err := state.FormatSnippet(incomingJsonnet) + if err != nil { + http.Error(w, "unable to format jsonnet", 400) + return + } + log.Println("Formatted:", formattedJsonnet) + w.Write([]byte(formattedJsonnet)) + return + } +} diff --git a/internal/server/server.go b/internal/server/server.go index adb92f6..47c67f3 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -34,6 +34,7 @@ func (srv *PlaygroundServer) Routes() error { // Backend/API routes http.HandleFunc("/api/health", routes.Health()) http.HandleFunc("/api/run", routes.HandleRun(srv.State)) + http.HandleFunc("/api/format", routes.HandleFormat(srv.State)) http.HandleFunc("/api/share", routes.HandleCreateShare(srv.State)) http.HandleFunc("/api/share/{shareHash}", routes.HandleGetShare(srv.State)) return nil From 08cf15091d7a970e52c3220c1b92bb9fa455b8c5 Mon Sep 17 00:00:00 2001 From: Jack Dockerty Date: Tue, 14 May 2024 17:29:52 +0100 Subject: [PATCH 3/7] feat: add share button --- internal/components/components.templ | 7 +++++++ internal/components/components_templ.go | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/components/components.templ b/internal/components/components.templ index 2ccf054..03f65d2 100644 --- a/internal/components/components.templ +++ b/internal/components/components.templ @@ -89,6 +89,13 @@ templ jsonnetDisplay(sharedHash string) { > Share + diff --git a/internal/components/components_templ.go b/internal/components/components_templ.go index 368a651..029a92a 100644 --- a/internal/components/components_templ.go +++ b/internal/components/components_templ.go @@ -219,7 +219,7 @@ func jsonnetDisplay(sharedHash string) templ.Component { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From 9d85597b711d7bed19b652ef05268ca52c30849c Mon Sep 17 00:00:00 2001 From: Jack Dockerty Date: Tue, 14 May 2024 22:55:45 +0100 Subject: [PATCH 4/7] feat: replace textarea with formatted jsonnet --- internal/components/components.templ | 27 ++++++++++-- internal/components/components_templ.go | 55 ++++++++++++++++++++++--- internal/server/state/state.go | 7 +++- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/internal/components/components.templ b/internal/components/components.templ index 03f65d2..38ab6c5 100644 --- a/internal/components/components.templ +++ b/internal/components/components.templ @@ -41,6 +41,26 @@ templ title() { } +// Hacky function to replace the textarea with formatted Jsonnet. +// +// TODO(jdockerty): there may be a nicer way to do this with htmx, but disabling the +// functionality of the hx-post and replacing the textarea value did not work +// in the same way as the other htmx swaps, it had very odd behaviour instead. +script handleFormat() { + var textarea = document.getElementById('jsonnet-input'); + var data = new FormData(); + data.append("jsonnet-input", textarea.value); + fetch("/api/format", { + // Using URLSearchParams means we send the expected www-form-url-encoded data. + // https://developer.mozilla.org/en-US/docs/Web/API/FormData + body: new URLSearchParams(data), + method: 'POST' + }).then(async (x) => { + textarea.value = await x.text(); + htmx.process(document.body); + }); +} + // Allow tab/shift-tab for (de)indentation within the input textarea. script allowTabs() { var textarea = document.getElementById('jsonnet-input'); @@ -71,7 +91,7 @@ templ jsonnetDisplay(sharedHash string) { if sharedHash != "" { hx-get={ fmt.Sprintf("/api/share/%s", sharedHash) } hx-trigger="load" } else { - autofocus id="jsonnet-input" placeholder="Type your Jsonnet here..." + autofocus placeholder="Type your Jsonnet here..." } > @@ -90,9 +110,8 @@ templ jsonnetDisplay(sharedHash string) { Share diff --git a/internal/components/components_templ.go b/internal/components/components_templ.go index 029a92a..e3eed03 100644 --- a/internal/components/components_templ.go +++ b/internal/components/components_templ.go @@ -132,6 +132,32 @@ func title() templ.Component { }) } +// Hacky function to replace the textarea with formatted Jsonnet. +// +// TODO(jdockerty): there may be a nicer way to do this with htmx, but disabling the +// functionality of the hx-post and replacing the textarea value did not work +// in the same way as the other htmx swaps, it had very odd behaviour instead. +func handleFormat() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_handleFormat_ee7f`, + Function: `function __templ_handleFormat_ee7f(){var textarea = document.getElementById('jsonnet-input'); + var data = new FormData(); + data.append("jsonnet-input", textarea.value); + fetch("/api/format", { + // Using URLSearchParams means we send the expected www-form-url-encoded data. + // https://developer.mozilla.org/en-US/docs/Web/API/FormData + body: new URLSearchParams(data), + method: 'POST' + }).then(async (x) => { + textarea.value = await x.text(); + htmx.process(document.body); + }); +}`, + Call: templ.SafeScript(`__templ_handleFormat_ee7f`), + CallInline: templ.SafeScriptInline(`__templ_handleFormat_ee7f`), + } +} + // Allow tab/shift-tab for (de)indentation within the input textarea. func allowTabs() templ.ComponentScript { return templ.ComponentScript{ @@ -203,7 +229,7 @@ func jsonnetDisplay(sharedHash string) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/api/share/%s", sharedHash)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/components.templ`, Line: 72, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/components.templ`, Line: 92, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -214,12 +240,29 @@ func jsonnetDisplay(sharedHash string) templ.Component { return templ_7745c5c3_Err } } else { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" autofocus id=\"jsonnet-input\" placeholder=\"Type your Jsonnet here...\"") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" autofocus placeholder=\"Type your Jsonnet here...\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, handleFormat()) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -238,9 +281,9 @@ func RootPage() templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var8 := templ.GetChildren(ctx) - if templ_7745c5c3_Var8 == nil { - templ_7745c5c3_Var8 = templ.NopComponent + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") diff --git a/internal/server/state/state.go b/internal/server/state/state.go index 6e8b2c1..a32b9b2 100644 --- a/internal/server/state/state.go +++ b/internal/server/state/state.go @@ -46,10 +46,15 @@ func (s *State) EvaluateSnippet(snippet string) (string, error) { } func (s *State) FormatSnippet(snippet string) (string, error) { + _, err := s.EvaluateSnippet(snippet) + if err != nil { + return "", err + } + opts := formatter.DefaultOptions() output, err := formatter.Format(PlaygroundFile, snippet, opts) if err != nil { - return "", nil + return "", err } return output, nil } From d4216f6f01ee387d762e372c3f1c5f830f26e5f5 Mon Sep 17 00:00:00 2001 From: Jack Dockerty Date: Tue, 14 May 2024 22:57:30 +0100 Subject: [PATCH 5/7] chore: add todo --- internal/components/components.templ | 1 + internal/components/components_templ.go | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/components/components.templ b/internal/components/components.templ index 38ab6c5..ce59198 100644 --- a/internal/components/components.templ +++ b/internal/components/components.templ @@ -56,6 +56,7 @@ script handleFormat() { body: new URLSearchParams(data), method: 'POST' }).then(async (x) => { + // TODO: handle error into output box textarea.value = await x.text(); htmx.process(document.body); }); diff --git a/internal/components/components_templ.go b/internal/components/components_templ.go index e3eed03..94d595b 100644 --- a/internal/components/components_templ.go +++ b/internal/components/components_templ.go @@ -139,8 +139,8 @@ func title() templ.Component { // in the same way as the other htmx swaps, it had very odd behaviour instead. func handleFormat() templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_handleFormat_ee7f`, - Function: `function __templ_handleFormat_ee7f(){var textarea = document.getElementById('jsonnet-input'); + Name: `__templ_handleFormat_8779`, + Function: `function __templ_handleFormat_8779(){var textarea = document.getElementById('jsonnet-input'); var data = new FormData(); data.append("jsonnet-input", textarea.value); fetch("/api/format", { @@ -149,12 +149,13 @@ func handleFormat() templ.ComponentScript { body: new URLSearchParams(data), method: 'POST' }).then(async (x) => { + // TODO: handle error into output box textarea.value = await x.text(); htmx.process(document.body); }); }`, - Call: templ.SafeScript(`__templ_handleFormat_ee7f`), - CallInline: templ.SafeScriptInline(`__templ_handleFormat_ee7f`), + Call: templ.SafeScript(`__templ_handleFormat_8779`), + CallInline: templ.SafeScriptInline(`__templ_handleFormat_8779`), } } @@ -229,7 +230,7 @@ func jsonnetDisplay(sharedHash string) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/api/share/%s", sharedHash)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/components.templ`, Line: 92, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/components.templ`, Line: 93, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { From 1c32f29801902cf7224e660d3f2908f58f0a196e Mon Sep 17 00:00:00 2001 From: Jack Dockerty Date: Wed, 15 May 2024 09:35:13 +0100 Subject: [PATCH 6/7] feat: basic js error handling for server response --- internal/components/components.templ | 14 +++++++++----- internal/components/components_templ.go | 24 ++++++++++++++---------- internal/server/routes/backend.go | 2 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/internal/components/components.templ b/internal/components/components.templ index ce59198..7675d32 100644 --- a/internal/components/components.templ +++ b/internal/components/components.templ @@ -47,17 +47,21 @@ templ title() { // functionality of the hx-post and replacing the textarea value did not work // in the same way as the other htmx swaps, it had very odd behaviour instead. script handleFormat() { - var textarea = document.getElementById('jsonnet-input'); var data = new FormData(); - data.append("jsonnet-input", textarea.value); + data.append("jsonnet-input", jsonnetInput.value); fetch("/api/format", { // Using URLSearchParams means we send the expected www-form-url-encoded data. // https://developer.mozilla.org/en-US/docs/Web/API/FormData body: new URLSearchParams(data), method: 'POST' - }).then(async (x) => { - // TODO: handle error into output box - textarea.value = await x.text(); + }).then(async (resp) => { + if (resp.status === 400) { + // Use the same area for share errors to avoid issues with interacting + // with the regular jsonnet output box. + document.getElementById('share-output').innerText = await resp.text(); + } else { + document.getElementById('jsonnet-input').value = await resp.text(); + } htmx.process(document.body); }); } diff --git a/internal/components/components_templ.go b/internal/components/components_templ.go index 94d595b..6a128da 100644 --- a/internal/components/components_templ.go +++ b/internal/components/components_templ.go @@ -139,23 +139,27 @@ func title() templ.Component { // in the same way as the other htmx swaps, it had very odd behaviour instead. func handleFormat() templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_handleFormat_8779`, - Function: `function __templ_handleFormat_8779(){var textarea = document.getElementById('jsonnet-input'); - var data = new FormData(); - data.append("jsonnet-input", textarea.value); + Name: `__templ_handleFormat_31cd`, + Function: `function __templ_handleFormat_31cd(){var data = new FormData(); + data.append("jsonnet-input", jsonnetInput.value); fetch("/api/format", { // Using URLSearchParams means we send the expected www-form-url-encoded data. // https://developer.mozilla.org/en-US/docs/Web/API/FormData body: new URLSearchParams(data), method: 'POST' - }).then(async (x) => { - // TODO: handle error into output box - textarea.value = await x.text(); + }).then(async (resp) => { + if (resp.status === 400) { + // Use the same area for share errors to avoid issues with interacting + // with the regular jsonnet output box. + document.getElementById('share-output').innerText = await resp.text(); + } else { + document.getElementById('jsonnet-input').value = await resp.text(); + } htmx.process(document.body); }); }`, - Call: templ.SafeScript(`__templ_handleFormat_8779`), - CallInline: templ.SafeScriptInline(`__templ_handleFormat_8779`), + Call: templ.SafeScript(`__templ_handleFormat_31cd`), + CallInline: templ.SafeScriptInline(`__templ_handleFormat_31cd`), } } @@ -230,7 +234,7 @@ func jsonnetDisplay(sharedHash string) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/api/share/%s", sharedHash)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/components.templ`, Line: 93, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/components.templ`, Line: 97, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { diff --git a/internal/server/routes/backend.go b/internal/server/routes/backend.go index 415e4c3..4d9e4ea 100644 --- a/internal/server/routes/backend.go +++ b/internal/server/routes/backend.go @@ -123,7 +123,7 @@ func HandleFormat(state *state.State) http.HandlerFunc { log.Println("Incoming:", incomingJsonnet) formattedJsonnet, err := state.FormatSnippet(incomingJsonnet) if err != nil { - http.Error(w, "unable to format jsonnet", 400) + http.Error(w, "Format is not available for invalid Jsonnet. Run your snippet to see the result.", 400) return } log.Println("Formatted:", formattedJsonnet) From 25497ec2373c72d94dd95bc5fa99cb2c62cecfbc Mon Sep 17 00:00:00 2001 From: Jack Dockerty Date: Wed, 15 May 2024 09:40:11 +0100 Subject: [PATCH 7/7] fix: err msg from HandleFormat --- internal/server/routes/backend.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/server/routes/backend.go b/internal/server/routes/backend.go index 4d9e4ea..09fbc13 100644 --- a/internal/server/routes/backend.go +++ b/internal/server/routes/backend.go @@ -108,7 +108,7 @@ func HandleGetShare(state *state.State) http.HandlerFunc { func HandleFormat(state *state.State) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Error(w, "must be GET", 400) + http.Error(w, "must be POST", 400) return } log.Println("Formatting snippet")