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 {
@@ -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