Build both old & new UI into Prometheus, allow choosing via feature flag

This keeps the old "react-app" directory in its existing location (to make
it easier to merge changes from the main branch), but separates it from the
npm workspaces setup. Thus it now needs to be npm-installed/built/linted
separately. This is a bit hacky, but should only be needed temporarily,
until the old UI can be removed.

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2024-04-18 15:00:27 +02:00
parent a057601771
commit e8bbe191d4
13 changed files with 24675 additions and 149 deletions

3
.gitignore vendored
View file

@ -22,7 +22,8 @@ benchmark.txt
/documentation/examples/remote_storage/example_write_adapter/example_write_adapter /documentation/examples/remote_storage/example_write_adapter/example_write_adapter
npm_licenses.tar.bz2 npm_licenses.tar.bz2
/web/ui/static/react /web/ui/static/react-app
/web/ui/static/mantine-ui
/vendor /vendor
/.build /.build

View file

@ -48,6 +48,10 @@ ui-bump-version:
.PHONY: ui-install .PHONY: ui-install
ui-install: ui-install:
cd $(UI_PATH) && npm install cd $(UI_PATH) && npm install
# The old React app has been separated from the npm workspaces setup to avoid
# issues with conflicting dependencies. This is a temporary solution until the
# new Mantine-based UI is fully integrated and the old app can be removed.
cd $(UI_PATH)/react-app && npm install
.PHONY: ui-build .PHONY: ui-build
ui-build: ui-build:
@ -64,6 +68,10 @@ ui-test:
.PHONY: ui-lint .PHONY: ui-lint
ui-lint: ui-lint:
cd $(UI_PATH) && npm run lint cd $(UI_PATH) && npm run lint
# The old React app has been separated from the npm workspaces setup to avoid
# issues with conflicting dependencies. This is a temporary solution until the
# new Mantine-based UI is fully integrated and the old app can be removed.
cd $(UI_PATH)/react-app && npm run lint
.PHONY: assets .PHONY: assets
assets: ui-install ui-build assets: ui-install ui-build

View file

@ -230,6 +230,9 @@ func (c *flagConfig) setFeatureListOptions(logger log.Logger) error {
continue continue
case "promql-at-modifier", "promql-negative-offset": case "promql-at-modifier", "promql-negative-offset":
level.Warn(logger).Log("msg", "This option for --enable-feature is now permanently enabled and therefore a no-op.", "option", o) level.Warn(logger).Log("msg", "This option for --enable-feature is now permanently enabled and therefore a no-op.", "option", o)
case "new-ui":
c.web.UseNewUI = true
level.Info(logger).Log("msg", "Serving experimental new web UI.")
default: default:
level.Warn(logger).Log("msg", "Unknown option for --enable-feature", "option", o) level.Warn(logger).Log("msg", "Unknown option for --enable-feature", "option", o)
} }
@ -446,7 +449,7 @@ func main() {
a.Flag("scrape.discovery-reload-interval", "Interval used by scrape manager to throttle target groups updates."). a.Flag("scrape.discovery-reload-interval", "Interval used by scrape manager to throttle target groups updates.").
Hidden().Default("5s").SetValue(&cfg.scrape.DiscoveryReloadInterval) Hidden().Default("5s").SetValue(&cfg.scrape.DiscoveryReloadInterval)
a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: agent, auto-gomemlimit, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-at-modifier, promql-negative-offset, promql-per-step-stats, promql-experimental-functions, remote-write-receiver (DEPRECATED), extra-scrape-metrics, new-service-discovery-manager, auto-gomaxprocs, no-default-scrape-port, native-histograms, otlp-write-receiver. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details."). a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: agent, auto-gomemlimit, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-at-modifier, promql-negative-offset, promql-per-step-stats, promql-experimental-functions, remote-write-receiver (DEPRECATED), extra-scrape-metrics, new-service-discovery-manager, auto-gomaxprocs, no-default-scrape-port, native-histograms, otlp-write-receiver, new-ui. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details.").
Default("").StringsVar(&cfg.featureList) Default("").StringsVar(&cfg.featureList)
promlogflag.AddFlags(a, &cfg.promlogConfig) promlogflag.AddFlags(a, &cfg.promlogConfig)

View file

@ -193,7 +193,7 @@ This should **only** be applied to metrics that currently produce such labels.
`--enable-feature=otlp-write-receiver` `--enable-feature=otlp-write-receiver`
The OTLP receiver allows Prometheus to accept [OpenTelemetry](https://opentelemetry.io/) metrics writes. The OTLP receiver allows Prometheus to accept [OpenTelemetry](https://opentelemetry.io/) metrics writes.
Prometheus is best used as a Pull based system, and staleness, `up` metric, and other Pull enabled features Prometheus is best used as a Pull based system, and staleness, `up` metric, and other Pull enabled features
won't work when you push OTLP metrics. won't work when you push OTLP metrics.
## Experimental PromQL functions ## Experimental PromQL functions
@ -224,3 +224,9 @@ When the `concurrent-rule-eval` feature flag is enabled, rules without any depen
This has the potential to improve rule group evaluation latency and resource utilization at the expense of adding more concurrent query load. This has the potential to improve rule group evaluation latency and resource utilization at the expense of adding more concurrent query load.
The number of concurrent rule evaluations can be configured with `--rules.max-concurrent-rule-evals`, which is set to `4` by default. The number of concurrent rule evaluations can be configured with `--rules.max-concurrent-rule-evals`, which is set to `4` by default.
## Experimental new web UI
Enables the new experimental web UI instead of the old and stable web UI. The new UI is a complete rewrite and aims to be cleaner, less cluttered, and more modern under the hood. It is not feature complete yet and is still under active development.
`--enable-feature=new-ui`

View file

@ -30,8 +30,8 @@ function publish() {
cmd+=" --dry-run" cmd+=" --dry-run"
fi fi
for workspace in ${workspaces}; do for workspace in ${workspaces}; do
# package "app" is private so we shouldn't try to publish it. # package "mantine-ui" is private so we shouldn't try to publish it.
if [[ "${workspace}" != "react-app" ]]; then if [[ "${workspace}" != "mantine-ui" ]]; then
cd "${workspace}" cd "${workspace}"
eval "${cmd}" eval "${cmd}"
cd "${root_ui_folder}" cd "${root_ui_folder}"

View file

@ -30,10 +30,19 @@ function buildModule() {
} }
function buildReactApp() { function buildReactApp() {
echo "build react-app"
cd react-app
npm run build
cd ..
rm -rf ./static/react-app
mv ./react-app/build ./static/react-app
}
function buildMantineUI() {
echo "build mantine-ui" echo "build mantine-ui"
npm run build -w @prometheus-io/mantine-ui npm run build -w @prometheus-io/mantine-ui
rm -rf ./static/react rm -rf ./static/mantine-ui
mv ./mantine-ui/dist ./static/react mv ./mantine-ui/dist ./static/mantine-ui
} }
for i in "$@"; do for i in "$@"; do
@ -41,6 +50,7 @@ for i in "$@"; do
--all) --all)
buildModule buildModule
buildReactApp buildReactApp
buildMantineUI
shift shift
;; ;;
--build-module) --build-module)

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/prometheus-logo.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <!--

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -41,6 +41,7 @@
"@lezer/common": "^1.1.1", "@lezer/common": "^1.1.1",
"@lezer/highlight": "^1.2.0", "@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.3.14", "@lezer/lr": "^1.3.14",
"eslint-plugin-prettier": "^5.1.3",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"nock": "^13.4.0" "nock": "^13.4.0"
}, },

688
web/ui/package-lock.json generated

File diff suppressed because it is too large Load diff

24050
web/ui/react-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{ {
"name": "@prometheus-io/app", "name": "@prometheus-io/react-app",
"version": "0.49.1", "version": "0.49.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -68,7 +68,9 @@
"@testing-library/react-hooks": "^7.0.2", "@testing-library/react-hooks": "^7.0.2",
"@types/enzyme": "^3.10.18", "@types/enzyme": "^3.10.18",
"@types/flot": "0.0.36", "@types/flot": "0.0.36",
"@types/jest": "^29.5.11",
"@types/jquery": "^3.5.29", "@types/jquery": "^3.5.29",
"@types/node": "^20.10.4",
"@types/react": "^17.0.71", "@types/react": "^17.0.71",
"@types/react-copy-to-clipboard": "^5.0.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^17.0.25", "@types/react-dom": "^17.0.25",
@ -78,8 +80,16 @@
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2", "enzyme-to-json": "^3.6.2",
"eslint-config-prettier": "^8.10.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^4.2.1",
"jest-canvas-mock": "^2.5.2",
"jest-fetch-mock": "^3.0.3",
"mutationobserver-shim": "^0.3.7", "mutationobserver-shim": "^0.3.7",
"sinon": "^14.0.2" "prettier": "^2.8.8",
"react-scripts": "^5.0.1",
"sinon": "^14.0.2",
"ts-jest": "^29.1.1"
}, },
"jest": { "jest": {
"snapshotSerializers": [ "snapshotSerializers": [

View file

@ -81,6 +81,7 @@ var reactRouterAgentPaths = []string{
// Paths that are handled by the React router when the Agent mode is not set. // Paths that are handled by the React router when the Agent mode is not set.
var reactRouterServerPaths = []string{ var reactRouterServerPaths = []string{
"/alerts", "/alerts",
"/graph",
"/query", "/query",
"/rules", "/rules",
"/tsdb-status", "/tsdb-status",
@ -251,6 +252,7 @@ type Options struct {
UserAssetsPath string UserAssetsPath string
ConsoleTemplatesPath string ConsoleTemplatesPath string
ConsoleLibrariesPath string ConsoleLibrariesPath string
UseNewUI bool
EnableLifecycle bool EnableLifecycle bool
EnableAdminAPI bool EnableAdminAPI bool
PageTitle string PageTitle string
@ -361,10 +363,13 @@ func New(logger log.Logger, o *Options) *Handler {
router = router.WithPrefix(o.RoutePrefix) router = router.WithPrefix(o.RoutePrefix)
} }
homePage := "/query" homePage := "/graph"
if o.IsAgent { if o.IsAgent {
homePage = "/agent" homePage = "/agent"
} }
if o.UseNewUI {
homePage = "/query"
}
readyf := h.testReady readyf := h.testReady
@ -372,6 +377,11 @@ func New(logger log.Logger, o *Options) *Handler {
http.Redirect(w, r, path.Join(o.ExternalURL.Path, homePage), http.StatusFound) http.Redirect(w, r, path.Join(o.ExternalURL.Path, homePage), http.StatusFound)
}) })
reactAssetsRoot := "/static/react-app"
if h.options.UseNewUI {
reactAssetsRoot = "/static/mantine-ui"
}
// The console library examples at 'console_libraries/prom.lib' still depend on old asset files being served under `classic`. // The console library examples at 'console_libraries/prom.lib' still depend on old asset files being served under `classic`.
router.Get("/classic/static/*filepath", func(w http.ResponseWriter, r *http.Request) { router.Get("/classic/static/*filepath", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = path.Join("/static", route.Param(r.Context(), "filepath")) r.URL.Path = path.Join("/static", route.Param(r.Context(), "filepath"))
@ -389,7 +399,8 @@ func New(logger log.Logger, o *Options) *Handler {
router.Get("/consoles/*filepath", readyf(h.consoles)) router.Get("/consoles/*filepath", readyf(h.consoles))
serveReactApp := func(w http.ResponseWriter, r *http.Request) { serveReactApp := func(w http.ResponseWriter, r *http.Request) {
f, err := ui.Assets.Open("/static/react/index.html") indexPath := reactAssetsRoot + "/index.html"
f, err := ui.Assets.Open(indexPath)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error opening React index.html: %v", err) fmt.Fprintf(w, "Error opening React index.html: %v", err)
@ -426,8 +437,8 @@ func New(logger log.Logger, o *Options) *Handler {
// The favicon and manifest are bundled as part of the React app, but we want to serve // The favicon and manifest are bundled as part of the React app, but we want to serve
// them on the root. // them on the root.
for _, p := range []string{"/favicon.ico", "/manifest.json"} { for _, p := range []string{"/favicon.svg", "/favicon.ico", "/manifest.json"} {
assetPath := "/static/react" + p assetPath := reactAssetsRoot + p
router.Get(p, func(w http.ResponseWriter, r *http.Request) { router.Get(p, func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = assetPath r.URL.Path = assetPath
fs := server.StaticFileServer(ui.Assets) fs := server.StaticFileServer(ui.Assets)
@ -435,9 +446,13 @@ func New(logger log.Logger, o *Options) *Handler {
}) })
} }
reactStaticAssetsDir := "/static"
if h.options.UseNewUI {
reactStaticAssetsDir = "/assets"
}
// Static files required by the React app. // Static files required by the React app.
router.Get("/assets/*filepath", func(w http.ResponseWriter, r *http.Request) { router.Get(reactStaticAssetsDir+"/*filepath", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = path.Join("/static/react/assets", route.Param(r.Context(), "filepath")) r.URL.Path = path.Join(reactAssetsRoot+reactStaticAssetsDir, route.Param(r.Context(), "filepath"))
fs := server.StaticFileServer(ui.Assets) fs := server.StaticFileServer(ui.Assets)
fs.ServeHTTP(w, r) fs.ServeHTTP(w, r)
}) })