feat(dashboard): add local frontend development environment

Add hack/frontend-dev.sh script and dev-nginx.conf for local frontend
development. The script clones upstream repos, builds patched toolkit,
and starts nginx + BFF + Vite dev server with production-like routing.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
Andrei Kvapil 2026-02-06 01:43:16 +01:00
parent e62a6d2a8e
commit d33630ad1e
No known key found for this signature in database
GPG key ID: 931CF7FEACEAF765
4 changed files with 287 additions and 0 deletions

View file

@ -7,6 +7,9 @@ include ../../../hack/package.mk
update: update-crd update-dockerfiles
image: image-openapi-ui image-openapi-ui-k8s-bff image-token-proxy update-tenant-text
frontend-dev:
hack/frontend-dev.sh
update-dockerfiles:
@echo Update dockerfiles manually

View file

@ -0,0 +1,5 @@
# Cloned repos (created by frontend-dev.sh)
openapi-k8s-toolkit/
openapi-ui/
openapi-ui-k8s-bff/
.toolkit-patched

View file

@ -0,0 +1,90 @@
worker_processes 1;
daemon off;
error_log /dev/stderr warn;
events {
worker_connections 4096;
}
http {
access_log /dev/stderr;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 8080 default_server;
# Static cluster list
location ~ ^(/clusterlist|/api/clusters)$ {
add_header Content-Type application/json;
return 200 '[{"api":"localhost","baseDomain":"localhost","description":"local","externalDomain":"localhost","name":"default","tenant":"dev"}]';
}
# Strip /api/clusters/default prefix and self-proxy (same as production)
location /api/clusters/default {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
rewrite /api/clusters/default/(.*) /$1 break;
proxy_pass http://127.0.0.1:8080;
}
# K8s API via kubectl proxy
location /k8s {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_buffering off;
rewrite /k8s/(.*) /$1 break;
proxy_pass http://127.0.0.1:8081;
}
# BFF HTTP
location /openapi-bff {
proxy_pass http://127.0.0.1:4002;
}
# BFF WebSocket
location /openapi-bff-ws/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_pass http://127.0.0.1:4002;
}
# Redirect / to /openapi-ui
location = / {
return 301 /openapi-ui;
}
# Frontend (Vite dev server) — catch-all with WebSocket support for HMR
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:4001;
}
location /healthcheck {
access_log off;
return 200 "Healthy\n";
}
}
}

View file

@ -0,0 +1,189 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DASHBOARD_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$SCRIPT_DIR"
GREEN='\033[0;32m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${CYAN}==>${NC} $*"; }
# Verify kubectl access
log "Using kubectl context: $(kubectl config current-context)"
if ! kubectl api-resources --api-group=core.cozystack.io &>/dev/null; then
echo "ERROR: Cluster does not have core.cozystack.io API group." >&2
echo "Set KUBECONFIG to a cluster with Cozystack installed, e.g.:" >&2
echo " KUBECONFIG=/path/to/kubeconfig $0" >&2
exit 1
fi
# Cleanup on exit
cleanup() {
echo ""
log "Stopping all services..."
kill 0 2>/dev/null
}
trap cleanup EXIT
# --- Clone repositories ---
clone_repo() {
local url="$1" dir="$2" branch="${3:-}"
if [ ! -d "$dir/.git" ]; then
log "Cloning $dir..."
git clone "$url" "$dir"
fi
if [ -n "$branch" ]; then
git -C "$dir" checkout "$branch" 2>/dev/null || true
fi
}
log "Preparing repositories..."
clone_repo git@github.com:PRO-Robotech/openapi-ui.git openapi-ui release/1.3.0
clone_repo git@github.com:PRO-Robotech/openapi-ui-k8s-bff.git openapi-ui-k8s-bff release/1.3.0
clone_repo git@github.com:PRO-Robotech/openapi-k8s-toolkit.git openapi-k8s-toolkit release/1.3.0
# --- Install dependencies ---
log "Installing dependencies..."
for dir in openapi-ui openapi-ui-k8s-bff openapi-k8s-toolkit; do
if [ ! -d "$dir/node_modules" ]; then
log " npm install in $dir"
(cd "$dir" && npm install --silent)
fi
done
# --- Build patched toolkit ---
PATCHES_DIR="$DASHBOARD_DIR/images/openapi-ui/openapi-k8s-toolkit/patches"
UI_TOOLKIT_DIST="openapi-ui/node_modules/@prorobotech/openapi-k8s-toolkit/dist"
if [ ! -f .toolkit-patched ]; then
log "Building patched openapi-k8s-toolkit..."
(
cd openapi-k8s-toolkit
git checkout -- . 2>/dev/null || true
for patch in "$PATCHES_DIR"/*.diff; do
[ -f "$patch" ] && git apply "$patch" && log " Applied $(basename "$patch")"
done
npm run build --silent
)
log "Replacing toolkit in openapi-ui node_modules..."
rm -rf "$UI_TOOLKIT_DIST"
cp -r openapi-k8s-toolkit/dist "$UI_TOOLKIT_DIST"
touch .toolkit-patched
fi
# --- Prepare UI ---
mkdir -p openapi-ui/public
if [ ! -f openapi-ui/.env.options ]; then
log "Creating openapi-ui/.env.options..."
sed 's|^KUBE_API_URL=.*|KUBE_API_URL=http://localhost:8080|;s|^BFF_URL=.*|BFF_URL=http://localhost:4002|' \
openapi-ui/.env.options.dist > openapi-ui/.env.options
fi
if [ ! -f openapi-ui/public/env.js ]; then
log "Generating openapi-ui/public/env.js..."
cat > openapi-ui/public/env.js <<'ENVJS'
window._env_ = {
BASEPREFIX: "/openapi-ui",
HIDE_INSIDE: "true",
CUSTOMIZATION_API_GROUP: "dashboard.cozystack.io",
CUSTOMIZATION_API_VERSION: "v1alpha1",
CUSTOMIZATION_NAVIGATION_RESOURCE: "navigation",
CUSTOMIZATION_NAVIGATION_RESOURCE_NAME: "navigations",
INSTANCES_API_GROUP: "dashboard.cozystack.io",
INSTANCES_RESOURCE_NAME: "instances",
INSTANCES_VERSION: "v1alpha1",
MARKETPLACE_GROUP: "dashboard.cozystack.io",
MARKETPLACE_KIND: "MarketplacePanel",
MARKETPLACE_RESOURCE_NAME: "marketplacepanels",
MARKETPLACE_VERSION: "v1alpha1",
NAVIGATE_FROM_CLUSTERLIST: "/openapi-ui/~recordValue~/api-table/core.cozystack.io/v1alpha1/tenantnamespaces",
PROJECTS_API_GROUP: "core.cozystack.io",
PROJECTS_RESOURCE_NAME: "tenantnamespaces",
PROJECTS_VERSION: "v1alpha1",
CUSTOM_NAMESPACE_API_RESOURCE_API_GROUP: "core.cozystack.io",
CUSTOM_NAMESPACE_API_RESOURCE_API_VERSION: "v1alpha1",
CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME: "tenantnamespaces",
USE_NAMESPACE_NAV: "true",
TITLE_TEXT: "Cozystack Dashboard",
FOOTER_TEXT: "Cozystack",
CUSTOM_TENANT_TEXT: "dev",
THEME_TOKENS_COLORS_DARK: {
"colorText": "rgba(232, 236, 244, 1)",
"colorTextSecondary": "rgba(167, 177, 196, 1)",
"colorTextDisabled": "rgba(109, 119, 136, 1)",
"colorTextTertiary": "rgba(15, 17, 21, 1)",
"colorBgLayout": "rgba(15, 17, 21, 1)",
"colorBgContainer": "rgba(23, 26, 32, 1)",
"colorBgSpotlight": "rgba(30, 34, 43, 1)",
"colorBgElevated": "rgba(35, 40, 50, 1)",
"colorBorder": "rgba(42, 49, 60, 1)",
"colorBgMask": "rgba(0, 0, 0, 0.55)",
"colorPrimaryHover": "rgba(37, 108, 219, 1)",
"colorPrimary": "rgba(58, 134, 255, 1)",
"colorPrimaryActive": "rgba(27, 86, 178, 1)",
"colorPrimaryBg": "rgba(54, 134, 255, 0.13)",
"colorSuccess": "rgba(61, 209, 138, 1)",
"colorWarning": "rgba(245, 165, 36, 1)",
"colorError": "rgba(240, 81, 77, 1)",
"colorInfo": "rgba(108, 166, 255, 1)"
},
THEME_TOKENS_SIZES: {"borderRadius": 6},
THEME_TOKENS_COMPONENTS_LIGHT: {"Layout": {}},
THEME_TOKENS_COMPONENTS_DARK: {"Layout": {}},
THEME_TOKENS_USE_MERGE_STRATEGY: 'false'
}
ENVJS
fi
# --- Prepare BFF ---
if [ ! -f openapi-ui-k8s-bff/.env ]; then
log "Generating openapi-ui-k8s-bff/.env..."
cat > openapi-ui-k8s-bff/.env <<'BFFENV'
DEV_KUBE_API_URL=http://localhost:8080/k8s
BASE_API_GROUP=dashboard.cozystack.io
BASE_API_VERSION=v1alpha1
BASE_NAVIGATION_RESOURCE_PLURAL=navigations
BASE_NAVIGATION_RESOURCE_NAME=navigation
BASE_FRONTEND_PREFIX=/openapi-ui
BASE_NAMESPACE_FULL_PATH=/apis/core.cozystack.io/v1alpha1/tenantnamespaces
BASE_FACTORY_NAMESPACED_API_KEY=base-factory-namespaced-api
BASE_FACTORY_CLUSTERSCOPED_API_KEY=base-factory-clusterscoped-api
BASE_FACTORY_NAMESPACED_BUILTIN_KEY=base-factory-namespaced-builtin
BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY=base-factory-clusterscoped-builtin
BASE_NAMESPACE_FACTORY_KEY=base-factory-clusterscoped-builtin
BASE_ALLOWED_AUTH_HEADERS=user-agent,accept,content-type,application/json,origin,referer,accept-encoding,cookie
BFFENV
fi
# --- Start kubectl proxy + nginx reverse proxy ---
# nginx on :8080 mirrors the production self-proxy architecture:
# /api/clusters/default/* → rewrite → self-proxy back to :8080
# /k8s/* → kubectl proxy on :8081
# /openapi-bff* → BFF on :4002
# /* → Vite dev server on :4001
log "Starting kubectl proxy on :8081..."
kubectl proxy --port=8081 &
log "Starting nginx reverse proxy on :8080..."
nginx -c "$SCRIPT_DIR/dev-nginx.conf" -p "$SCRIPT_DIR" &
# --- Start BFF ---
log "Starting BFF on :4002..."
(cd openapi-ui-k8s-bff && npm run dev) &
# --- Start UI ---
log "Starting UI on :4001..."
(cd openapi-ui && npm run dev) &
echo ""
echo -e "${GREEN}==========================================${NC}"
echo -e "${GREEN} Cozystack Dashboard is running${NC}"
echo -e "${GREEN} http://localhost:8080/openapi-ui${NC}"
echo -e "${GREEN}==========================================${NC}"
echo ""
wait