diff --git a/Makefile b/Makefile index 1a4e05f66c..af1d6cf873 100644 --- a/Makefile +++ b/Makefile @@ -149,6 +149,7 @@ build-frontend-dev: build-backend: @echo "$(BLUE)🔨 Building backend for $(CURRENT_PLATFORM)...$(NC)" @mkdir -p $(BIN_DIR) + cd src/jetstream && go generate ./... cd src/jetstream && \ go build \ -ldflags "-X main.appVersion=$(VERSION) -X main.buildDate=$(BUILD_DATE) -X main.gitCommit=$(GIT_COMMIT)" \ diff --git a/README.md b/README.md index 20d4859a7d..f827e0ed68 100644 --- a/README.md +++ b/README.md @@ -159,23 +159,42 @@ Note: `sgs` creates security groups the first time, upgrades do not use `sgs`. ## Packaging -Golang is required, and version 1.21 is recommended as this is the version used by the Stratos build system. +Requires `node`, `go`, and `zip`. -When you want to build the `4.8.1` tag in -[Stratos UI releases](https://github.com/cloudfoundry/stratos/releases), -run this command: +Build and package for all targets (linux/amd64, linux/arm64): ```bash -./bin/package +bin/package ``` -OR to package a specific tag + +To skip the build step and package from existing artifacts: + +```bash +bin/package --skip-build +``` + +To override the version (default: from `package.json`): + +```bash +VERSION="4.8.1" bin/package +``` + +This produces per-target zips in `dist/`: + +``` +dist/stratos-cf---.zip +``` + +Deploy with: + ```bash -TAG="4.8.1" ./bin/package +cf push -f dist/cf-package--/manifest.yml \ + -p dist/stratos-cf---.zip ``` ### NOTE -The original code for this feature can be found in the -[Orange Cloud foundry Github Repository](https://github.com/orange-cloudfoundry/stratos-ui-cf-packager/). +The original packaging code was based on work from the +[Orange Cloud Foundry Github Repository](https://github.com/orange-cloudfoundry/stratos-ui-cf-packager/). Many thanks to Benjamin & Arthur, we appreciate you both! ## License diff --git a/bin/package b/bin/package index e107cf4fae..4c7eb74aa5 100755 --- a/bin/package +++ b/bin/package @@ -1,134 +1,190 @@ #!/usr/bin/env bash -# shellcheck disable=SC2317 - -set -xeuo pipefail - -declare git_url git_tag work_dir -declare -x TAG NODE_VERSION TMP_DIR - -git_url="https://github.com/cloudfoundry/stratos.git" -git_tag="${TAG:-develop}" -work_dir="${PWD}" -NODE_VERSION="${NODE_VERISON:-24.10.0}" -NODE_SHA2="${NODE_SHA2:-TBD_NEED_TO_LOOKUP_FROM_CF_BUILDPACKS}" -TMP_DIR=/tmp - -node::install() { - local download_file - download_file="${TMP_DIR}/node${NODE_VERSION}.tar.gz" - - export node_install_dir="/tmp/node${NODE_VERSION}" - #export node_dir="${node_install_dir}/node-v${NODE_VERSION}-linux-x64" - - mkdir -p "${node_install_dir}" - - if [[ ! -f "${node_install_dir}/bin/node" ]]; then -# - name: node -# version: 20.13.1 -# uri: https://buildpacks.cloudfoundry.org/dependencies/node/node_20.13.1_linux_x64_cflinuxfs4_71ec5c92.tgz -# sha256: 71ec5c92b35770170dad21ff65130f3e4201e8a0bcd32986e5dcf32b57f379e6 -# cf_stacks: -# - cflinuxfs4 -# source: https://nodejs.org/dist/v20.13.1/node-v20.13.1.tar.gz -# source_sha256: a85ee53aa0a5c2f5ca94fa414cdbceb91eb7d18a77fc498358512c14cc6c6991 - - URL=https://buildpacks.cloudfoundry.org/dependencies/node/node_${NODE_VERSION}_linux_x64_cflinuxfs4_71ec5c92.tgz - - echo "-----> Download Nodejs ${NODE_VERSION}" - curl -s -L --retry 15 --retry-delay 2 "$URL" -o "${download_file}" - - DOWNLOAD_SHA2=$(sha256sum "${download_file}" | cut -d ' ' -f 1) - - if [[ ${DOWNLOAD_SHA2} != "${NODE_SHA2}" ]]; then - echo " **ERROR** MD5 mismatch: got $DOWNLOAD_SHA2 expected $NODE_SHA2" - exit 1 - fi - - tar xzf "${download_file}" -C "${node_install_dir}" - rm "${download_file}" +# +# Package Stratos for Cloud Foundry deployment. +# +# Builds the frontend and backend for all platforms, then produces +# per-architecture zips deployable with: +# cf push -f dist/cf-package--/manifest.yml -p dist/stratos-cf---.zip +# +# Usage: +# bin/package # build + package +# bin/package --skip-build # package from existing build artifacts +# +# Environment: +# VERSION - override version (default: from package.json) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +cd "${ROOT_DIR}" + +VERSION="${VERSION:-$(node -p "require('./package.json').version" 2>/dev/null || echo "dev")}" +SKIP_BUILD="${1:-}" + +DIST_DIR="${ROOT_DIR}/dist" +GOOS="linux" +CF_TARGETS=("${GOOS}:amd64" "${GOOS}:arm64") + +log() { echo "-----> $1"; } +error() { echo "ERROR: $1" >&2; exit 1; } + +# ------------------------------------------------------------------- +# Preflight checks +# ------------------------------------------------------------------- +preflight() { + local missing=() + command -v node >/dev/null || missing+=(node) + command -v go >/dev/null || missing+=(go) + command -v zip >/dev/null || missing+=(zip) + + if [[ ${#missing[@]} -gt 0 ]]; then + error "Required tools not found: ${missing[*]}" fi - - if [[ ! -f "${node_install_dir}/bin/node" ]]; then - echo " **ERROR** Could not download nodejs" - exit 1 +} + +# ------------------------------------------------------------------- +# Build everything via Makefile +# ------------------------------------------------------------------- +build() { + if [[ "${SKIP_BUILD}" == "--skip-build" ]]; then + log "Skipping build (--skip-build)" + return fi - - export NODE_HOME="${node_install_dir}" + + log "Building frontend and backend for all platforms..." + make build-frontend + make build-backend-all } +# ------------------------------------------------------------------- +# Find the frontend build output directory +# ------------------------------------------------------------------- +find_ui_source() { + if [[ -d "${DIST_DIR}/browser" ]]; then + echo "${DIST_DIR}/browser" + return + fi + if [[ -d "${DIST_DIR}/stratos" ]]; then + echo "${DIST_DIR}/stratos" + return + fi + for item in "${DIST_DIR}"/*; do + base="$(basename "$item")" + case "${base}" in + jetstream*|cf-package*|release|bin|"stratos-cf-"*) continue ;; + esac + if [[ -d "$item" ]]; then + echo "$item" + return + fi + done + error "Frontend build output not found in ${DIST_DIR}/" +} + +# ------------------------------------------------------------------- +# Stage and archive one architecture +# ------------------------------------------------------------------- +package_target() { + local os=$1 + local arch=$2 + local binary="${DIST_DIR}/bin/jetstream-${os}-${arch}" + local pkg_dir="${DIST_DIR}/cf-package-${os}-${arch}" + local zip_file="${DIST_DIR}/stratos-cf-${VERSION}-${os}-${arch}.zip" + + if [[ ! -f "${binary}" ]]; then + error "Backend binary not found: ${binary}" + fi + + log "Staging CF package (${os}/${arch})..." + rm -rf "${pkg_dir}" + mkdir -p "${pkg_dir}" -if [[ -z ${USE_LOCAL:-""} ]] ; then + # Backend binary + cp "${binary}" "${pkg_dir}/jetstream" + chmod +x "${pkg_dir}/jetstream" - git clone "${git_url}" stratos-ui || true + # Frontend assets — backend serves from ui/ + local ui_src + ui_src="$(find_ui_source)" + cp -r "${ui_src}" "${pkg_dir}/ui" - if [[ -n ${git_tag} ]]; then - pushd stratos-ui - git checkout "${git_tag}" - export stratos_version="${git_tag}" - popd + # Config + cp "${ROOT_DIR}/deploy/cloud-foundry/config.properties" "${pkg_dir}/" + + # Plugins + if [[ -f "${ROOT_DIR}/src/jetstream/plugins.yaml" ]]; then + cp "${ROOT_DIR}/src/jetstream/plugins.yaml" "${pkg_dir}/" + fi + + # User invite templates + if [[ -d "${ROOT_DIR}/src/jetstream/templates" ]]; then + cp -r "${ROOT_DIR}/src/jetstream/templates" "${pkg_dir}/templates" fi -else - echo "Using local checked out copy on stratos-ui" -fi - -if [[ -n ${VERSION:-""} ]] ; then - export stratos_version="${VERSION}" # Will be tagged on publish in Concourse -fi - -exit_trap() { - # See: install_nodejs.sh - [[ -d "${TMP_DIR}/node${NODE_VERSION}" ]] && rm -rf "${TMP_DIR}/node${NODE_VERSION}" - [[ -f "${TMP_DIR}/node${NODE_VERSION}.tar.gz" ]] && rm -rf "${TMP_DIR}/node${NODE_VERSION}.tar.gz" - true + + # Procfile for CF + echo "web: ./jetstream" > "${pkg_dir}/Procfile" + + # CF manifest for pre-built package + cat > "${pkg_dir}/manifest.yml" <-/manifest.yml -p dist/stratos-cf-${VERSION}--.zip" + echo "" } -trap exit_trap EXIT - -if ! which npm > /dev/null; then - node::install - export PATH="${NODE_HOME}/bin:$PATH" -else - npm_location="$(which npm)" - export NODE_HOME="${npm_location%%/bin/npm}" -fi - -mkdir -p cache -build_dir="${work_dir}/stratos-ui" - -# Fix the "authenticity of host can't be established" error during build -#ssh-keyscan "bitbucket.org" >> ~/.ssh/known_hosts - -# prebuild ui -cd stratos-ui -if [[ -n "${stratos_version:-""}" ]]; then - sed -i package.json -e 's/"version": ".*",$/"version": "'"$stratos_version"'",/' -fi -npm install --legacy-peer-deps -npm run prebuild-ui -rm -Rf ./dist - -# Actually build Stratos -bash -x deploy/cloud-foundry/build.sh "${build_dir}" "${work_dir}/cache" -cd "${work_dir}" - -# Remove build artifacts (node_modules & bower_components) -if [[ -d "${build_dir}/node_modules" ]]; then - rm -rf "${build_dir}/node_modules" -fi - -if [[ -d "${build_dir}/bower_components" ]]; then - rm -rf "${build_dir}/bower_components" -fi - -echo "web: ./deploy/cloud-foundry/start.sh" > "${build_dir}/Procfile" - -ls -lah "${build_dir}" -cd "${build_dir}" -package_version="${stratos_version:-"dev-$(date +%Y%m%d%H%M%S)"}" -if [[ -n ${RELEASE_DIR:-""} ]] ; then - zip -r -x@exclude.lst "${work_dir}/${RELEASE_DIR}/stratos-ui-${package_version}.zip" ./* -else - zip -r -x@exclude.lst "${work_dir}/stratos-ui-${package_version}.zip" ./* -fi -cd "${work_dir}" - -exit 0 + +# ------------------------------------------------------------------- +# Main +# ------------------------------------------------------------------- +preflight +build + +for target in "${CF_TARGETS[@]}"; do + os="${target%%:*}" + arch="${target##*:}" + package_target "${os}" "${arch}" +done + +summary diff --git a/bin/package.old b/bin/package.old new file mode 100755 index 0000000000..e107cf4fae --- /dev/null +++ b/bin/package.old @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2317 + +set -xeuo pipefail + +declare git_url git_tag work_dir +declare -x TAG NODE_VERSION TMP_DIR + +git_url="https://github.com/cloudfoundry/stratos.git" +git_tag="${TAG:-develop}" +work_dir="${PWD}" +NODE_VERSION="${NODE_VERISON:-24.10.0}" +NODE_SHA2="${NODE_SHA2:-TBD_NEED_TO_LOOKUP_FROM_CF_BUILDPACKS}" +TMP_DIR=/tmp + +node::install() { + local download_file + download_file="${TMP_DIR}/node${NODE_VERSION}.tar.gz" + + export node_install_dir="/tmp/node${NODE_VERSION}" + #export node_dir="${node_install_dir}/node-v${NODE_VERSION}-linux-x64" + + mkdir -p "${node_install_dir}" + + if [[ ! -f "${node_install_dir}/bin/node" ]]; then +# - name: node +# version: 20.13.1 +# uri: https://buildpacks.cloudfoundry.org/dependencies/node/node_20.13.1_linux_x64_cflinuxfs4_71ec5c92.tgz +# sha256: 71ec5c92b35770170dad21ff65130f3e4201e8a0bcd32986e5dcf32b57f379e6 +# cf_stacks: +# - cflinuxfs4 +# source: https://nodejs.org/dist/v20.13.1/node-v20.13.1.tar.gz +# source_sha256: a85ee53aa0a5c2f5ca94fa414cdbceb91eb7d18a77fc498358512c14cc6c6991 + + URL=https://buildpacks.cloudfoundry.org/dependencies/node/node_${NODE_VERSION}_linux_x64_cflinuxfs4_71ec5c92.tgz + + echo "-----> Download Nodejs ${NODE_VERSION}" + curl -s -L --retry 15 --retry-delay 2 "$URL" -o "${download_file}" + + DOWNLOAD_SHA2=$(sha256sum "${download_file}" | cut -d ' ' -f 1) + + if [[ ${DOWNLOAD_SHA2} != "${NODE_SHA2}" ]]; then + echo " **ERROR** MD5 mismatch: got $DOWNLOAD_SHA2 expected $NODE_SHA2" + exit 1 + fi + + tar xzf "${download_file}" -C "${node_install_dir}" + rm "${download_file}" + fi + + if [[ ! -f "${node_install_dir}/bin/node" ]]; then + echo " **ERROR** Could not download nodejs" + exit 1 + fi + + export NODE_HOME="${node_install_dir}" +} + + +if [[ -z ${USE_LOCAL:-""} ]] ; then + + git clone "${git_url}" stratos-ui || true + + if [[ -n ${git_tag} ]]; then + pushd stratos-ui + git checkout "${git_tag}" + export stratos_version="${git_tag}" + popd + fi +else + echo "Using local checked out copy on stratos-ui" +fi + +if [[ -n ${VERSION:-""} ]] ; then + export stratos_version="${VERSION}" # Will be tagged on publish in Concourse +fi + +exit_trap() { + # See: install_nodejs.sh + [[ -d "${TMP_DIR}/node${NODE_VERSION}" ]] && rm -rf "${TMP_DIR}/node${NODE_VERSION}" + [[ -f "${TMP_DIR}/node${NODE_VERSION}.tar.gz" ]] && rm -rf "${TMP_DIR}/node${NODE_VERSION}.tar.gz" + true +} +trap exit_trap EXIT + +if ! which npm > /dev/null; then + node::install + export PATH="${NODE_HOME}/bin:$PATH" +else + npm_location="$(which npm)" + export NODE_HOME="${npm_location%%/bin/npm}" +fi + +mkdir -p cache +build_dir="${work_dir}/stratos-ui" + +# Fix the "authenticity of host can't be established" error during build +#ssh-keyscan "bitbucket.org" >> ~/.ssh/known_hosts + +# prebuild ui +cd stratos-ui +if [[ -n "${stratos_version:-""}" ]]; then + sed -i package.json -e 's/"version": ".*",$/"version": "'"$stratos_version"'",/' +fi +npm install --legacy-peer-deps +npm run prebuild-ui +rm -Rf ./dist + +# Actually build Stratos +bash -x deploy/cloud-foundry/build.sh "${build_dir}" "${work_dir}/cache" +cd "${work_dir}" + +# Remove build artifacts (node_modules & bower_components) +if [[ -d "${build_dir}/node_modules" ]]; then + rm -rf "${build_dir}/node_modules" +fi + +if [[ -d "${build_dir}/bower_components" ]]; then + rm -rf "${build_dir}/bower_components" +fi + +echo "web: ./deploy/cloud-foundry/start.sh" > "${build_dir}/Procfile" + +ls -lah "${build_dir}" +cd "${build_dir}" +package_version="${stratos_version:-"dev-$(date +%Y%m%d%H%M%S)"}" +if [[ -n ${RELEASE_DIR:-""} ]] ; then + zip -r -x@exclude.lst "${work_dir}/${RELEASE_DIR}/stratos-ui-${package_version}.zip" ./* +else + zip -r -x@exclude.lst "${work_dir}/stratos-ui-${package_version}.zip" ./* +fi +cd "${work_dir}" + +exit 0 diff --git a/build/build-orchestrator.js b/build/build-orchestrator.js index 21127c56e8..b7fc1cef52 100755 --- a/build/build-orchestrator.js +++ b/build/build-orchestrator.js @@ -27,7 +27,7 @@ const tools = [ script: '../src/frontend/packages/devkit/src/backend.ts', required: true, description: 'Generates extra_plugins.go for Jetstream backend', - watchPaths: ['src/frontend/packages/*/package.json'], + watchPaths: ['src/jetstream/plugin-config.yaml'], useTsx: true // Run with tsx for TypeScript support }, { diff --git a/build/cross-compile.sh b/build/cross-compile.sh index f6ac52e99e..9dc6d20082 100755 --- a/build/cross-compile.sh +++ b/build/cross-compile.sh @@ -25,6 +25,8 @@ mkdir -p dist/bin cd src/jetstream +go generate ./... + for platform in "${PLATFORMS[@]}"; do GOOS=${platform%/*} GOARCH=${platform#*/} diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md new file mode 100644 index 0000000000..2a3539771b --- /dev/null +++ b/docs/plugin-architecture.md @@ -0,0 +1,403 @@ +# Plugin Architecture + +Stratos uses a plugin system to extend both the backend (Jetstream) and +frontend (Angular) with modular, independently loadable capabilities. +Plugins add endpoint types, HTTP routes, middleware, and frontend UI +packages to the console. + +## Overview + +``` +plugin-config.yaml (single source of truth) +┌──────────────────────────────────────────┐ +│ plugins: │ +│ - cloudfoundry │ +│ - cfapppush, cfappssh, userinvite │ +│ - autoscaler │ +│ - kubernetes, analysis, monocular │ +└──────────┬──────────────────┬────────────┘ + │ │ + go generate frontend prebuild + │ │ + ▼ ▼ + extra_plugins.go extra_plugins.go + (backend build) (frontend build) +``` + +## Backend Plugins + +### Plugin Interface + +Every backend plugin implements `api.StratosPlugin` +(`src/jetstream/api/plugin.go`): + +```go +type StratosPlugin interface { + Init() error + GetMiddlewarePlugin() (MiddlewarePlugin, error) + GetEndpointPlugin() (EndpointPlugin, error) + GetRoutePlugin() (RoutePlugin, error) +} +``` + +A plugin returns a concrete implementation from one or more of these +methods and `nil` for the rest. The sub-interfaces are: + +| Interface | Purpose | Methods | +|-----------|---------|---------| +| `EndpointPlugin` | Defines an endpoint type (CF, K8s, metrics) | `GetType`, `Register`, `Connect`, `Validate`, `Info`, `UpdateMetadata` | +| `RoutePlugin` | Adds HTTP routes to the Echo server | `AddAdminGroupRoutes`, `AddSessionGroupRoutes` | +| `MiddlewarePlugin` | Injects request/response middleware | `EchoMiddleware`, `SessionEchoMiddleware` | + +Two optional interfaces provide additional lifecycle hooks: + +| Interface | Purpose | +|-----------|---------| +| `StratosPluginCleanup` | Called on shutdown (`Destroy()`) | +| `EndpointNotificationPlugin` | Notified when endpoints are added/removed | + +A `ConfigPlugin` interface allows early configuration before plugin +loading via `RegisterJetstreamConfigPlugin()`. + +### Registration + +Plugins register themselves in a Go `init()` function using blank +imports. The `init()` function calls `api.AddPlugin` with the plugin +name, an optional dependency list, and an initializer function: + +```go +package cloudfoundry + +func init() { + api.AddPlugin("cloudfoundry", nil, Init) +} + +func Init(portalProxy api.PortalProxy) (api.StratosPlugin, error) { + return &CloudFoundrySpecification{portalProxy: portalProxy}, nil +} +``` + +Plugins that depend on other plugins declare them explicitly: + +```go +func init() { + api.AddPlugin("cfapppush", []string{"cloudfoundry"}, Init) +} +``` + +The loader in `load_plugins.go` resolves dependencies recursively. +A plugin with an unmet dependency is skipped with a log warning. + +### Import Files + +Two Go files control which plugins are compiled into the binary: + +| File | Purpose | Maintained by | +|------|---------|---------------| +| `src/jetstream/default_plugins.go` | Core plugins always included | Developer (manual) | +| `src/jetstream/extra_plugins.go` | Feature plugins from plugin-config.yaml | Build system (auto-generated) | +| `src/jetstream/plugin-config.yaml` | Extra plugin list (single source of truth) | Developer (manual) | + +**`default_plugins.go`** — hardcoded imports for plugins that every +deployment needs regardless of which frontend packages are enabled: + +```go +import ( + _ "github.com/cloudfoundry/stratos/src/jetstream/plugins/backup" + _ "github.com/cloudfoundry/stratos/src/jetstream/plugins/cloudfoundryhosting" + _ "github.com/cloudfoundry/stratos/src/jetstream/plugins/metrics" + _ "github.com/cloudfoundry/stratos/src/jetstream/plugins/userfavorites" + _ "github.com/cloudfoundry/stratos/src/jetstream/plugins/userinfo" +) +``` + +**`extra_plugins.go`** — auto-generated by the prebuild step. **Do not +edit manually.** See [Cross-Build Dependency](#cross-build-dependency) +for details. + +### Plugin Loading Order + +``` +main() → portalProxy.loadPlugins() + 1. yamlgenerated.MakePluginsFromConfig() ← reads plugins.yaml + 2. for each name in api.PluginInits: + addPlugin(name) + → resolve dependencies (recursive) + → call Init(portalProxy) + → store in pp.Plugins map +``` + +## Plugin Catalog + +### Default Plugins + +These are always compiled in via `default_plugins.go`. + +| Plugin | Type | Purpose | +|--------|------|---------| +| **backup** | Route | Endpoint and token backup/restore (admin-only) | +| **cloudfoundryhosting** | Middleware, Config | Stratos-in-CF deployment support — auto-registration, session affinity, CF UAA config | +| **metrics** | Endpoint, Route | Prometheus endpoint type; correlates metrics with CF and K8s endpoints | +| **userfavorites** | Route | User favorites/bookmarks persistence in the database | +| **userinfo** | Route | User profile management — get/update info and password; supports UAA, local, and no-auth modes | + +### Extra Plugins + +These are compiled in via the auto-generated `extra_plugins.go` based +on frontend package declarations. + +| Plugin | Depends On | Frontend Package | Purpose | +|--------|-----------|-----------------|---------| +| **cloudfoundry** | — | cloud-foundry | CF endpoint type, auth, firehose and app stream endpoints | +| **cfapppush** | cloudfoundry | cloud-foundry | Application deployment/push workflow from the UI | +| **cfappssh** | cloudfoundry | cloud-foundry | SSH into CF app instances via WebSocket proxy | +| **userinvite** | cloudfoundry | cloud-foundry | User invitation system with email and UAA client auth | +| **autoscaler** | cloudfoundry | cf-autoscaler | Autoscaling policies, metrics, and events for CF apps | +| **kubernetes** | — | kubernetes | K8s endpoint type with multi-auth (GKE, AWS, Azure, OIDC, cert, token) | +| **analysis** | kubernetes | kubernetes | Cluster analysis and reporting via Popeye (tech preview) | +| **monocular** | kubernetes | kubernetes | Helm chart repository management and ArtifactHub integration | + +### Special Plugins + +| Plugin | Purpose | +|--------|---------| +| **desktop** | Desktop hosting mode — local endpoint/token store overlay; declared by `desktop-extensions` frontend package | +| **yamlgenerated** | Generates endpoint type plugins at runtime from `plugins.yaml` (currently GitHub and GitLab git endpoints) | + +### Dependency Graph + +``` +cloudfoundry +├── cfapppush +├── cfappssh +├── userinvite +└── autoscaler + +kubernetes +├── analysis +└── monocular + +(no dependencies) +├── backup +├── cloudfoundryhosting +├── metrics +├── userfavorites +├── userinfo +└── desktop +``` + +## YAML-Generated Plugins + +`src/jetstream/plugins.yaml` defines lightweight endpoint types that +are created at runtime by the `yamlgenerated` plugin. These do not +require a Go source directory — they are configured entirely via YAML: + +```yaml +- name: git + sub_type: github + auth_type: Token + user_info: /user + user_info_path: login + +- name: git + sub_type: gitlab + auth_type: Bearer + user_info: /user + user_info_path: username +``` + +Each entry generates a plugin that implements `EndpointPlugin` and +`RoutePlugin` with standard connect/validate/info flows. This is the +preferred way to add simple token-authenticated endpoint types without +writing Go code. + +## Frontend Integration + +### Frontend Package Metadata + +Frontend packages declare their Angular integration metadata in +`package.json` under the `stratos` key: + +```json +{ + "name": "@stratosui/cloud-foundry", + "stratos": { + "module": "CloudFoundryPackageModule", + "routingModule": "CloudFoundryRoutingModule", + "theming": "sass/_all-theme#apply-theme-stratos-cloud-foundry" + } +} +``` + +The `stratos` metadata fields are: + +| Field | Purpose | +|-------|---------| +| `module` | Angular standalone module class name | +| `routingModule` | Routing module for lazy-loaded routes | +| `theming` | SASS theme file and mixin to apply | +| `assets` | Asset configuration | + +Backend plugin declarations are managed centrally in +`src/jetstream/plugin-config.yaml`, not in individual package.json +files. + +## Plugin Configuration + +The list of extra plugins is defined in +`src/jetstream/plugin-config.yaml`, which serves as the single source +of truth for both the backend and frontend builds. + +### Backend Build (go generate) + +The backend uses a standard `go generate` directive to produce +`extra_plugins.go` from `plugin-config.yaml`: + +``` +cd src/jetstream && go generate ./... + └── cmd/gen-plugins/main.go + → reads plugin-config.yaml + → validates plugin dirs exist + → writes extra_plugins.go +``` + +The Makefile's `build-backend` target runs `go generate` automatically +before `go build`. + +### Frontend Build (prebuild) + +The frontend prebuild pipeline also reads `plugin-config.yaml` to +generate the same `extra_plugins.go`: + +``` +npm prebuild + └── build-orchestrator.js + └── backend.ts → reads plugin-config.yaml + → validates plugin dirs exist + → writes extra_plugins.go + +npm build (ng build) + └── Angular CLI compiles frontend +``` + +### Decoupled Builds + +Either build can run independently. The backend only needs Go tooling +(`go generate ./...`). The frontend prebuild reads the same YAML file +with Node.js. Both produce identical `extra_plugins.go` output. + +The `stratos.yaml` `backend` key is still supported as an override +mechanism for custom builds. + +## Creating a New Plugin + +### Backend Plugin + +```bash +# 1. Create plugin directory and main file +mkdir -p src/jetstream/plugins/myplugin +``` + +```go +// src/jetstream/plugins/myplugin/main.go +package myplugin + +import ( + "github.com/cloudfoundry/stratos/src/jetstream/api" + "github.com/labstack/echo/v4" +) + +func init() { + api.AddPlugin("myplugin", nil, Init) +} + +type MyPlugin struct { + portalProxy api.PortalProxy +} + +func Init(portalProxy api.PortalProxy) (api.StratosPlugin, error) { + return &MyPlugin{portalProxy: portalProxy}, nil +} + +func (p *MyPlugin) Init() error { return nil } + +func (p *MyPlugin) GetMiddlewarePlugin() (api.MiddlewarePlugin, error) { + return nil, errors.New("not implemented") +} + +func (p *MyPlugin) GetEndpointPlugin() (api.EndpointPlugin, error) { + return nil, errors.New("not implemented") +} + +func (p *MyPlugin) GetRoutePlugin() (api.RoutePlugin, error) { + return p, nil +} + +func (p *MyPlugin) AddAdminGroupRoutes(echoGroup *echo.Group) { + // Admin-only routes +} + +func (p *MyPlugin) AddSessionGroupRoutes(echoGroup *echo.Group) { + echoGroup.GET("/myplugin/data", p.getData) +} + +func (p *MyPlugin) getData(c echo.Context) error { + return c.JSON(200, map[string]string{"status": "ok"}) +} +``` + +### Enabling an Extra Plugin + +Add the plugin name to `src/jetstream/plugin-config.yaml`: + +```yaml +plugins: + - cloudfoundry + - cfapppush + # ... + - myplugin +``` + +Run `go generate ./...` from `src/jetstream/` to regenerate +`extra_plugins.go`. + +### Adding a Default Plugin + +If the plugin should always be compiled in (regardless of frontend +packages), add its import to `default_plugins.go` instead: + +```go +import ( + _ "github.com/cloudfoundry/stratos/src/jetstream/plugins/myplugin" +) +``` + +### Adding a YAML-Generated Endpoint + +For simple token-authenticated endpoints, add an entry to +`src/jetstream/plugins.yaml` instead of writing Go code: + +```yaml +- name: myservice + sub_type: myvariant + auth_type: Token # Token, Bearer, or HttpBasic + user_info: /api/user # REST endpoint to fetch user info + user_info_path: username # JSON path to extract username +``` + +## Key Files Reference + +| File | Purpose | +|------|---------| +| `src/jetstream/api/plugin.go` | Plugin interfaces and registration API | +| `src/jetstream/load_plugins.go` | Plugin loader with dependency resolution | +| `src/jetstream/default_plugins.go` | Hardcoded default plugin imports | +| `src/jetstream/extra_plugins.go` | Auto-generated plugin imports (do not edit) | +| `src/jetstream/plugin-config.yaml` | Extra plugin list (single source of truth) | +| `src/jetstream/generate.go` | `go:generate` directive for extra_plugins.go | +| `src/jetstream/cmd/gen-plugins/main.go` | Go generator that reads plugin-config.yaml | +| `src/jetstream/plugins.yaml` | YAML config for runtime-generated endpoint types | +| `src/jetstream/plugins/yamlgenerated/main.go` | YAML plugin generator | +| `src/frontend/packages/devkit/src/backend.ts` | Prebuild script that generates extra_plugins.go from plugin-config.yaml | +| `src/frontend/packages/devkit/src/lib/stratos.config.ts` | Config parser that reads stratos/package metadata | +| `build/build-orchestrator.js` | Prebuild pipeline orchestrator | diff --git a/e2e/helpers/secrets-helpers.ts b/e2e/helpers/secrets-helpers.ts index bce35a9d7f..08c455b1ef 100644 --- a/e2e/helpers/secrets-helpers.ts +++ b/e2e/helpers/secrets-helpers.ts @@ -1,3 +1,4 @@ +import { execSync } from 'child_process'; import * as fs from 'fs'; import * as yaml from 'js-yaml'; import * as path from 'path'; @@ -10,6 +11,46 @@ import * as path from 'path'; export class SecretsHelper { private static SECRETS_FILE = 'secrets.yaml'; + /** + * Resolve a CF org or space GUID using the cf CLI. + * Returns the GUID string or empty string on failure. + */ + private static resolveCfGuid(type: 'org' | 'space', name: string): string { + try { + const cmd = type === 'org' + ? `cf org "${name}" --guid` + : `cf space "${name}" --guid`; + return execSync(cmd, { encoding: 'utf8', timeout: 10000 }).trim(); + } catch { + return ''; + } + } + + /** + * Ensure CF endpoint configs have GUIDs resolved. + * If testOrgGuid/testSpaceGuid are missing but testOrg/testSpace names + * are present, resolve them via the cf CLI. + */ + private static resolveEndpointGuids(cfEndpoints: any[]): any[] { + if (!Array.isArray(cfEndpoints)) return cfEndpoints; + + for (const ep of cfEndpoints) { + if (ep.testOrg && !ep.testOrgGuid) { + ep.testOrgGuid = this.resolveCfGuid('org', ep.testOrg); + } + if (ep.testSpace && !ep.testSpaceGuid) { + // Target the org first so cf space --guid works + if (ep.testOrg) { + try { + execSync(`cf target -o "${ep.testOrg}" > /dev/null 2>&1`, { timeout: 10000 }); + } catch { /* best effort */ } + } + ep.testSpaceGuid = this.resolveCfGuid('space', ep.testSpace); + } + } + return cfEndpoints; + } + /** * Load secrets from secrets.yaml file * Throws error if file doesn't exist or is invalid @@ -27,6 +68,10 @@ export class SecretsHelper { try { const secrets = yaml.load(fs.readFileSync(secretsPath, 'utf8')) as any; + // Get CF endpoints and resolve any missing GUIDs from names + const cfEndpoints = secrets.cloudFoundry || secrets.endpoints?.cf || []; + this.resolveEndpointGuids(cfEndpoints); + return { // Console user credentials console: { @@ -41,8 +86,7 @@ export class SecretsHelper { }, // Cloud Foundry configuration - // Support both 'cloudFoundry' (root level) and 'endpoints.cf' (nested) formats - cloudFoundry: secrets.cloudFoundry || secrets.endpoints?.cf || [], + cloudFoundry: cfEndpoints, // GitHub configuration github: { diff --git a/e2e/scripts/secrets-decrypt.sh b/e2e/scripts/secrets-decrypt.sh new file mode 100755 index 0000000000..5d9c9d99bd --- /dev/null +++ b/e2e/scripts/secrets-decrypt.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Decrypt secrets.yaml.enc → secrets.yaml using openssl aes-256-cbc +# +# Usage: e2e/scripts/secrets-decrypt.sh [passphrase] +# If passphrase is not provided, you will be prompted. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PLAIN="${ROOT_DIR}/secrets.yaml" +ENCRYPTED="${ROOT_DIR}/secrets.yaml.enc" + +if [[ ! -f "${ENCRYPTED}" ]]; then + echo "Error: ${ENCRYPTED} not found" >&2 + exit 1 +fi + +if [[ -f "${PLAIN}" ]]; then + echo "Warning: ${PLAIN} already exists and will be overwritten" + read -r -p "Continue? [y/N] " confirm + if [[ "${confirm}" != [yY] ]]; then + echo "Aborted" + exit 0 + fi +fi + +if [[ -n "${1:-}" ]]; then + openssl enc -aes-256-cbc -d -pbkdf2 -in "${ENCRYPTED}" -out "${PLAIN}" -pass "pass:$1" +else + openssl enc -aes-256-cbc -d -pbkdf2 -in "${ENCRYPTED}" -out "${PLAIN}" +fi + +echo "Decrypted: ${PLAIN}" diff --git a/e2e/scripts/secrets-encrypt.sh b/e2e/scripts/secrets-encrypt.sh new file mode 100755 index 0000000000..9ceb559855 --- /dev/null +++ b/e2e/scripts/secrets-encrypt.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# Encrypt secrets.yaml → secrets.yaml.enc using openssl aes-256-cbc +# +# Usage: e2e/scripts/secrets-encrypt.sh [passphrase] +# If passphrase is not provided, you will be prompted. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PLAIN="${ROOT_DIR}/secrets.yaml" +ENCRYPTED="${ROOT_DIR}/secrets.yaml.enc" + +if [[ ! -f "${PLAIN}" ]]; then + echo "Error: ${PLAIN} not found" >&2 + exit 1 +fi + +if [[ -n "${1:-}" ]]; then + openssl enc -aes-256-cbc -salt -pbkdf2 -in "${PLAIN}" -out "${ENCRYPTED}" -pass "pass:$1" +else + openssl enc -aes-256-cbc -salt -pbkdf2 -in "${PLAIN}" -out "${ENCRYPTED}" +fi + +echo "Encrypted: ${ENCRYPTED}" +echo "You can safely remove ${PLAIN} or keep it in .gitignore" diff --git a/e2e/secrets.yaml.template b/e2e/secrets.yaml.template index 440a72304e..df6ade0173 100644 --- a/e2e/secrets.yaml.template +++ b/e2e/secrets.yaml.template @@ -36,10 +36,11 @@ endpoints: # Test Organization and Space [REQUIRED] # Create with: cf create-org e2e && cf create-space e2e -o e2e + # GUIDs are auto-resolved from names via cf CLI if omitted testOrg: e2e # Test org name - testOrgGuid: # Get with: cf org e2e --guid testSpace: e2e # Test space name - testSpaceGuid: # Get with: cf space e2e --guid + # testOrgGuid: # Optional - auto-resolved from testOrg + # testSpaceGuid: # Optional - auto-resolved from testSpace # Test Service [OPTIONAL] - For service binding tests # testService: local-volume diff --git a/e2e/tests/core/check.spec.ts b/e2e/tests/core/check.spec.ts index 4e26269394..d181ba49b9 100644 --- a/e2e/tests/core/check.spec.ts +++ b/e2e/tests/core/check.spec.ts @@ -1,21 +1,20 @@ import { test, expect } from '../../fixtures/test-base'; import { LoginPage } from '../../pages/login.page'; +import { SSOLoginPage } from '../../pages/sso-login.page'; /** * System Availability Check Tests * Migrated from src/test-e2e/check/check-login-e2e.spec.ts * - * Basic smoke tests to verify system availability + * Basic smoke tests to verify system availability. + * Supports both local (username/password) and SSO (UAA) login flows. */ test.describe('Check Availability of System', () => { - let loginPage: LoginPage; - test.beforeEach(async ({ page }) => { - loginPage = new LoginPage(page); + test('should reach log in page', async ({ page }) => { + const loginPage = new LoginPage(page); await loginPage.navigateTo(); - }); - test('should reach log in page', async ({ page }) => { // Verify we're on the login page expect(await loginPage.isLoginPage()).toBeTruthy(); @@ -24,17 +23,28 @@ test.describe('Check Availability of System', () => { }); test('should be able to login', async ({ page, secrets }) => { - // Enter valid credentials - await loginPage.enterLogin( - secrets.console.admin.username, - secrets.console.admin.password - ); - - // Verify button is enabled - await expect(loginPage.loginButton()).toBeEnabled(); + const loginPage = new LoginPage(page); + await loginPage.navigateTo(); - // Click login - await loginPage.clickLogin(); + // Detect login type: SSO has a submit button but no username input + const hasUsernameInput = await page.locator('input[name="username"]').isVisible({ timeout: 2000 }).catch(() => false); + + if (hasUsernameInput) { + // Local login flow + await loginPage.enterLogin( + secrets.console.admin.username, + secrets.console.admin.password + ); + await expect(loginPage.loginButton()).toBeEnabled(); + await loginPage.clickLogin(); + } else { + // SSO login flow + const ssoPage = new SSOLoginPage(page); + await ssoPage.login( + secrets.console.admin.username, + secrets.console.admin.password + ); + } // Wait for application page await loginPage.waitForApplicationPage(); diff --git a/e2e/tests/core/home-layout.spec.ts b/e2e/tests/core/home-layout.spec.ts new file mode 100644 index 0000000000..987c307033 --- /dev/null +++ b/e2e/tests/core/home-layout.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '../../fixtures/test-base'; + +/** + * Home Page Layout Tests + * Verifies that recent applications data persists across layout changes + * + * Bug: FWT-678 - Timestamps disappear when switching from single to two column + * Root cause: cfhome-card.component.html binds [showDate]="layout.x === 1" + */ +test.describe('Home Page Layout', () => { + + test('recent apps should show timestamps in all column layouts', async ({ connectedEndpointsAdminPage }) => { + const page = connectedEndpointsAdminPage.page; + await page.goto('/home'); + await page.waitForLoadState('networkidle'); + + // Wait for recent apps to load + const recentAppsSection = page.locator('text=Recently updated applications'); + await recentAppsSection.waitFor({ timeout: 15000 }); + + // Open the layout dropdown (column selector button in top right) + const layoutButton = page.locator('button.home-layout-select, [matMenuTriggerFor]').first(); + await layoutButton.click(); + + // Switch to Single Column + await page.locator('button:has-text("Single Column")').click(); + await page.waitForTimeout(1000); + + // Verify timestamps are visible in single column + const appRows = page.locator('app-compact-app-card'); + const firstRowTimestamp = appRows.first().locator('.compact-app-card__date, .app-card-date, time'); + await expect(firstRowTimestamp).toBeVisible({ timeout: 5000 }); + const singleColumnDate = await firstRowTimestamp.textContent(); + expect(singleColumnDate).toBeTruthy(); + + // Switch to Two Column + await layoutButton.click(); + await page.locator('button:has-text("Two Column")').click(); + await page.waitForTimeout(1000); + + // Verify timestamps are still visible in two column layout + const twoColTimestamp = appRows.first().locator('.compact-app-card__date, .app-card-date, time'); + await expect(twoColTimestamp).toBeVisible({ timeout: 5000 }); + const twoColumnDate = await twoColTimestamp.textContent(); + expect(twoColumnDate).toBeTruthy(); + + // Switch back to Single Column and verify timestamps persist + await layoutButton.click(); + await page.locator('button:has-text("Single Column")').click(); + await page.waitForTimeout(1000); + + const revertedTimestamp = appRows.first().locator('.compact-app-card__date, .app-card-date, time'); + await expect(revertedTimestamp).toBeVisible({ timeout: 5000 }); + }); +}); diff --git a/src/frontend/packages/cf-autoscaler/package.json b/src/frontend/packages/cf-autoscaler/package.json index 37b3681e29..6b87b1bdd8 100644 --- a/src/frontend/packages/cf-autoscaler/package.json +++ b/src/frontend/packages/cf-autoscaler/package.json @@ -9,7 +9,6 @@ "stratos": { "module": "CfAutoscalerPackageModule", "routingModule": "CfAutoscalerRoutingModule", - "theming": "sass/_all-theme#apply-theme-stratos-autoscaler", - "backend": ["autoscaler"] + "theming": "sass/_all-theme#apply-theme-stratos-autoscaler" } } diff --git a/src/frontend/packages/cloud-foundry/package.json b/src/frontend/packages/cloud-foundry/package.json index abcfd12d5e..f59ee2b640 100644 --- a/src/frontend/packages/cloud-foundry/package.json +++ b/src/frontend/packages/cloud-foundry/package.json @@ -9,7 +9,6 @@ "stratos": { "module": "CloudFoundryPackageModule", "routingModule": "CloudFoundryRoutingModule", - "theming": "sass/_all-theme#apply-theme-stratos-cloud-foundry", - "backend": ["cloudfoundry", "cfapppush", "cfappssh", "userinvite"] + "theming": "sass/_all-theme#apply-theme-stratos-cloud-foundry" } } diff --git a/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts b/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts index 3cdcb2bb52..de24ae0d3a 100644 --- a/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts +++ b/src/frontend/packages/cloud-foundry/src/cf-entity-generator.ts @@ -273,6 +273,7 @@ export function generateCFEntities(): StratosBaseCatalogEntity[] { }), shortcuts: cfShortcuts, fullView: false, + columnSpan: 2, }, listDetailsComponent: CfEndpointDetailsComponent, renderPriority: 1, diff --git a/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.scss b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.scss index c7102785dc..4e99371fa3 100644 --- a/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.scss +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/card-cf-recent-apps.component.scss @@ -1,5 +1,6 @@ :host { - display: flex; + display: block; + width: 100%; } .recent-apps-card { display: flex; @@ -13,7 +14,10 @@ &__content { display: flex; + flex-direction: column; + flex: 1; overflow-y: auto; + width: 100%; } &__header { diff --git a/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss index 42650bf442..cfc2891649 100644 --- a/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss +++ b/src/frontend/packages/cloud-foundry/src/features/home/card-cf-recent-apps/compact-app-card/compact-app-card.component.scss @@ -1,6 +1,6 @@ :host { border: 1px solid #ccc; - display: flex; + display: block; padding: 0 12px; :host-context(.dark) & { @@ -17,9 +17,10 @@ display: flex; flex-direction: row; font-size: 14px; - height: 40px; + min-height: 40px; width: 100%; color: rgba(0, 0, 0, 0.87); + flex-wrap: wrap; :host-context(.dark) & { color: rgba(255, 255, 255, 0.87); @@ -27,18 +28,24 @@ &__name { flex: 1; + min-width: 0; padding-left: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + &__name a { + color: rgb(37 99 235); // blue-600 - a { - color: rgb(37 99 235); // blue-600 - - :host-context(.dark) & { - color: rgb(96 165 250); // blue-400 - } + :host-context(.dark) & { + color: rgb(96 165 250); // blue-400 } } &__updated { color: rgba(0, 0, 0, 0.6); + white-space: nowrap; + margin-left: auto; + padding-left: 8px; :host-context(.dark) & { color: rgba(255, 255, 255, 0.6); diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html index 8e7629c2bb..56d5031b94 100644 --- a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html @@ -8,7 +8,7 @@ - @if (layout.x === 1) { + @if (layout.x <= 2) { @@ -19,7 +19,7 @@ @if (cardLoaded && recentAppsRows > 0) { + dateMode="subtle" [showDate]="true" [maxRows]="recentAppsRows" mode="plain" [endpoint]="guid"> } @if (!cardLoaded && recentAppsRows > 0) { diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss index a80c13761b..f3464f3e4a 100644 --- a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss @@ -1,8 +1,21 @@ +:host { + display: block; + width: 100%; +} + .cf-home-card { &__plain-tiles { + display: flex !important; + flex-wrap: wrap; + gap: 0.5rem; margin-left: 0; margin-right: 1em; margin-top: 1em; + + > app-tile { + flex: 1 1 120px; + min-width: 120px; + } } &__no-apps { align-items: center; diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.ts b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.ts index 8f73f34312..bcb91ec5a0 100644 --- a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.ts +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.ts @@ -196,11 +196,16 @@ export class CFHomeCardComponent implements HomePageEndpointCard { public updateLayout() { const currentRows = this.recentAppsRows; - this.recentAppsRows = this.layout.y > 1 ? 5 : 10; // Hide recent apps if more than 2 columns if (this.layout.x > 2) { this.recentAppsRows = 0; + } else if (this.layout.y > 1) { + this.recentAppsRows = 5; + } else if (this.layout.x === 2) { + this.recentAppsRows = 7; + } else { + this.recentAppsRows = 10; } // If the layout changes and there are apps to show then we need to fetch the app stats for them @@ -211,6 +216,8 @@ export class CFHomeCardComponent implements HomePageEndpointCard { // Only show the deploy app tiles in the full view this.showDeployAppTiles = this.layout.x === 1 && this.layout.y === 1; + + this.cdr.markForCheck(); } // Fetch the app stats - we fetch two at a time diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html index 9c9913f276..d9f84d936e 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html @@ -10,12 +10,12 @@

Endpoints

(click)="openRegisterModal()" matTooltip="Register Endpoint" > - add + add @if (canBackupRestore$ | async) { } diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts index 7fa9a53ade..de64246af4 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts @@ -40,7 +40,6 @@ import { SnackBarService } from '../../../shared/services/snackbar.service'; import { SessionService } from '../../../shared/services/session.service'; import { EndpointModalService } from '../endpoint-register-modal/endpoint-modal.service'; import { EndpointRegisterModalComponent } from '../endpoint-register-modal/endpoint-register-modal.component'; -import { CustomIconComponent } from '../../../shared/components/custom-material/custom-material.component'; @Component({ selector: 'app-endpoints-page', @@ -55,7 +54,6 @@ import { CustomIconComponent } from '../../../shared/components/custom-material/ imports: [ CommonModule, RouterModule, - CustomIconComponent, CustomTooltipDirective, PageHeaderComponent, ListComponent, diff --git a/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html index 518aac82bc..1fad9362ed 100644 --- a/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html +++ b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html @@ -54,7 +54,7 @@

} @if (!fullView) { -