Skip to content

Commit 1aa6fbb

Browse files
committed
Added support for action suggest and validate
1 parent 887c802 commit 1aa6fbb

File tree

7 files changed

+447
-97
lines changed

7 files changed

+447
-97
lines changed

internal/app/action/action.go

Lines changed: 162 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"html/template"
1111
"io"
1212
"io/fs"
13+
"maps"
1314
"net/http"
1415
"net/url"
1516
"os"
@@ -61,12 +62,13 @@ type Action struct {
6162
containerProxyUrl string
6263
hidden map[string]bool // params which are not shown in the UI
6364
Links []ActionLink // links to other actions
65+
showValidate bool
6466
}
6567

6668
// NewAction creates a new action
6769
func NewAction(logger *types.Logger, sourceFS *appfs.SourceFs, isDev bool, name, description, apath string, run, suggest starlark.Callable,
6870
params []apptype.AppParam, paramValuesStr map[string]string, paramDict starlark.StringDict,
69-
appPath string, styleType types.StyleType, containerProxyUrl string, hidden []string) (*Action, error) {
71+
appPath string, styleType types.StyleType, containerProxyUrl string, hidden []string, showValidate bool) (*Action, error) {
7072

7173
funcMap := system.GetFuncMap()
7274

@@ -126,6 +128,7 @@ func NewAction(logger *types.Logger, sourceFS *appfs.SourceFs, isDev bool, name,
126128
StyleType: styleType,
127129
containerProxyUrl: containerProxyUrl,
128130
hidden: hiddenParams,
131+
showValidate: showValidate,
129132
// Links, AppTemplate and Theme names are initialized later
130133
}, nil
131134
}
@@ -158,14 +161,33 @@ func GetEmbeddedTemplates() (map[string][]byte, error) {
158161

159162
func (a *Action) BuildRouter() (*chi.Mux, error) {
160163
r := chi.NewRouter()
161-
r.Post("/", a.runAction)
162164
r.Get("/", a.getForm)
165+
r.Post("/", a.runAction)
166+
r.Post("/suggest", a.suggestAction)
167+
r.Post("/validate", a.validateAction)
163168

164169
r.Handle("/astatic/*", http.StripPrefix(path.Join(a.pagePath), hashfs.FileServer(embedFS)))
165170
return r, nil
166171
}
167172

168173
func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
174+
a.execAction(w, r, false, false)
175+
}
176+
177+
func (a *Action) suggestAction(w http.ResponseWriter, r *http.Request) {
178+
a.execAction(w, r, true, false)
179+
}
180+
181+
func (a *Action) validateAction(w http.ResponseWriter, r *http.Request) {
182+
a.execAction(w, r, false, true)
183+
}
184+
185+
func (a *Action) execAction(w http.ResponseWriter, r *http.Request, isSuggest, isValidate bool) {
186+
if isSuggest && a.suggest == nil {
187+
http.Error(w, "suggest not supported for this action", http.StatusNotImplemented)
188+
return
189+
}
190+
169191
thread := &starlark.Thread{
170192
Name: a.name,
171193
Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
@@ -180,16 +202,6 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
180202

181203
r.ParseMultipartForm(10 << 20) // 10 MB max file size
182204
var err error
183-
dryRun := false
184-
dryRunStr := r.Form.Get("dry-run")
185-
if dryRunStr != "" {
186-
dryRun, err = strconv.ParseBool(dryRunStr)
187-
if err != nil {
188-
http.Error(w, fmt.Sprintf("invalid value for dry-run: %s", dryRunStr), http.StatusBadRequest)
189-
return
190-
}
191-
}
192-
193205
deferredCleanup := func() error {
194206
// Check for any deferred cleanups
195207
err = RunDeferredCleanup(thread)
@@ -281,9 +293,16 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
281293

282294
argsValue := Args{members: args}
283295

296+
callable := a.run
297+
callInput := starlark.Tuple{starlark.Bool(isValidate), &argsValue}
298+
if isSuggest {
299+
callable = a.suggest
300+
callInput = starlark.Tuple{&argsValue}
301+
}
302+
284303
// Call the handler function
285304
var ret starlark.Value
286-
ret, err = starlark.Call(thread, a.run, starlark.Tuple{starlark.Bool(dryRun), &argsValue}, nil)
305+
ret, err = starlark.Call(thread, callable, callInput, nil)
287306

288307
if err == nil {
289308
pluginErrLocal := thread.Local(types.TL_PLUGIN_API_FAILED_ERROR)
@@ -313,11 +332,16 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
313332
msg = msg + " : " + firstFrame
314333
}
315334

316-
// No err handler defined, abort
335+
// err handler is not supported for actions
317336
http.Error(w, msg, http.StatusInternalServerError)
318337
return
319338
}
320339

340+
if isSuggest {
341+
a.handleSuggestResponse(w, ret)
342+
return
343+
}
344+
321345
var valuesMap []map[string]any
322346
var valuesStr []string
323347
var status string
@@ -423,6 +447,11 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
423447
}
424448
}
425449

450+
if isValidate {
451+
// No need to render the results
452+
return
453+
}
454+
426455
err = a.renderResults(w, report, valuesMap, valuesStr)
427456
if err != nil {
428457
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -721,9 +750,128 @@ func (a *Action) getForm(w http.ResponseWriter, r *http.Request) {
721750
"darkTheme": a.DarkTheme,
722751
"links": linksWithQS,
723752
"hasFileUpload": hasFileUpload,
753+
"showSuggest": a.suggest != nil,
754+
"showValidate": a.showValidate,
724755
}
725756
err := a.actionTemplate.ExecuteTemplate(w, "form.go.html", input)
726757
if err != nil {
727758
http.Error(w, err.Error(), http.StatusInternalServerError)
728759
}
729760
}
761+
762+
func (a *Action) handleSuggestResponse(w http.ResponseWriter, retVal starlark.Value) {
763+
ret, err := starlark_type.UnmarshalStarlark(retVal)
764+
if err != nil {
765+
http.Error(w, fmt.Sprintf("error unmarshalling suggest response: %s", err), http.StatusInternalServerError)
766+
return
767+
}
768+
769+
message, retIsString := ret.(string)
770+
if !retIsString {
771+
message = "Suggesting values"
772+
}
773+
774+
err = a.actionTemplate.ExecuteTemplate(w, "status", message)
775+
if err != nil {
776+
http.Error(w, err.Error(), http.StatusInternalServerError)
777+
return
778+
}
779+
780+
if retIsString {
781+
// No suggestions available
782+
return
783+
}
784+
785+
retDict := map[string]any{}
786+
switch retType := ret.(type) {
787+
case map[string]any:
788+
for k, v := range retType {
789+
retDict[k] = v
790+
}
791+
case map[string]string:
792+
for k, v := range retType {
793+
retDict[k] = v
794+
}
795+
case map[string]int:
796+
for k, v := range retType {
797+
retDict[k] = v
798+
}
799+
case map[string]bool:
800+
for k, v := range retType {
801+
retDict[k] = v
802+
}
803+
case map[string][]string:
804+
for k, v := range retType {
805+
retDict[k] = v
806+
}
807+
default:
808+
http.Error(w, fmt.Sprintf("invalid suggest response type: %T, expected dict", retType), http.StatusInternalServerError)
809+
return
810+
}
811+
812+
paramMap := map[string]apptype.AppParam{}
813+
for _, p := range a.params {
814+
paramMap[p.Name] = p
815+
}
816+
817+
keys := slices.Collect(maps.Keys(retDict))
818+
slices.Sort(keys)
819+
for _, key := range keys {
820+
value := retDict[key]
821+
p, ok := paramMap[key]
822+
if !ok || strings.HasPrefix(key, OPTIONS_PREFIX) {
823+
a.Info().Msgf("ignoring suggest response for param: %s", key)
824+
continue
825+
}
826+
param := ParamDef{
827+
Name: p.Name,
828+
Description: p.Description,
829+
}
830+
831+
param.Value = fmt.Sprintf("%v", value)
832+
param.InputType = "text"
833+
834+
valueList, valueIsList := value.([]string)
835+
if p.DisplayType == apptype.DisplayTypeFileUpload {
836+
http.Error(w, fmt.Sprintf("suggest not supported for file upload param: %s", p.Name), http.StatusInternalServerError)
837+
return
838+
} else if p.Type == starlark_type.STRING && valueIsList {
839+
param.InputType = "select"
840+
param.Value = valueList[0]
841+
param.Options = valueList
842+
} else if p.Type == starlark_type.BOOLEAN {
843+
boolValue, err := strconv.ParseBool(fmt.Sprintf("%v", value))
844+
if err != nil {
845+
http.Error(w, fmt.Sprintf("invalid value for %s: %s", p.Name, value), http.StatusInternalServerError)
846+
return
847+
}
848+
if boolValue {
849+
param.Value = "checked"
850+
}
851+
param.InputType = "checkbox"
852+
}
853+
854+
if p.DisplayType != "" {
855+
switch p.DisplayType {
856+
case apptype.DisplayTypePassword:
857+
param.DisplayType = "password"
858+
case apptype.DisplayTypeTextArea:
859+
param.DisplayType = "textarea"
860+
case apptype.DisplayTypeFileUpload:
861+
param.DisplayType = "file"
862+
default:
863+
http.Error(w, fmt.Sprintf("invalid display type for %s: %s", p.Name, p.DisplayType), http.StatusInternalServerError)
864+
return
865+
}
866+
param.DisplayTypeOptions = p.DisplayTypeOptions
867+
} else {
868+
param.DisplayType = "text"
869+
}
870+
871+
err = a.actionTemplate.ExecuteTemplate(w, "param_suggest", param)
872+
if err != nil {
873+
http.Error(w, err.Error(), http.StatusInternalServerError)
874+
return
875+
}
876+
}
877+
}

0 commit comments

Comments
 (0)