Skip to content

Commit

Permalink
enable configurable endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
wdbaruni committed Sep 11, 2024
1 parent 9fe5ce4 commit ace5904
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 60 deletions.
23 changes: 8 additions & 15 deletions cmd/cli/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"net"
"net/url"
"os"
"strconv"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -193,24 +192,18 @@ func serve(cmd *cobra.Command, cfg types.Bacalhau, fsRepo *repo.FsRepo) error {

// Start up Dashboard - default: 8483
if cfg.WebUI.Enabled {
apiURL := standardNode.APIServer.GetURI().JoinPath("api", "v1")
host, portStr, err := net.SplitHostPort(cfg.WebUI.Listen)
if err != nil {
return err
webuiConfig := webui.Config{
APIEndpoint: standardNode.APIServer.GetURI().String(),
Listen: cfg.WebUI.Listen,
}
webuiPort, err := strconv.ParseInt(portStr, 10, 64)
webuiServer, err := webui.NewServer(webuiConfig)
if err != nil {
return err
// not failing the node if the webui server fails to start
log.Error().Err(err).Msg("Failed to start ui server")
}
go func() {
// Specifically leave the host blank. The app will just use whatever
// host it is served on and replace the port and path.
apiPort := apiURL.Port()
apiPath := apiURL.Path

err := webui.ListenAndServe(ctx, host, apiPort, apiPath, int(webuiPort))
if err != nil {
cmd.PrintErrln(err)
if err := webuiServer.ListenAndServe(ctx); err != nil {
log.Error().Err(err).Msg("ui server error")
}
}()
}
Expand Down
1 change: 1 addition & 0 deletions webui/.earthlyignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ coverage
build
.yarn
.storybook
.next

.git
2 changes: 1 addition & 1 deletion webui/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nodeLinker: node-modules
nodeLinker: node-modules
30 changes: 15 additions & 15 deletions webui/components/ui/toast.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use client"
'use client'

import * as React from "react"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react'
import { Cross2Icon } from '@radix-ui/react-icons'
import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from 'class-variance-authority'

import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'

const ToastProvider = ToastPrimitives.Provider

Expand All @@ -16,7 +16,7 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className
)}
{...props}
Expand All @@ -25,17 +25,17 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName

const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: "border bg-background text-foreground",
default: 'border bg-background text-foreground',
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
'destructive group border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
}
)
Expand All @@ -62,7 +62,7 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className
)}
{...props}
Expand All @@ -77,7 +77,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className
)}
toast-close=""
Expand All @@ -94,7 +94,7 @@ const ToastTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
{...props}
/>
))
Expand All @@ -106,7 +106,7 @@ const ToastDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
className={cn('text-sm opacity-90', className)}
{...props}
/>
))
Expand Down
6 changes: 3 additions & 3 deletions webui/components/ui/toaster.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"use client"
'use client'

import { useToast } from "@/hooks/use-toast"
import { useToast } from '@/hooks/use-toast'
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
} from '@/components/ui/toast'

export function Toaster() {
const { toasts } = useToast()
Expand Down
28 changes: 18 additions & 10 deletions webui/lib/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { OpenAPI } from './generated'
import { useState, useEffect } from 'react'

// This function will be used to initialize the API
export function initializeApi(apiUrl?: string) {
OpenAPI.BASE =
apiUrl ||
process.env.NEXT_PUBLIC_BACALHAU_API_ADDRESS ||
'http://localhost:1234'
// log all env
console.log('API initialized with URL:', OpenAPI.BASE)
interface Config {
APIEndpoint: string
}

export async function initializeApi() {
try {
const response = await fetch('/_config')
if (!response.ok) {
throw new Error('Failed to fetch config')
}
const config: Config = await response.json()
OpenAPI.BASE = config.APIEndpoint || 'http://localhost:1234'
console.log('API initialized with URL:', OpenAPI.BASE)
} catch (error) {
console.error('Error initializing API:', error)
OpenAPI.BASE = 'http://localhost:1234' // Fallback to default
}
}

export { OpenAPI }
Expand All @@ -17,8 +26,7 @@ export function useApiInitialization() {
const [isInitialized, setIsInitialized] = useState(false)

useEffect(() => {
initializeApi()
setIsInitialized(true)
initializeApi().then(() => setIsInitialized(true))
}, [])

return isInitialized
Expand Down
87 changes: 71 additions & 16 deletions webui/webui.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package webui

import (
"bytes"
"context"
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
Expand All @@ -12,6 +12,7 @@ import (
"os"
"path"
"strings"
"sync"
"time"

"github.com/rs/zerolog/log"
Expand All @@ -20,22 +21,77 @@ import (
//go:embed build/**
var buildFiles embed.FS

func ListenAndServe(ctx context.Context, host, apiPort, apiPath string, listenPort int) error {
type Config struct {
APIEndpoint string `json:"APIEndpoint"`
Listen string `json:"Listen"`
}

type Server struct {
config Config
configLock sync.RWMutex
mux *http.ServeMux
}

func NewServer(cfg Config) (*Server, error) {
if cfg.Listen == "" {
return nil, fmt.Errorf("listen address cannot be empty")
}
if cfg.APIEndpoint == "" {
return nil, fmt.Errorf("API endpoint cannot be empty")
}

s := &Server{
config: cfg,
mux: http.NewServeMux(),
}

s.mux.HandleFunc("/_config", s.handleConfig)
s.mux.HandleFunc("/", s.handleFiles)

return s, nil
}

func (s *Server) ListenAndServe(ctx context.Context) error {
server := &http.Server{
Addr: fmt.Sprintf(":%d", listenPort),
Handler: http.HandlerFunc(serveFiles),
Addr: s.config.Listen,
Handler: s.mux,
ReadTimeout: time.Minute,
WriteTimeout: time.Minute,
ReadHeaderTimeout: time.Minute,
IdleTimeout: time.Minute,
BaseContext: func(l net.Listener) context.Context { return ctx },
}

log.Info().Int("port", listenPort).Msg("Starting ui server")
return server.ListenAndServe()
log.Info().Str("listen", s.config.Listen).Msg("Starting UI server")

go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Error().Err(err).Msg("Server shutdown error")
}
}()

if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("server error: %w", err)
}

return nil
}

func serveFiles(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
s.configLock.RLock()
defer s.configLock.RUnlock()

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(s.config); err != nil {
log.Error().Err(err).Msg("Failed to encode config")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}

func (s *Server) handleFiles(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
statusCode := http.StatusOK
var message string
Expand Down Expand Up @@ -80,7 +136,7 @@ func serveFiles(w http.ResponseWriter, r *http.Request) {
// If still not found, serve our custom 404 page
statusCode = http.StatusNotFound
message = "File not found"
serve404(w, r)
s.serve404(w, r)
return
}
fsPath = htmlPath
Expand Down Expand Up @@ -112,26 +168,26 @@ func serveFiles(w http.ResponseWriter, r *http.Request) {
// If we found an index.html in this directory, serve it
defer indexFile.Close()
message = "Served index.html from directory"
serveFileContent(w, r, indexFile, "index.html")
s.serveFileContent(w, r, indexFile, "index.html")
return
}
// If there's no index.html in this specific directory,
// or if we encountered any errors, serve our custom 404 page
statusCode = http.StatusNotFound
message = "Directory without index.html"
serve404(w, r)
s.serve404(w, r)
return
}

// If we've reached here, we're dealing with a normal file (not a directory)
// Serve the file with its correct name
message = fmt.Sprintf("Served file: %s", stat.Name())
serveFileContent(w, r, file, stat.Name())
s.serveFileContent(w, r, file, stat.Name())
}

// serveFileContent reads the entire file into memory and serves it
// This approach is used because fs.File doesn't guarantee implementation of io.ReadSeeker
func serveFileContent(w http.ResponseWriter, r *http.Request, file fs.File, name string) {
func (s *Server) serveFileContent(w http.ResponseWriter, r *http.Request, file fs.File, name string) {
// Read the entire file content
content, err := io.ReadAll(file)
if err != nil {
Expand All @@ -143,12 +199,12 @@ func serveFileContent(w http.ResponseWriter, r *http.Request, file fs.File, name
// Create a ReadSeeker from the content and serve it
// We use time.Time{} as the modtime, which will make the file always downloadable
// You might want to get the actual modtime if caching is important
http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(content))
http.ServeContent(w, r, name, time.Time{}, strings.NewReader(string(content)))
}

// serve404 serves the custom 404.html file
// This is used as a fallback for non-existent paths or directories without an index.html
func serve404(w http.ResponseWriter, r *http.Request) {
func (s *Server) serve404(w http.ResponseWriter, r *http.Request) {
notFoundPath := path.Join("build", "404.html")
notFoundFile, err := buildFiles.Open(notFoundPath)
if err != nil {
Expand All @@ -172,8 +228,7 @@ func serve404(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")

// Write the content
_, err = w.Write(content)
if err != nil {
if _, err := w.Write(content); err != nil {
log.Error().Err(err).Msg("Failed to write 404 page content")
}
}

0 comments on commit ace5904

Please sign in to comment.