diff --git a/Makefile b/Makefile index 03685a95e8..1ed2a0436a 100644 --- a/Makefile +++ b/Makefile @@ -151,12 +151,16 @@ $(GO_APP_BUILD_TARGETS): build-%: .PHONY: clean-web-console clean-web-console: rm -rf ui/web-v2/dist/* + rm -rf ui/dashboard/build/* touch ui/web-v2/dist/DONT-EDIT-FILES-IN-THIS-DIRECTORY + touch ui/dashboard/build/DONT-EDIT-FILES-IN-THIS-DIRECTORY .PHONY: build-web-console build-web-console: rm -rf ui/web-v2/dist/* + rm -rf ui/dashboard/build/* make -C ui/web-v2 install build + make -C ui/dashboard install build .PHONY: build-go build-go: $(GO_APP_BUILD_TARGETS) diff --git a/manifests/bucketeer/charts/web/templates/deployment.yaml b/manifests/bucketeer/charts/web/templates/deployment.yaml index e7d127bdd8..2ebc4155aa 100644 --- a/manifests/bucketeer/charts/web/templates/deployment.yaml +++ b/manifests/bucketeer/charts/web/templates/deployment.yaml @@ -164,6 +164,8 @@ spec: value: "{{ .Values.env.pushServicePort }}" - name: BUCKETEER_WEB_WEB_CONSOLE_SERVICE_PORT value: "{{ .Values.env.webConsoleServicePort }}" + - name: BUCKETEER_WEB_DASHBOARD_SERVICE_PORT + value: "{{ .Values.env.dashboardServicePort }}" - name: BUCKETEER_WEB_ACCOUNT_SERVICE value: "{{ .Values.env.accountService }}" - name: BUCKETEER_WEB_AUTH_SERVICE diff --git a/manifests/bucketeer/charts/web/templates/envoy-configmap.yaml b/manifests/bucketeer/charts/web/templates/envoy-configmap.yaml index 70ec3ca9e6..0fe984e4a0 100644 --- a/manifests/bucketeer/charts/web/templates/envoy-configmap.yaml +++ b/manifests/bucketeer/charts/web/templates/envoy-configmap.yaml @@ -435,6 +435,38 @@ data: explicit_http_config: http2_protocol_options: {} ignore_health_on_host_removal: true + - name: dashboard + type: strict_dns + connect_timeout: 5s + dns_lookup_family: V4_ONLY + lb_policy: {{ .Values.envoy.lbPolicy }} + load_assignment: + cluster_name: dashboard + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: localhost + port_value: 9103 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + alpn_protocols: + - h2 + tls_certificates: + - certificate_chain: + filename: /usr/local/certs/service/tls.crt + private_key: + filename: /usr/local/certs/service/tls.key + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: {} + ignore_health_on_host_removal: true listeners: - name: grpc-ingress @@ -851,6 +883,14 @@ data: retry_policy: retry_on: 5xx num_retries: 3 + - match: + prefix: /v3 + route: + cluster: dashboard + timeout: 15s + retry_policy: + retry_on: 5xx + num_retries: 3 - match: prefix: / route: @@ -1016,3 +1056,5 @@ data: '@type': type.googleapis.com/envoy.extensions.resource_monitors.downstream_connections.v3.DownstreamConnectionsConfig # We want disable the warning without setting a limit. So, we set a large number. max_active_downstream_connections: 100000 + + diff --git a/manifests/bucketeer/charts/web/values.yaml b/manifests/bucketeer/charts/web/values.yaml index eace66fa28..9bff0a74a5 100644 --- a/manifests/bucketeer/charts/web/values.yaml +++ b/manifests/bucketeer/charts/web/values.yaml @@ -49,6 +49,7 @@ env: notificationServicePort: 9100 pushServicePort: 9101 webConsoleServicePort: 9102 + dashboardServicePort: 9103 metricsPort: 9002 timezone: UTC emailFilter: diff --git a/manifests/bucketeer/values.dev.yaml b/manifests/bucketeer/values.dev.yaml index 395e52d5e9..7acd363233 100644 --- a/manifests/bucketeer/values.dev.yaml +++ b/manifests/bucketeer/values.dev.yaml @@ -77,6 +77,7 @@ web: clientSecret: redirectUrls: - https://localhost:9000/auth/callback + - https://localhost:9000/v3/auth/callback demoSignIn: enabled: true email: demo@bucketeer.io diff --git a/pkg/web/cmd/server/server.go b/pkg/web/cmd/server/server.go index 657ecc03fa..1e202b30d3 100644 --- a/pkg/web/cmd/server/server.go +++ b/pkg/web/cmd/server/server.go @@ -111,6 +111,7 @@ type server struct { notificationServicePort *int pushServicePort *int webConsoleServicePort *int + dashboardServicePort *int // Service accountService *string authService *string @@ -228,6 +229,10 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { "web-console-service-port", "Port to bind to console service.", ).Default("9102").Int(), + dashboardServicePort: cmd.Flag( + "dashboard-service-port", + "Port to bind to dashboard service.", + ).Default("9103").Int(), accountService: cmd.Flag( "account-service", "bucketeer-account-service address.", @@ -640,6 +645,14 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L rest.WithMetrics(registerer), ) go webConsoleServer.Run() + dashboardServer := rest.NewServer( + *s.certPath, *s.keyPath, + rest.WithLogger(logger), + rest.WithPort(*s.dashboardServicePort), + rest.WithService(NewDashboardService()), + rest.WithMetrics(registerer), + ) + go dashboardServer.Run() // To detach this pod from Kubernetes Service before the app servers stop, we stop the health check service first. // Then, after 10 seconds of sleep, the app servers can be shut down, as no new requests are expected to be sent. // In this case, the Readiness prove must fail within 10 seconds and the pod must be detached. diff --git a/pkg/web/cmd/server/web_console.go b/pkg/web/cmd/server/web_console.go index 9dfb8b391f..11a2a866c6 100644 --- a/pkg/web/cmd/server/web_console.go +++ b/pkg/web/cmd/server/web_console.go @@ -17,24 +17,46 @@ package server import ( "net/http" "os" + "strings" + "github.com/bucketeer-io/bucketeer/ui/dashboard" webv2 "github.com/bucketeer-io/bucketeer/ui/web-v2" ) type spaFileSystem struct { - root http.FileSystem + root http.FileSystem + prefix string } +// Open method for spaFileSystem func (fs *spaFileSystem) Open(name string) (http.File, error) { + // First try with original path f, err := fs.root.Open(name) - if os.IsNotExist(err) { - return fs.root.Open("index.html") + if !os.IsNotExist(err) { + return f, err } - return f, err + + // If file not found and we have a prefix, try stripping it + if fs.prefix != "" && strings.HasPrefix(name, fs.prefix) { + strippedName := strings.TrimPrefix(name, fs.prefix) + f, err = fs.root.Open(strippedName) + if !os.IsNotExist(err) { + return f, err + } + } + + // If still not found, return index.html + return fs.root.Open("index.html") } +// webConsoleHandler returns a http.Handler for the old web console UI. func webConsoleHandler() http.Handler { - return http.FileServer(&spaFileSystem{http.FS(webv2.FS)}) + return http.FileServer(&spaFileSystem{root: http.FS(webv2.FS)}) +} + +// dashboardHandler returns a http.Handler for the new dashboard UI. +func dashboardHandler() http.Handler { + return http.FileServer(&spaFileSystem{root: http.FS(dashboard.FS), prefix: "/v3/"}) } func webConsoleEnvJSHandler(path string) http.Handler { @@ -54,3 +76,14 @@ func (c WebConsoleService) Register(mux *http.ServeMux) { mux.HandleFunc("/static/js/", http.StripPrefix("/static/js/", webConsoleEnvJSHandler(c.consoleEnvJSPath)).ServeHTTP) } + +type DashboardService struct { +} + +func NewDashboardService() DashboardService { + return DashboardService{} +} + +func (d DashboardService) Register(mux *http.ServeMux) { + mux.HandleFunc("/", dashboardHandler().ServeHTTP) +} diff --git a/ui/dashboard/.gitignore b/ui/dashboard/.gitignore index 13a063d819..fcca562cd0 100644 --- a/ui/dashboard/.gitignore +++ b/ui/dashboard/.gitignore @@ -7,7 +7,8 @@ dist-ssr /coverage # production -/build +/build/** +!/build/DONT-EDIT-FILES-IN-THIS-DIRECTORY # misc .DS_Store diff --git a/ui/dashboard/Makefile b/ui/dashboard/Makefile new file mode 100644 index 0000000000..0362ee65eb --- /dev/null +++ b/ui/dashboard/Makefile @@ -0,0 +1,17 @@ +# Makefile for the dashboard UI + +# Install dependencies +.PHONY: install +install: + yarn install + +# Run the development server +.PHONY: dev +dev: + yarn start + +# Build for production +.PHONY: build +build: + VITE_RELEASE_CHANNEL=prod \ + yarn build diff --git a/ui/dashboard/build/DONT-EDIT-FILES-IN-THIS-DIRECTORY b/ui/dashboard/build/DONT-EDIT-FILES-IN-THIS-DIRECTORY new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/dashboard/embed.go b/ui/dashboard/embed.go new file mode 100644 index 0000000000..4e7cdad527 --- /dev/null +++ b/ui/dashboard/embed.go @@ -0,0 +1,12 @@ +package dashboard + +import ( + "embed" + "io/fs" +) + +//go:embed build +var assets embed.FS + +// FS contains the new dashboard assets. +var FS, _ = fs.Sub(assets, "build") diff --git a/ui/dashboard/index.html b/ui/dashboard/index.html index 62eaa312d6..5f7cd1a13f 100644 --- a/ui/dashboard/index.html +++ b/ui/dashboard/index.html @@ -8,6 +8,7 @@
+ diff --git a/ui/dashboard/src/configs/index.ts b/ui/dashboard/src/configs/index.ts index 6cb4352d93..04d6b70c7e 100644 --- a/ui/dashboard/src/configs/index.ts +++ b/ui/dashboard/src/configs/index.ts @@ -5,14 +5,38 @@ export const tailwindConfig = resolveConfig(customTailwindConfig); const releaseMode = import.meta.env.VITE_RELEASE_CHANNEL; +declare global { + interface Window { + env: { + DEMO_SIGN_IN_ENABLED?: boolean; + DEMO_SIGN_IN_EMAIL?: string; + DEMO_SIGN_IN_PASSWORD?: string; + GOOGLE_TAG_MANAGER_ID?: string; + }; + } +} + export const urls = { GRPC: releaseMode !== 'prod' ? import.meta.env.VITE_WEB_API_ENDPOINT : '', AUTH_REDIRECT: releaseMode !== 'prod' ? `${import.meta.env.VITE_AUTH_REDIRECT_ENDPOINT}/auth/callback` - : `${window.location.origin}/auth/callback` + : `${window.location.origin}/v3/auth/callback` // TODO: Remove the `/v3` when the new console is released }; -export const DEMO_SIGN_IN_ENABLED = import.meta.env.VITE_DEMO_SIGN_IN_ENABLED; -export const DEMO_SIGN_IN_EMAIL = import.meta.env.VITE_DEMO_SIGN_IN_EMAIL; -export const DEMO_SIGN_IN_PASSWORD = import.meta.env.VITE_DEMO_SIGN_IN_PASSWORD; +export const GOOGLE_TAG_MANAGER_ID = window.env?.GOOGLE_TAG_MANAGER_ID || ''; + +export const DEMO_SIGN_IN_ENABLED = + releaseMode !== 'prod' + ? import.meta.env.VITE_DEMO_SIGN_IN_ENABLED + : window.env?.DEMO_SIGN_IN_ENABLED; + +export const DEMO_SIGN_IN_EMAIL = + releaseMode !== 'prod' + ? import.meta.env.VITE_DEMO_SIGN_IN_EMAIL + : window.env?.DEMO_SIGN_IN_EMAIL; + +export const DEMO_SIGN_IN_PASSWORD = + releaseMode !== 'prod' + ? import.meta.env.VITE_DEMO_SIGN_IN_ENABLED + : window.env?.DEMO_SIGN_IN_PASSWORD;