From e023f00923f423c53cebb0b2004dcbe4341ee23e Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Fri, 6 Mar 2026 14:58:39 -0800 Subject: [PATCH] feat: remove /api/gateways webserver endpoint Refactor webui so that we have a template, webui/web/js/config.js.tmpl, instead of the /api/gateways to retrieve the gateways listing. This also restructures the css theme into separate common files for easier maintenance. --- webui/web/assets/css/tailwind-config.js | 55 ++++++ webui/web/assets/css/theme.css | 194 +++++++++++++++++++++ webui/web/buckets.html | 61 +------ webui/web/dashboard.html | 23 +-- webui/web/explorer.html | 21 +-- webui/web/index.html | 213 ++---------------------- webui/web/js/api.js | 29 ---- webui/web/js/config.js.tmpl | 1 + webui/web/users.html | 54 +----- webui/webserver.go | 40 +++-- 10 files changed, 300 insertions(+), 391 deletions(-) create mode 100644 webui/web/assets/css/tailwind-config.js create mode 100644 webui/web/assets/css/theme.css create mode 100644 webui/web/js/config.js.tmpl diff --git a/webui/web/assets/css/tailwind-config.js b/webui/web/assets/css/tailwind-config.js new file mode 100644 index 00000000..826e03dd --- /dev/null +++ b/webui/web/assets/css/tailwind-config.js @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Versity Software + * This file is licensed under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Shared Tailwind CSS configuration for all pages. +// Edit this file to change the global color palette, typography, etc. +tailwind.config = { + theme: { + extend: { + colors: { + primary: { + DEFAULT: '#002A7A', + 50: '#E6EBF4', + 100: '#B3C2E0', + 200: '#809ACC', + 300: '#4D71B8', + 400: '#264DA3', + 500: '#002A7A', + 600: '#002468', + 700: '#001D56', + }, + accent: { + DEFAULT: '#0076CD', + 50: '#E6F3FA', + 100: '#B3DCF2', + 500: '#0076CD', + 600: '#0065AF', + }, + charcoal: { + DEFAULT: '#191B2A', + 300: '#757884', + 400: '#565968', + }, + surface: { + DEFAULT: '#F3F8FC', + } + }, + fontFamily: { + sans: ['Roboto', 'system-ui', 'sans-serif'], + }, + } + } +} diff --git a/webui/web/assets/css/theme.css b/webui/web/assets/css/theme.css new file mode 100644 index 00000000..d024abce --- /dev/null +++ b/webui/web/assets/css/theme.css @@ -0,0 +1,194 @@ +/* + * Copyright 2026 Versity Software + * This file is licensed under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * theme.css — Global shared styles for all pages. + * Page-specific styles remain inline in their respective HTML files. + */ + +body { + font-family: 'Roboto', system-ui, sans-serif; +} + +/* Sidebar Navigation */ +.nav-item { + transition: all 0.15s ease; +} +.nav-item:hover { + background: rgba(255, 255, 255, 0.1); +} +.nav-item.active { + background: rgba(0, 118, 205, 0.2); + border-left: 4px solid #0076CD; +} + +/* Modal */ +.modal-backdrop { + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); +} + +/* Custom Dropdown */ +.custom-dropdown { + display: none; + position: absolute; + z-index: 10; + width: 100%; + margin-top: 4px; + background: white; + border: 2px solid #e5e7eb; + border-radius: 0.5rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + max-height: 12rem; + overflow: auto; +} +.custom-dropdown.show { + display: block; +} +.custom-dropdown-item { + padding: 0.75rem 1rem; + cursor: pointer; + color: #191B2A; + transition: background-color 0.15s; +} +.custom-dropdown-item:hover { + background-color: #f9fafb; +} +.custom-dropdown-item.selected { + background-color: rgba(0, 118, 205, 0.1); + color: #0076CD; +} +/* Dropup variant — opens upward */ +.custom-dropdown.dropup { + bottom: 100%; + top: auto; + margin-top: 0; + margin-bottom: 4px; +} + +/* Toggle Switch */ +.toggle-switch { + position: relative; + display: inline-block; + width: 60px; + height: 28px; +} +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #e5e7eb; + transition: 0.3s; + border-radius: 28px; +} +.toggle-slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 4px; + background-color: white; + transition: 0.3s; + border-radius: 50%; +} +input:checked + .toggle-slider { + background-color: #0076CD; +} +input:checked + .toggle-slider:before { + transform: translateX(32px); +} +.toggle-label { + font-size: 0.875rem; + color: #565968; + font-weight: 500; +} + +/* Login Page */ +.input-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: #808080; +} +.input-with-icon { + padding-left: 44px; +} +.password-toggle { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + color: #808080; + cursor: pointer; + background: none; + border: none; + padding: 4px; +} +.password-toggle:hover { + color: #002A7A; +} +.advanced-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + padding: 0.75rem 0; + margin: 0.5rem 0; + background: none; + border: none; + font: inherit; + color: inherit; + text-align: left; + width: 100%; +} +.advanced-toggle:hover { + opacity: 0.8; +} +.advanced-toggle-carat { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.25rem; + height: 1.25rem; + flex-shrink: 0; + transition: transform 0.3s ease; +} +.advanced-toggle.expanded .advanced-toggle-carat { + transform: rotate(90deg); +} +.advanced-toggle-label { + font-weight: 500; + color: #565968; + cursor: pointer; +} +.advanced-options { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease-out; +} +.advanced-options.show { + max-height: 500px; +} diff --git a/webui/web/buckets.html b/webui/web/buckets.html index 565a6f9e..9d19543c 100644 --- a/webui/web/buckets.html +++ b/webui/web/buckets.html @@ -21,67 +21,10 @@ under the License. VersityGW Admin - Buckets + + - - diff --git a/webui/web/dashboard.html b/webui/web/dashboard.html index 855a3431..7a705b14 100644 --- a/webui/web/dashboard.html +++ b/webui/web/dashboard.html @@ -21,29 +21,10 @@ under the License. VersityGW Admin - Dashboard + + - - diff --git a/webui/web/explorer.html b/webui/web/explorer.html index c4615bec..29c65671 100644 --- a/webui/web/explorer.html +++ b/webui/web/explorer.html @@ -21,28 +21,11 @@ under the License. VersityGW - Explorer + + - @@ -405,7 +224,7 @@ under the License. } // ============================================ - // Configured Gateways (from vgwmgr CLI) + // Configured Gateways // ============================================ let configuredGateways = []; let configuredAdminGateways = []; @@ -434,24 +253,18 @@ under the License. return out; } - async function loadConfiguredGateways() { - try { - const res = await fetch('/api/gateways', { cache: 'no-store' }); - if (!res.ok) return { gateways: [], adminGateways: [], defaultRegion: null }; - const data = await res.json(); - if (!data || !Array.isArray(data.gateways)) return { gateways: [], adminGateways: [], defaultRegion: null }; - return { - gateways: data.gateways, - adminGateways: data.adminGateways || data.gateways || [], - defaultRegion: normalizeRegion(typeof data.defaultRegion === 'string' ? data.defaultRegion : null), - }; - } catch (e) { - return { gateways: [], adminGateways: [], defaultRegion: null }; - } + function loadConfiguredGateways() { + const cfg = window.__VGWCONFIG__ || {}; + if (!Array.isArray(cfg.gateways)) return { gateways: [], adminGateways: [], defaultRegion: null }; + return { + gateways: cfg.gateways, + adminGateways: cfg.adminGateways || cfg.gateways || [], + defaultRegion: normalizeRegion(typeof cfg.defaultRegion === 'string' ? cfg.defaultRegion : null), + }; } - async function initConfiguredGateways() { - const cfg = await loadConfiguredGateways(); + function initConfiguredGateways() { + const cfg = loadConfiguredGateways(); configuredGateways = uniqNonEmpty(cfg.gateways); configuredAdminGateways = uniqNonEmpty(cfg.adminGateways); configuredDefaultRegion = cfg.defaultRegion; diff --git a/webui/web/js/api.js b/webui/web/js/api.js index 0ec6de2f..0e9fd1de 100644 --- a/webui/web/js/api.js +++ b/webui/web/js/api.js @@ -291,15 +291,6 @@ class VersityAPI { // User Context Methods (ROOT user detection) // ============================================ - /** - * Detect ROOT user and get accessible gateways - * Calls /api/detect-root endpoint to check if credentials match ROOT config - * @param {string} accessKey - Access key to check - * @param {string} secretKey - Secret key to check - * @returns {Object} - { userType: 'root'|'user', matchingGateways: [...] } - */ - // detectRootUser removed - single gateway mode - /** * Store user type and accessible gateways in session * @param {string} userType - 'root' | 'admin' | 'user' @@ -318,26 +309,6 @@ class VersityAPI { this._userType = sessionStorage.getItem('vgw_user_type') || 'user'; } - /** - * Check if current user is ROOT user (has ROOT credentials matching gateway configs) - * @returns {boolean} - */ - // isRootUser removed - - /** - * Get list of gateways accessible to current user - * Only populated for ROOT users - * @returns {Array} - Array of { name, port, endpoint, region, status } - */ - // getAccessibleGateways removed - - /** - * Check if authenticated - */ - isAuthenticated() { - return this.credentials !== null && this.s3Endpoint !== null; - } - /** * Check if user has admin privileges */ diff --git a/webui/web/js/config.js.tmpl b/webui/web/js/config.js.tmpl new file mode 100644 index 00000000..b2f73741 --- /dev/null +++ b/webui/web/js/config.js.tmpl @@ -0,0 +1 @@ +window.__VGWCONFIG__ = {{.ConfigJSON}}; diff --git a/webui/web/users.html b/webui/web/users.html index 8d9dc1bf..219e1b68 100644 --- a/webui/web/users.html +++ b/webui/web/users.html @@ -21,60 +21,10 @@ under the License. VersityGW Admin - Users + + - - diff --git a/webui/webserver.go b/webui/webserver.go index 0f63a560..97f9ff2e 100644 --- a/webui/webserver.go +++ b/webui/webserver.go @@ -15,9 +15,11 @@ package webui import ( + "encoding/json" "fmt" "net" "net/http" + "strings" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/filesystem" @@ -96,32 +98,48 @@ func (s *Server) setupMiddleware() { // setupRoutes configures all routes func (s *Server) setupRoutes() { - // API endpoint to get configured gateways - s.app.Get("/api/gateways", s.handleGetGateways) + // Serve index.html + s.app.Get("/", s.handleIndexHTML) + s.app.Get("/index.html", s.handleIndexHTML) // Serve embedded static files from web/ s.app.Use("/", filesystem.New(filesystem.Config{ - Root: http.FS(webFS), - PathPrefix: "web", - Index: "index.html", - NotFoundFile: "index.html", // SPA fallback - Browse: false, + Root: http.FS(webFS), + PathPrefix: "web", + Browse: false, })) } -// handleGetGateways returns the configured gateway URLs (both S3 and Admin) -func (s *Server) handleGetGateways(c *fiber.Ctx) error { +// handleIndexHTML serves index.html with server config injected as an inline script. +func (s *Server) handleIndexHTML(c *fiber.Ctx) error { + data, err := webFiles.ReadFile("web/index.html") + if err != nil { + return fiber.ErrInternalServerError + } + adminGateways := s.config.AdminGateways if len(adminGateways) == 0 { - // Fallback to S3 gateways if admin gateways not configured adminGateways = s.config.Gateways } - return c.JSON(fiber.Map{ + configJSON, err := json.Marshal(map[string]interface{}{ "gateways": s.config.Gateways, "adminGateways": adminGateways, "defaultRegion": s.config.Region, }) + if err != nil { + return fiber.ErrInternalServerError + } + + html := strings.Replace( + string(data), + "", + "", + 1, + ) + + c.Set("Content-Type", "text/html; charset=utf-8") + return c.SendString(html) } // ServeMultiPort creates listeners for multiple address specifications and serves