diff --git a/api/renderers.go b/api/renderers.go new file mode 100644 index 000000000..132657c8f --- /dev/null +++ b/api/renderers.go @@ -0,0 +1,27 @@ +package api + +import "fmt" + +type Renderers struct { + Components []RenderComponent `json:"components,omitempty"` + Properties []RenderComponent `json:"properties,omitempty"` +} + +type RenderComponent struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + JSX string `json:"jsx,omitempty"` +} + +func (c *RenderComponent) Key(isProp bool) string { + prefix := "component" + if isProp { + prefix = "property" + } + + if c.Type != "" { + return fmt.Sprintf("%s_%s_%s", prefix, c.Type, c.Name) + } + + return fmt.Sprintf("%s_%s", prefix, c.Name) +} diff --git a/cmd/server.go b/cmd/server.go index 2fc20e91e..2ecaabb06 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -18,6 +18,7 @@ import ( "github.com/flanksource/incident-commander/events" "github.com/flanksource/incident-commander/jobs" "github.com/flanksource/incident-commander/snapshot" + "github.com/flanksource/incident-commander/topology" "github.com/flanksource/incident-commander/utils" ) @@ -69,6 +70,8 @@ var Serve = &cobra.Command{ e.GET("/snapshot/incident/:id", snapshot.Incident) e.GET("/snapshot/config/:id", snapshot.Config) + e.GET("/custom_renderer", topology.GetCustomRenderer) + // Serve openapi schemas schemaServer, err := utils.HTTPFileserver(openapi.Schemas) if err != nil { diff --git a/go.mod b/go.mod index a5ff59706..b7982dfba 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/cel-go v0.13.0 github.com/google/uuid v1.3.0 github.com/jackc/pgx/v4 v4.18.0 + github.com/jvatic/goja-babel v0.0.0-20230204121733-ac82a55cfa50 github.com/labstack/echo/v4 v4.7.2 github.com/microsoft/kiota-authentication-azure-go v0.6.0 github.com/microsoftgraph/msgraph-sdk-go v0.54.0 @@ -57,6 +58,8 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.17.7 // indirect github.com/aws/smithy-go v1.13.5 // indirect github.com/cloudflare/circl v1.3.1 // indirect + github.com/dlclark/regexp2 v1.8.0 // indirect + github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/flanksource/gomplate/v3 v3.20.1 // indirect github.com/go-errors/errors v1.0.1 // indirect @@ -65,6 +68,7 @@ require ( github.com/go-git/go-git/v5 v5.5.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/google/gnostic v0.6.9 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect @@ -187,7 +191,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect - github.com/microsoft/kiota-abstractions-go v0.17.0 // indirect + github.com/microsoft/kiota-abstractions-go v0.17.0 github.com/microsoft/kiota-http-go v0.14.0 // indirect github.com/microsoft/kiota-serialization-json-go v0.8.0 // indirect github.com/microsoft/kiota-serialization-text-go v0.7.0 // indirect diff --git a/go.sum b/go.sum index 52eef62b7..504d9a42e 100644 --- a/go.sum +++ b/go.sum @@ -535,6 +535,10 @@ github.com/dgryski/go-sip13 v0.0.0-20200911182023-62edffca9245/go.mod h1:vAd38F8 github.com/digitalocean/godo v1.78.0/go.mod h1:GBmu8MkjZmNARE7IXRPmkbbnocNN8+uBm0xbEVw2LCs= github.com/digitalocean/godo v1.81.0/go.mod h1:BPCqvwbjbGqxuUnIKB4EvS/AX7IDnNmt5fwvIkWo+ew= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.8.0 h1:rJD5HeGIT/2b5CDk63FVCwZA3qgYElfg+oQK7uH5pfE= +github.com/dlclark/regexp2 v1.8.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= @@ -556,6 +560,11 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32 h1:audXtK7nV3y4W9ckAxRBE+eQV5Bljf5Non4NTa9kLVE= +github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -717,6 +726,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -1132,6 +1143,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jvatic/goja-babel v0.0.0-20230204121733-ac82a55cfa50 h1:ewpo/HSvn4zCct4LvULM3Oc1PB9gDUR2f5RzgONOVIY= +github.com/jvatic/goja-babel v0.0.0-20230204121733-ac82a55cfa50/go.mod h1:e6baxuoF3V4g/q0HMjzOtziMK//6Ui0RjfwbFgPQWB4= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -1605,6 +1618,8 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b h1:GlTM/aMVIwU3luIuSN2SIVRuTqGPt1P97YxAi514ulw= +github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b/go.mod h1:CC7OXV9IjEZRA+znA6/Kz5vbSwh69QioernOHeDCatU= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -2192,6 +2207,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/topology/controllers.go b/topology/controllers.go new file mode 100644 index 000000000..54b24c1a7 --- /dev/null +++ b/topology/controllers.go @@ -0,0 +1,138 @@ +package topology + +import ( + "bytes" + "fmt" + "io" + "net/http" + "text/template" + "time" + + "github.com/flanksource/commons/logger" + "github.com/flanksource/incident-commander/api" + babel "github.com/jvatic/goja-babel" + "github.com/labstack/echo/v4" + "github.com/patrickmn/go-cache" +) + +var ( + jsComponentTpl *template.Template + templateCache *cache.Cache +) + +func init() { + tpl, err := template.New("registry").Parse(jsComponentRegistryTpl) + if err != nil { + logger.Fatalf("error parsing template 'jsComponentRegistryTpl'. %v", err) + } + jsComponentTpl = tpl + + templateCache = cache.New(time.Hour*24, time.Hour*12) + + if err := babel.Init(10); err != nil { + logger.Fatalf("failed to init babel: %v", err) + } +} + +type component struct { + Name string + JS string +} + +// GetCustomRenderer returns an application/javascript HTTP response +// with custom components and a registry. +// This registry needs to be used to select custom components +// for rendering of properties and cards. +func GetCustomRenderer(ctx echo.Context) error { + id := ctx.QueryParams().Get("id") + results, err := QueryRenderComponents(ctx.Request().Context(), id) + if err != nil { + return errorResponse(ctx, http.StatusBadRequest, err, "failed to query components by id") + } + + var components = make(map[string]component) + for _, r := range results { + if err := compileComponents(components, r.Components, false); err != nil { + return errorResponse(ctx, http.StatusInternalServerError, err, "failed to compile components") + } + + if err := compileComponents(components, r.Properties, true); err != nil { + return errorResponse(ctx, http.StatusInternalServerError, err, "failed to compile property components") + } + } + + registryResp, err := renderComponents(components) + if err != nil { + return errorResponse(ctx, http.StatusInternalServerError, err, "failed to render components") + } + + return ctx.Stream(http.StatusOK, "application/javascript", registryResp) +} + +func compileComponents(output map[string]component, components []api.RenderComponent, isProp bool) error { + if len(components) == 0 { + return nil + } + + for _, c := range components { + res, err := transformJSX(c.JSX) + if err != nil { + return fmt.Errorf("error transforming jsx: %w", err) + } + + output[c.Key(isProp)] = component{ + Name: c.Name, + JS: res, + } + } + + return nil +} + +// transformJSX transforms the provided jsx and also +// caches the result. +func transformJSX(jsx string) (string, error) { + if val, ok := templateCache.Get(jsx); ok { + return val.(string), nil + } + + res, err := babel.TransformString(jsx, map[string]any{ + "plugins": []string{ + "transform-react-jsx", + "transform-block-scoping", + }, + }) + if err != nil { + return "", fmt.Errorf("error transforming jsx: %w", err) + } + + templateCache.Set(jsx, res, cache.DefaultExpiration) + + return res, nil +} + +func renderComponents(components map[string]component) (io.Reader, error) { + var buf bytes.Buffer + if err := jsComponentTpl.Execute(&buf, components); err != nil { + return nil, fmt.Errorf("error generating components: %w", err) + } + + return &buf, nil +} + +const jsComponentRegistryTpl = ` +{{range $k, $v := .}} +const {{$k}} = {{$v.JS}} +{{end}} +const componentRegistry = { + {{range $k, $v := .}}"{{$k}}": {{$k}}, + {{end}} +}; +` + +func errorResponse(c echo.Context, code int, err error, msg string) error { + return c.JSON(code, api.HTTPErrorMessage{ + Error: err.Error(), + Message: msg, + }) +} diff --git a/topology/query.go b/topology/query.go new file mode 100644 index 000000000..49014b4f3 --- /dev/null +++ b/topology/query.go @@ -0,0 +1,34 @@ +package topology + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/flanksource/incident-commander/api" + "github.com/flanksource/incident-commander/db" +) + +func QueryRenderComponents(ctx context.Context, systemTemplateID string) ([]api.Renderers, error) { + rows, err := db.Gorm.WithContext(ctx).Table("templates").Select("spec->'renderers'").Where("id = ?", systemTemplateID).Rows() + if err != nil { + return nil, fmt.Errorf("failed to query renderers: %w", err) + } + defer rows.Close() + + var results []api.Renderers + for rows.Next() { + var renderers api.Renderers + var s string + if err := rows.Scan(&s); err != nil { + return nil, fmt.Errorf("error scanning row: %w", err) + } + + if err := json.Unmarshal([]byte(s), &renderers); err != nil { + return nil, fmt.Errorf("error unmarshalling to renderers: %w", err) + } + results = append(results, renderers) + } + + return results, nil +}