diff --git a/.dockerignore b/.dockerignore index 692ab6150..2c484c6a9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,6 @@ Dockerfile fasttrack fasttrack.db* -pkg/ui/*/build* -!pkg/ui/*/build.sh +pkg/ui/*/embed/build* +!pkg/ui/*/embed/build.sh tests/*/*.src diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49cd02a09..17b8a584f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,8 @@ jobs: - name: Mock UI builds run: | - mkdir pkg/ui/{aim,mlflow}/build - touch pkg/ui/{aim,mlflow}/build/index.html + mkdir pkg/ui/{aim,mlflow}/embed/build + touch pkg/ui/{aim,mlflow}/embed/build/index.html - name: Check with go vet run: go vet --tags "${{ steps.tags.outputs.tags }}" ./... @@ -74,8 +74,8 @@ jobs: - name: Mock UI builds run: | - mkdir pkg/ui/{aim,mlflow}/build - touch pkg/ui/{aim,mlflow}/build/index.html + mkdir pkg/ui/{aim,mlflow}/embed/build + touch pkg/ui/{aim,mlflow}/embed/build/index.html - name: Run MLFlow integration tests run: ./tests/mlflow/test.sh diff --git a/.gitignore b/.gitignore index 0b25d4a9d..4b3b6f4c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ fasttrack fasttrack.db* -pkg/ui/*/build* -!pkg/ui/*/build.sh +pkg/ui/*/embed/build* +!pkg/ui/*/embed/build.sh tests/*/*.src diff --git a/Dockerfile b/Dockerfile index e96414c99..211998f44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ # Build MLFlow UI FROM --platform=$BUILDPLATFORM node:16 AS mlflow-build -COPY pkg/ui/mlflow /mlflow +COPY pkg/ui/mlflow/embed /mlflow RUN /mlflow/build.sh # Build Aim UI FROM --platform=$BUILDPLATFORM node:16 AS aim-build -COPY pkg/ui/aim /aim +COPY pkg/ui/aim/embed /aim RUN /aim/build.sh # Build fasttrack binary @@ -18,8 +18,8 @@ COPY go.mod go.sum ./ RUN go mod download COPY main.go . COPY pkg ./pkg -COPY --from=mlflow-build /mlflow/build ./pkg/ui/mlflow/build -COPY --from=aim-build /aim/build ./pkg/ui/aim/build +COPY --from=mlflow-build /mlflow/build ./pkg/ui/mlflow/embed/build +COPY --from=aim-build /aim/build ./pkg/ui/aim/embed/build ARG TARGETARCH RUN bash -c "\ diff --git a/pkg/api/aim/app.go b/pkg/api/aim/app.go deleted file mode 100644 index 719d80fac..000000000 --- a/pkg/api/aim/app.go +++ /dev/null @@ -1,138 +0,0 @@ -package aim - -import ( - "errors" - "net/http" - - "github.com/G-Research/fasttrack/pkg/ui" - "github.com/G-Research/fasttrack/pkg/version" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/basicauth" - "github.com/gofiber/fiber/v2/middleware/etag" - "github.com/gofiber/fiber/v2/middleware/filesystem" - log "github.com/sirupsen/logrus" -) - -func NewApp(authUsername string, authPassword string) *fiber.App { - app := fiber.New(fiber.Config{ - ErrorHandler: func(c *fiber.Ctx, err error) error { - var e *ErrorResponse - var f *fiber.Error - var d DetailedError - - switch { - case errors.As(err, &e): - case errors.As(err, &f): - e = &ErrorResponse{ - Code: f.Code, - Message: f.Message, - Detail: "", - } - case errors.As(err, &d): - e = &ErrorResponse{ - Code: d.Code(), - Message: d.Message(), - Detail: d.Detail(), - } - default: - e = &ErrorResponse{ - Code: fiber.StatusInternalServerError, - Message: err.Error(), - Detail: "", - } - } - - fn := log.Errorf - - switch e.Code { - case fiber.StatusNotFound: - fn = log.Debugf - case fiber.StatusInternalServerError: - default: - fn = log.Warnf - } - - fn("Error encountered in %s %s: %s", c.Method(), c.Path(), err) - - return c.Status(e.Code).JSON(e) - }, - }) - - api := app.Group("/api") - - if authUsername != "" && authPassword != "" { - log.Infof(`BasicAuth enabled for modern UI with user "%s"`, authUsername) - api.Use(basicauth.New(basicauth.Config{ - Users: map[string]string{ - authUsername: authPassword, - }, - })) - } - - apps := api.Group("apps") - apps.Get("/", GetApps) - apps.Post("/", CreateApp) - apps.Get("/:id/", GetApp) - apps.Put("/:id/", UpdateApp) - apps.Delete("/:id/", DeleteApp) - - dashboards := api.Group("/dashboards") - dashboards.Get("/", GetDashboards) - dashboards.Post("/", CreateDashboard) - dashboards.Get("/:id/", GetDashboard) - dashboards.Put("/:id/", UpdateDashboard) - dashboards.Delete("/:id/", DeleteDashboard) - - experiments := api.Group("experiments") - experiments.Get("/", GetExperiments) - experiments.Get("/:id/", GetExperiment) - experiments.Get("/:id/activity/", GetExperimentActivity) - experiments.Get("/:id/runs/", GetExperimentRuns) - - projects := api.Group("/projects") - projects.Get("/", GetProject) - projects.Get("/activity/", GetProjectActivity) - projects.Get("/pinned-sequences/", GetProjectPinnedSequences) - projects.Post("/pinned-sequences/", UpdateProjectPinnedSequences) - projects.Get("/params/", GetProjectParams) - projects.Get("/status/", GetProjectStatus) - - runs := api.Group("/runs") - runs.Get("/active/", GetRunsActive) - runs.Get("/search/run/", GetRunsSearch) - runs.Get("/search/metric/", GetRunsMetricsSearch) - runs.Get("/:id/info/", GetRunInfo) - runs.Post("/:id/metric/get-batch/", GetRunMetricBatch) - - tags := api.Group("/tags") - tags.Get("/", GetTags) - - api.Get("/version", func(c *fiber.Ctx) error { - return c.JSON(fiber.Map{ - "version": version.Version, - }) - }) - - api.Use(func(c *fiber.Ctx) error { - return fiber.ErrNotFound - }) - - app.Use("/static-files/", etag.New(), filesystem.New(filesystem.Config{ - Root: http.FS(ui.AimFS), - })) - - app.Use(etag.New(), func(c *fiber.Ctx) error { - if c.Method() != fiber.MethodGet { - return fiber.ErrMethodNotAllowed - } - - file, _ := ui.AimFS.Open("index.html") - stat, _ := file.Stat() - c.Set("Content-Type", "text/html; charset=utf-8") - c.Response().SetBodyStream(file, int(stat.Size())) - return nil - }) - - return app -} diff --git a/pkg/api/aim/errors.go b/pkg/api/aim/errors.go new file mode 100644 index 000000000..f8e9b5987 --- /dev/null +++ b/pkg/api/aim/errors.go @@ -0,0 +1,50 @@ +package aim + +import ( + "errors" + + "github.com/gofiber/fiber/v2" + log "github.com/sirupsen/logrus" +) + +func ErrorHandler(c *fiber.Ctx, err error) error { + var e *ErrorResponse + var f *fiber.Error + var d DetailedError + + switch { + case errors.As(err, &e): + case errors.As(err, &f): + e = &ErrorResponse{ + Code: f.Code, + Message: f.Message, + Detail: "", + } + case errors.As(err, &d): + e = &ErrorResponse{ + Code: d.Code(), + Message: d.Message(), + Detail: d.Detail(), + } + default: + e = &ErrorResponse{ + Code: fiber.StatusInternalServerError, + Message: err.Error(), + Detail: "", + } + } + + fn := log.Errorf + + switch e.Code { + case fiber.StatusNotFound: + fn = log.Debugf + case fiber.StatusInternalServerError: + default: + fn = log.Warnf + } + + fn("Error encountered in %s %s: %s", c.Method(), c.Path(), err) + + return c.Status(e.Code).JSON(e) +} diff --git a/pkg/api/aim/routes.go b/pkg/api/aim/routes.go new file mode 100644 index 000000000..ca769d983 --- /dev/null +++ b/pkg/api/aim/routes.go @@ -0,0 +1,49 @@ +package aim + +import ( + "github.com/gofiber/fiber/v2" +) + +func AddRoutes(r fiber.Router) { + apps := r.Group("apps") + apps.Get("/", GetApps) + apps.Post("/", CreateApp) + apps.Get("/:id/", GetApp) + apps.Put("/:id/", UpdateApp) + apps.Delete("/:id/", DeleteApp) + + dashboards := r.Group("/dashboards") + dashboards.Get("/", GetDashboards) + dashboards.Post("/", CreateDashboard) + dashboards.Get("/:id/", GetDashboard) + dashboards.Put("/:id/", UpdateDashboard) + dashboards.Delete("/:id/", DeleteDashboard) + + experiments := r.Group("experiments") + experiments.Get("/", GetExperiments) + experiments.Get("/:id/", GetExperiment) + experiments.Get("/:id/activity/", GetExperimentActivity) + experiments.Get("/:id/runs/", GetExperimentRuns) + + projects := r.Group("/projects") + projects.Get("/", GetProject) + projects.Get("/activity/", GetProjectActivity) + projects.Get("/pinned-sequences/", GetProjectPinnedSequences) + projects.Post("/pinned-sequences/", UpdateProjectPinnedSequences) + projects.Get("/params/", GetProjectParams) + projects.Get("/status/", GetProjectStatus) + + runs := r.Group("/runs") + runs.Get("/active/", GetRunsActive) + runs.Get("/search/run/", GetRunsSearch) + runs.Get("/search/metric/", GetRunsMetricsSearch) + runs.Get("/:id/info/", GetRunInfo) + runs.Post("/:id/metric/get-batch/", GetRunMetricBatch) + + tags := r.Group("/tags") + tags.Get("/", GetTags) + + r.Use(func(c *fiber.Ctx) error { + return fiber.ErrNotFound + }) +} diff --git a/pkg/api/mlflow/app.go b/pkg/api/mlflow/app.go deleted file mode 100644 index 86866307b..000000000 --- a/pkg/api/mlflow/app.go +++ /dev/null @@ -1,143 +0,0 @@ -package mlflow - -import ( - "errors" - "net/http" - - "github.com/G-Research/fasttrack/pkg/ui" - "github.com/G-Research/fasttrack/pkg/version" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/basicauth" - "github.com/gofiber/fiber/v2/middleware/etag" - "github.com/gofiber/fiber/v2/middleware/filesystem" - log "github.com/sirupsen/logrus" -) - -func NewApp(authUsername string, authPassword string) *fiber.App { - api := fiber.New(fiber.Config{ - ErrorHandler: func(c *fiber.Ctx, err error) error { - var e *ErrorResponse - if !errors.As(err, &e) { - var code ErrorCode = ErrorCodeInternalError - - var f *fiber.Error - if errors.As(err, &f) { - switch f.Code { - case fiber.StatusBadRequest: - code = ErrorCodeBadRequest - case fiber.StatusServiceUnavailable: - code = ErrorCodeTemporarilyUnavailable - case fiber.StatusNotFound: - code = ErrorCodeEndpointNotFound - } - } - - e = &ErrorResponse{ - ErrorCode: code, - Message: err.Error(), - } - } - - var code int - var fn func(format string, args ...any) - - switch e.ErrorCode { - case ErrorCodeBadRequest, ErrorCodeInvalidParameterValue, ErrorCodeResourceAlreadyExists: - code = fiber.StatusBadRequest - fn = log.Infof - case ErrorCodeTemporarilyUnavailable: - code = fiber.StatusServiceUnavailable - fn = log.Warnf - case ErrorCodeEndpointNotFound, ErrorCodeResourceDoesNotExist: - code = fiber.StatusNotFound - fn = log.Debugf - default: - code = fiber.StatusInternalServerError - fn = log.Errorf - } - - fn("Error encountered in %s %s: %s", c.Method(), c.Path(), err) - - return c.Status(code).JSON(e) - }, - }) - - if authUsername != "" && authPassword != "" { - log.Infof(`BasicAuth enabled for classic UI with user "%s"`, authUsername) - api.Use(basicauth.New(basicauth.Config{ - Users: map[string]string{ - authUsername: authPassword, - }, - })) - } - - artifacts := api.Group("/artifacts") - artifacts.Get("/list", ListArtifacts) - - experiments := api.Group("/experiments") - experiments.Post("/create", CreateExperiment) - experiments.Post("/delete", DeleteExperiment) - experiments.Get("/get", GetExperiment) - experiments.Get("/get-by-name", GetExperimentByName) - experiments.Get("/list", SearchExperiments) - experiments.Post("/restore", RestoreExperiment) - experiments.Get("/search", SearchExperiments) - experiments.Post("/search", SearchExperiments) - experiments.Post("/set-experiment-tag", SetExperimentTag) - experiments.Post("/update", UpdateExperiment) - - metrics := api.Group("/metrics") - metrics.Get("/get-history", GetMetricHistory) - metrics.Get("/get-history-bulk", GetMetricHistoryBulk) - metrics.Post("/get-histories", GetMetricHistories) - - runs := api.Group("/runs") - runs.Post("/create", CreateRun) - runs.Post("/delete", DeleteRun) - runs.Post("/delete-tag", DeleteRunTag) - runs.Get("/get", GetRun) - runs.Post("/log-batch", LogBatch) - runs.Post("/log-metric", LogMetric) - runs.Post("/log-parameter", LogParam) - runs.Post("/restore", RestoreRun) - runs.Post("/search", SearchRuns) - runs.Post("/set-tag", SetRunTag) - runs.Post("/update", UpdateRun) - - api.Get("/model-versions/search", SearchModelVersions) - api.Get("/registered-models/search", SearchRegisteredModels) - - api.Use(func(c *fiber.Ctx) error { - return NewError(ErrorCodeEndpointNotFound, "Not found") - }) - - app := fiber.New() - - app.Mount("/api/2.0/mlflow", api) - app.Mount("/ajax-api/2.0/mlflow/", api) - app.Mount("/api/2.0/preview/mlflow/", api) - app.Mount("/ajax-api/2.0/preview/mlflow/", api) - - app.Get("/health", func(c *fiber.Ctx) error { - return c.SendString("OK") - }) - - app.Get("/version", func(c *fiber.Ctx) error { - return c.SendString(version.Version) - }) - - app.Use("/static-files/", etag.New(), filesystem.New(filesystem.Config{ - Root: http.FS(ui.MlflowFS), - })) - - app.Get("/", etag.New(), func(c *fiber.Ctx) error { - file, _ := ui.MlflowFS.Open("index.html") - stat, _ := file.Stat() - c.Set("Content-Type", "text/html; charset=utf-8") - c.Response().SetBodyStream(file, int(stat.Size())) - return nil - }) - - return app -} diff --git a/pkg/api/mlflow/errors.go b/pkg/api/mlflow/errors.go new file mode 100644 index 000000000..fed5bbb29 --- /dev/null +++ b/pkg/api/mlflow/errors.go @@ -0,0 +1,54 @@ +package mlflow + +import ( + "errors" + + "github.com/gofiber/fiber/v2" + log "github.com/sirupsen/logrus" +) + +func ErrorHandler(c *fiber.Ctx, err error) error { + var e *ErrorResponse + if !errors.As(err, &e) { + var code ErrorCode = ErrorCodeInternalError + + var f *fiber.Error + if errors.As(err, &f) { + switch f.Code { + case fiber.StatusBadRequest: + code = ErrorCodeBadRequest + case fiber.StatusServiceUnavailable: + code = ErrorCodeTemporarilyUnavailable + case fiber.StatusNotFound: + code = ErrorCodeEndpointNotFound + } + } + + e = &ErrorResponse{ + ErrorCode: code, + Message: err.Error(), + } + } + + var code int + var fn func(format string, args ...any) + + switch e.ErrorCode { + case ErrorCodeBadRequest, ErrorCodeInvalidParameterValue, ErrorCodeResourceAlreadyExists: + code = fiber.StatusBadRequest + fn = log.Infof + case ErrorCodeTemporarilyUnavailable: + code = fiber.StatusServiceUnavailable + fn = log.Warnf + case ErrorCodeEndpointNotFound, ErrorCodeResourceDoesNotExist: + code = fiber.StatusNotFound + fn = log.Debugf + default: + code = fiber.StatusInternalServerError + fn = log.Errorf + } + + fn("Error encountered in %s %s: %s", c.Method(), c.Path(), err) + + return c.Status(code).JSON(e) +} diff --git a/pkg/api/mlflow/routes.go b/pkg/api/mlflow/routes.go new file mode 100644 index 000000000..b66b3942d --- /dev/null +++ b/pkg/api/mlflow/routes.go @@ -0,0 +1,47 @@ +package mlflow + +import ( + "github.com/gofiber/fiber/v2" +) + +func AddRoutes(r fiber.Router) { + artifacts := r.Group("/artifacts") + artifacts.Get("/list", ListArtifacts) + + experiments := r.Group("/experiments") + experiments.Post("/create", CreateExperiment) + experiments.Post("/delete", DeleteExperiment) + experiments.Get("/get", GetExperiment) + experiments.Get("/get-by-name", GetExperimentByName) + experiments.Get("/list", SearchExperiments) + experiments.Post("/restore", RestoreExperiment) + experiments.Get("/search", SearchExperiments) + experiments.Post("/search", SearchExperiments) + experiments.Post("/set-experiment-tag", SetExperimentTag) + experiments.Post("/update", UpdateExperiment) + + metrics := r.Group("/metrics") + metrics.Get("/get-history", GetMetricHistory) + metrics.Get("/get-history-bulk", GetMetricHistoryBulk) + metrics.Post("/get-histories", GetMetricHistories) + + runs := r.Group("/runs") + runs.Post("/create", CreateRun) + runs.Post("/delete", DeleteRun) + runs.Post("/delete-tag", DeleteRunTag) + runs.Get("/get", GetRun) + runs.Post("/log-batch", LogBatch) + runs.Post("/log-metric", LogMetric) + runs.Post("/log-parameter", LogParam) + runs.Post("/restore", RestoreRun) + runs.Post("/search", SearchRuns) + runs.Post("/set-tag", SetRunTag) + runs.Post("/update", UpdateRun) + + r.Get("/model-versions/search", SearchModelVersions) + r.Get("/registered-models/search", SearchRegisteredModels) + + r.Use(func(c *fiber.Ctx) error { + return NewError(ErrorCodeEndpointNotFound, "Not found") + }) +} diff --git a/pkg/cmd/server.go b/pkg/cmd/server.go index 8a5224b9f..1c1c10659 100644 --- a/pkg/cmd/server.go +++ b/pkg/cmd/server.go @@ -8,15 +8,17 @@ import ( "strings" "time" - "github.com/G-Research/fasttrack/pkg/api/aim" - "github.com/G-Research/fasttrack/pkg/api/mlflow" + aimAPI "github.com/G-Research/fasttrack/pkg/api/aim" + mlflowAPI "github.com/G-Research/fasttrack/pkg/api/mlflow" "github.com/G-Research/fasttrack/pkg/database" - "github.com/G-Research/fasttrack/pkg/ui" + aimUI "github.com/G-Research/fasttrack/pkg/ui/aim" + "github.com/G-Research/fasttrack/pkg/ui/chooser" + mlflowUI "github.com/G-Research/fasttrack/pkg/ui/mlflow" "github.com/G-Research/fasttrack/pkg/version" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/basicauth" "github.com/gofiber/fiber/v2/middleware/compress" - "github.com/gofiber/fiber/v2/middleware/etag" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" log "github.com/sirupsen/logrus" @@ -51,27 +53,33 @@ func serverCmd(cmd *cobra.Command, args []string) error { IdleTimeout: 30 * time.Second, ServerHeader: fmt.Sprintf("fasttrack/%s", version.Version), DisableStartupMessage: true, + ErrorHandler: func(c *fiber.Ctx, err error) error { + p := string(c.Request().URI().Path()) + switch { + case strings.HasPrefix(p, "/aim/api/"): + return aimAPI.ErrorHandler(c, err) + + case strings.HasPrefix(p, "/api/2.0/mlflow/") || + strings.HasPrefix(p, "/ajax-api/2.0/mlflow/") || + strings.HasPrefix(p, "/mlflow/ajax-api/2.0/mlflow/"): + return mlflowAPI.ErrorHandler(c, err) + + default: + return fiber.DefaultErrorHandler(c, err) + } + }, }) - server.Mount("/aim/", aim.NewApp(viper.GetString("auth-username"), viper.GetString("auth-password"))) - server.Mount("/mlflow/", mlflow.NewApp(viper.GetString("auth-username"), viper.GetString("auth-password"))) - - // Somehow mounting/using ChooserFS as a filesystem handler _sometimes_ results in 404 status code for /mlflow/ - // This is working around it in a non-intellectually-satisfying but effective way - server.Get("/", etag.New(), func(c *fiber.Ctx) error { - file, _ := ui.ChooserFS.Open("index.html") - stat, _ := file.Stat() - c.Set("Content-Type", "text/html; charset=utf-8") - c.Response().SetBodyStream(file, int(stat.Size())) - return nil - }) - server.Get("/simple.min.css", etag.New(), func(c *fiber.Ctx) error { - file, _ := ui.ChooserFS.Open("simple.min.css") - stat, _ := file.Stat() - c.Set("Content-Type", "text/css") - c.Response().SetBodyStream(file, int(stat.Size())) - return nil - }) + authUsername := viper.GetString("auth-username") + authPassword := viper.GetString("auth-password") + if authUsername != "" && authPassword != "" { + log.Infof(`BasicAuth enabled with user "%s"`, authUsername) + server.Use(basicauth.New(basicauth.Config{ + Users: map[string]string{ + authUsername: authPassword, + }, + })) + } server.Use(compress.New(compress.Config{ Next: func(c *fiber.Ctx) bool { @@ -88,6 +96,23 @@ func serverCmd(cmd *cobra.Command, args []string) error { Output: log.StandardLogger().Writer(), })) + aimAPI.AddRoutes(server.Group("/aim/api/")) + aimUI.AddRoutes(server.Group("/aim/")) + + mlflowAPI.AddRoutes(server.Group("/api/2.0/mlflow/")) + mlflowAPI.AddRoutes(server.Group("/ajax-api/2.0/mlflow/")) + mlflowAPI.AddRoutes(server.Group("/mlflow/ajax-api/2.0/mlflow/")) + mlflowUI.AddRoutes(server.Group("/mlflow/")) + + server.Get("/health", func(c *fiber.Ctx) error { + return c.SendString("OK") + }) + server.Get("/version", func(c *fiber.Ctx) error { + return c.SendString(version.Version) + }) + + chooser.AddRoutes(server.Group("/")) + idleConnsClosed := make(chan struct{}) go func() { sigint := make(chan os.Signal, 1) diff --git a/pkg/ui/aim/build.sh b/pkg/ui/aim/embed/build.sh similarity index 100% rename from pkg/ui/aim/build.sh rename to pkg/ui/aim/embed/build.sh diff --git a/pkg/ui/aim/custom.patch b/pkg/ui/aim/embed/custom.patch similarity index 92% rename from pkg/ui/aim/custom.patch rename to pkg/ui/aim/embed/custom.patch index 3314b6f9a..57c860557 100644 --- a/pkg/ui/aim/custom.patch +++ b/pkg/ui/aim/embed/custom.patch @@ -1,8 +1,23 @@ diff --git a/aim/web/ui/package.json b/aim/web/ui/package.json -index 8c8b1e9..4ef1195 100644 +index 8c8b1e9..816e3f1 100644 --- a/aim/web/ui/package.json +++ b/aim/web/ui/package.json -@@ -66,7 +66,7 @@ +@@ -53,7 +53,7 @@ + }, + "scripts": { + "start": "react-app-rewired --max_old_space_size=4096 start", +- "build": "react-app-rewired --max_old_space_size=4096 build && gzipper c -i js,css,html ./build && node tasks/index-html-template-generator.js", ++ "build": "GENERATE_SOURCEMAP=false react-app-rewired --max_old_space_size=2048 build", + "test": "react-scripts test ", + "test:coverage": "react-app-rewired test --collectCoverage", + "test:watch": "react-app-rewired test --watchAll", +@@ -61,12 +61,12 @@ + "lint": "eslint src/. --ext .js,.jsx,.ts,.tsx", + "format:fix": "eslint src/. --ext .js,.jsx,.ts,.tsx --quiet --fix", + "preinstall": "rimraf public/vs", +- "postinstall": "cp -R node_modules/monaco-editor/min/vs public/vs", ++ "postinstall": "cp -R node_modules/monaco-editor/min/vs public/vs && find public/vs -type f | xargs sed -i -e '/^\\/\\/# sourceMappingURL=/d'", + "analyze-bundles": "node tasks/bundle-analyzer.js", "crc-kit": "func() { node tasks/cli/index.js create-component --name=\"$1\" --path=./src/components/kit/ --lint; }; func", "crc": "func() { node tasks/cli/index.js create-component --name=\"$1\" --path=./src/components/ --lint; }; func" }, @@ -12,10 +27,21 @@ index 8c8b1e9..4ef1195 100644 "production": [ ">0.2%", diff --git a/aim/web/ui/public/index.html b/aim/web/ui/public/index.html -index 74776c4..159b897 100644 +index 74776c4..88f4df1 100644 --- a/aim/web/ui/public/index.html +++ b/aim/web/ui/public/index.html -@@ -214,7 +214,7 @@ +@@ -147,10 +147,6 @@ + rel="stylesheet" + href="%PUBLIC_URL%/assets/icomoon/icomoonIcons.css" + /> +- + Aim + + diff --git a/aim/web/ui/src/components/SideBar/SideBar.tsx b/aim/web/ui/src/components/SideBar/SideBar.tsx -index a82cad8..56f21dd 100644 +index a82cad8..80043c4 100644 --- a/aim/web/ui/src/components/SideBar/SideBar.tsx +++ b/aim/web/ui/src/components/SideBar/SideBar.tsx @@ -1,30 +1,39 @@ @@ -58,14 +84,14 @@ index a82cad8..56f21dd 100644 import './Sidebar.scss'; -+const api = new NetworkService(getAPIHost()); -+ function SideBar(): React.FunctionComponentElement { + const [version, setVersion] = React.useState('unknown'); + + useEffect(() => { -+ api.makeAPIGetRequest('version').then((response) => { -+ setVersion(response.body.version); ++ fetch('/version').then((response) => { ++ response.text().then((version) => { ++ setVersion(version); ++ }); + }); + }, []); + diff --git a/pkg/ui/aim/version b/pkg/ui/aim/embed/version similarity index 100% rename from pkg/ui/aim/version rename to pkg/ui/aim/embed/version diff --git a/pkg/ui/aim/routes.go b/pkg/ui/aim/routes.go new file mode 100644 index 000000000..4f90cbe1b --- /dev/null +++ b/pkg/ui/aim/routes.go @@ -0,0 +1,38 @@ +package aim + +import ( + "embed" + "io/fs" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/etag" + "github.com/gofiber/fiber/v2/middleware/filesystem" +) + +//go:embed embed/build +var content embed.FS + +type singleFileFS struct { + fs.FS + Path string +} + +func (f singleFileFS) Open(name string) (fs.File, error) { + return f.FS.Open(f.Path) +} + +func AddRoutes(r fiber.Router) { + sub, _ := fs.Sub(content, "embed/build") + + r.Use("/static-files/", etag.New(), filesystem.New(filesystem.Config{ + Root: http.FS(sub), + })) + + r.Use("/", etag.New(), filesystem.New(filesystem.Config{ + Root: http.FS(singleFileFS{ + sub, + "index.html", + }), + })) +} diff --git a/pkg/ui/chooser/index.html b/pkg/ui/chooser/embed/index.html similarity index 76% rename from pkg/ui/chooser/index.html rename to pkg/ui/chooser/embed/index.html index ed87008af..4b212e9e5 100644 --- a/pkg/ui/chooser/index.html +++ b/pkg/ui/chooser/embed/index.html @@ -26,15 +26,16 @@

fasttrack

+

Which UI do you want to use?

- +

This is the classic MLFlow UI, albeit fast and responsive.

- +

This is the modern Aim UI, much faster than MLFlow.

diff --git a/pkg/ui/chooser/simple.min.css b/pkg/ui/chooser/embed/simple.min.css similarity index 100% rename from pkg/ui/chooser/simple.min.css rename to pkg/ui/chooser/embed/simple.min.css diff --git a/pkg/ui/chooser/routes.go b/pkg/ui/chooser/routes.go new file mode 100644 index 000000000..1a198eb01 --- /dev/null +++ b/pkg/ui/chooser/routes.go @@ -0,0 +1,22 @@ +package chooser + +import ( + "embed" + "io/fs" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/etag" + "github.com/gofiber/fiber/v2/middleware/filesystem" +) + +//go:embed embed +var content embed.FS + +func AddRoutes(r fiber.Router) { + sub, _ := fs.Sub(content, "embed") + + r.Use("/", etag.New(), filesystem.New(filesystem.Config{ + Root: http.FS(sub), + })) +} diff --git a/pkg/ui/embed.go b/pkg/ui/embed.go deleted file mode 100644 index 6a31ceefc..000000000 --- a/pkg/ui/embed.go +++ /dev/null @@ -1,27 +0,0 @@ -package ui - -import ( - "embed" - "io/fs" -) - -//go:embed chooser -var chooserFS embed.FS - -//go:embed mlflow/build -var mlflowFS embed.FS - -//go:embed aim/build -var aimFS embed.FS - -var ( - ChooserFS fs.FS - MlflowFS fs.FS - AimFS fs.FS -) - -func init() { - ChooserFS, _ = fs.Sub(chooserFS, "chooser") - MlflowFS, _ = fs.Sub(mlflowFS, "mlflow/build") - AimFS, _ = fs.Sub(aimFS, "aim/build") -} diff --git a/pkg/ui/mlflow/build.sh b/pkg/ui/mlflow/embed/build.sh similarity index 100% rename from pkg/ui/mlflow/build.sh rename to pkg/ui/mlflow/embed/build.sh diff --git a/pkg/ui/mlflow/custom.patch b/pkg/ui/mlflow/embed/custom.patch similarity index 98% rename from pkg/ui/mlflow/custom.patch rename to pkg/ui/mlflow/embed/custom.patch index 82d091f6d..fd0e6ee7b 100644 --- a/pkg/ui/mlflow/custom.patch +++ b/pkg/ui/mlflow/embed/custom.patch @@ -11,6 +11,19 @@ index 97c79d2..b413a82 100644 // eslint-disable-next-line no-param-reassign loaderConfig.options = { publicPath }; +diff --git a/mlflow/server/js/package.json b/mlflow/server/js/package.json +index ca622dc..d6f3d8f 100644 +--- a/mlflow/server/js/package.json ++++ b/mlflow/server/js/package.json +@@ -145,7 +145,7 @@ + "start:dev-proxy-iframe": "MLFLOW_DEV_PROXY_MODE=true yarn start:iframe", + "start:dev-proxy": "MLFLOW_DEV_PROXY_MODE=true yarn start:mfe", + "start:mfe": "PUBLIC_URL=/ MLFLOW_MFE_DEV=true npm run start:iframe", +- "build": "craco --max_old_space_size=8192 build", ++ "build": "GENERATE_SOURCEMAP=false craco --max_old_space_size=1536 build", + "build:fs": "scripts/build.sh fs", + "build:mfe": "MLFLOW_MFE_DEV=true craco --max_old_space_size=8192 build", + "build:fs:mfe": "FEATURE_STORE_MFE_DEV=true craco --max_old_space_size=8192 build", diff --git a/mlflow/server/js/src/common/constants.js b/mlflow/server/js/src/common/constants.js index b176e1c..66fe90e 100644 --- a/mlflow/server/js/src/common/constants.js @@ -927,7 +940,7 @@ index 6ad01a9..f1f8b5b 100644 div.github { diff --git a/mlflow/server/js/src/experiment-tracking/components/App.js b/mlflow/server/js/src/experiment-tracking/components/App.js -index bf1f184..232955c 100644 +index bf1f184..8b35333 100644 --- a/mlflow/server/js/src/experiment-tracking/components/App.js +++ b/mlflow/server/js/src/experiment-tracking/components/App.js @@ -3,7 +3,8 @@ import { connect } from 'react-redux'; @@ -940,7 +953,7 @@ index bf1f184..232955c 100644 import logo from '../../common/static/home-logo.png'; import ErrorModal from '../../experiment-tracking/components/modals/ErrorModal'; import { CompareModelVersionsPage } from '../../model-registry/components/CompareModelVersionsPage'; -@@ -45,6 +46,21 @@ const classNames = { +@@ -45,6 +46,23 @@ const classNames = { const InteractionTracker = ({ children }) => children; class App extends Component { @@ -952,9 +965,11 @@ index bf1f184..232955c 100644 + } + + componentDidMount() { -+ fetchEndpoint({ relativeUrl: 'version' }).then((result) => { -+ this.setState({ -+ version: result, ++ fetch('/version').then((response) => { ++ response.text().then((version) => { ++ this.setState({ ++ version: version, ++ }); + }); + }); + } @@ -962,7 +977,7 @@ index bf1f184..232955c 100644 render() { const marginRight = 24; return ( -@@ -63,7 +79,7 @@ class App extends Component { +@@ -63,7 +81,7 @@ class App extends Component { MLflow @@ -971,7 +986,7 @@ index bf1f184..232955c 100644
Experiments
diff --git a/pkg/ui/mlflow/version b/pkg/ui/mlflow/embed/version similarity index 100% rename from pkg/ui/mlflow/version rename to pkg/ui/mlflow/embed/version diff --git a/pkg/ui/mlflow/routes.go b/pkg/ui/mlflow/routes.go new file mode 100644 index 000000000..afbfe908d --- /dev/null +++ b/pkg/ui/mlflow/routes.go @@ -0,0 +1,41 @@ +package mlflow + +import ( + "embed" + "io/fs" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/etag" + "github.com/gofiber/fiber/v2/middleware/filesystem" +) + +//go:embed embed/build +var content embed.FS + +type onlyRootFS struct { + fs.FS + Path string +} + +func (f onlyRootFS) Open(name string) (fs.File, error) { + if name != "." { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + return f.FS.Open(f.Path) +} + +func AddRoutes(r fiber.Router) { + sub, _ := fs.Sub(content, "embed/build") + + r.Use("/static-files/", etag.New(), filesystem.New(filesystem.Config{ + Root: http.FS(sub), + })) + + r.Use("/", etag.New(), filesystem.New(filesystem.Config{ + Root: http.FS(onlyRootFS{ + sub, + "index.html", + }), + })) +} diff --git a/tests/mlflow/custom.patch b/tests/mlflow/custom.patch index e51dc65de..3aebd5974 100644 --- a/tests/mlflow/custom.patch +++ b/tests/mlflow/custom.patch @@ -1,8 +1,8 @@ diff --git a/tests/tracking/integration_test_utils.py b/tests/tracking/integration_test_utils.py -index 41ec85f..94cb399 100644 +index 41ec85f..a99de1d 100644 --- a/tests/tracking/integration_test_utils.py +++ b/tests/tracking/integration_test_utils.py -@@ -52,19 +52,21 @@ def _init_server(backend_uri, root_artifact_uri): +@@ -52,14 +52,16 @@ def _init_server(backend_uri, root_artifact_uri): server_port = get_safe_port() process = Popen( [ @@ -24,12 +24,6 @@ index 41ec85f..94cb399 100644 }, ) - _await_server_up_or_die(server_port) -- url = f"http://{LOCALHOST}:{server_port}" -+ url = f"http://{LOCALHOST}:{server_port}/mlflow" - _logger.info(f"Launching tracking server against backend URI {backend_uri}. Server URL: {url}") - return url, process - diff --git a/tests/tracking/test_rest_tracking.py b/tests/tracking/test_rest_tracking.py index 45ee41e..3afdfcb 100644 --- a/tests/tracking/test_rest_tracking.py