diff --git a/backend/capellacollab/routes.py b/backend/capellacollab/routes.py index bc2b1b1bda..cec52f9b5f 100644 --- a/backend/capellacollab/routes.py +++ b/backend/capellacollab/routes.py @@ -16,7 +16,6 @@ from capellacollab.sessions import routes as sessions_routes from capellacollab.settings import routes as settings_routes from capellacollab.tools import routes as tools_routes -from capellacollab.unifiedconfig import routes as unified_config_routes from capellacollab.users import routes as users_routes log = logging.getLogger(__name__) @@ -30,7 +29,6 @@ tags=["Health"], ) router.include_router(feedback_routes.router, tags=["Feedback"]) -router.include_router(unified_config_routes.router, tags=["Unified Config"]) router.include_router( sessions_routes.router, prefix="/sessions", diff --git a/backend/capellacollab/settings/configuration/models.py b/backend/capellacollab/settings/configuration/models.py index c1cbc1d03f..ef92af9822 100644 --- a/backend/capellacollab/settings/configuration/models.py +++ b/backend/capellacollab/settings/configuration/models.py @@ -12,7 +12,7 @@ from sqlalchemy import orm from capellacollab import core -from capellacollab.core import database +from capellacollab.core import DEVELOPMENT_MODE, database from capellacollab.core import pydantic as core_pydantic from capellacollab.users import models as users_models @@ -69,6 +69,27 @@ class CustomNavbarLink(NavbarLink): ) +class BadgeVariant(str, enum.Enum): + AUTO = "auto" + WARNING = "warning" + SUCCESS = "success" + + +class Badge(core_pydantic.BaseModelStrict): + show: bool = pydantic.Field( + default=True, + description="Show a badge with the current environment.", + ) + variant: BadgeVariant = pydantic.Field( + default=BadgeVariant.AUTO, + description="Color of the badge.", + ) + text: str | t.Literal["auto"] = pydantic.Field( + default="auto", + description="Text to display in the badge. Use 'auto' to display the environment name.", + ) + + class NavbarConfiguration(core_pydantic.BaseModelStrict): external_links: collections_abc.Sequence[ BuiltInNavbarLink | CustomNavbarLink @@ -105,6 +126,14 @@ class NavbarConfiguration(core_pydantic.BaseModelStrict): ), description="Links to display in the navigation bar.", ) + logo_url: str | None = pydantic.Field( + default=None, + description="URL to a logo to display in the navigation bar.", + ) + badge: Badge = pydantic.Field( + default=Badge(show=DEVELOPMENT_MODE), + description="Badge to display in the navigation bar.", + ) class FeedbackIntervalConfiguration(core_pydantic.BaseModelStrict): diff --git a/backend/capellacollab/settings/configuration/unifiedconfig/util.py b/backend/capellacollab/settings/configuration/unifiedconfig/util.py index 0d41003251..e88b36f505 100644 --- a/backend/capellacollab/settings/configuration/unifiedconfig/util.py +++ b/backend/capellacollab/settings/configuration/unifiedconfig/util.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import capellacollab +from capellacollab import core from capellacollab.config import config from capellacollab.settings.configuration import models as config_models from capellacollab.settings.configuration.unifiedconfig import models @@ -38,7 +39,31 @@ def get_feedback( def get_navbar( global_config: config_models.GlobalConfiguration, ) -> config_models.NavbarConfiguration: - return global_config.navbar + navbar_config = global_config.navbar + + if navbar_config.badge.show: + if navbar_config.badge.text == "auto": + if core.CLUSTER_DEVELOPMENT_MODE: + navbar_config.badge.text = "Cluster Development" + elif core.LOCAL_DEVELOPMENT_MODE: + navbar_config.badge.text = "Local Development" + else: + navbar_config.badge.text = ( + global_config.metadata.environment or "Unknown Environment" + ) + + if navbar_config.badge.variant == config_models.BadgeVariant.AUTO: + words = ["dev", "development", "unknown", "staging"] + if any(word in navbar_config.badge.text.lower() for word in words): + navbar_config.badge.variant = ( + config_models.BadgeVariant.WARNING + ) + else: + navbar_config.badge.variant = ( + config_models.BadgeVariant.SUCCESS + ) + + return navbar_config def get_beta( diff --git a/backend/capellacollab/unifiedconfig/__init__.py b/backend/capellacollab/unifiedconfig/__init__.py deleted file mode 100644 index 04412280d8..0000000000 --- a/backend/capellacollab/unifiedconfig/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 diff --git a/backend/capellacollab/unifiedconfig/models.py b/backend/capellacollab/unifiedconfig/models.py deleted file mode 100644 index 526eb179ac..0000000000 --- a/backend/capellacollab/unifiedconfig/models.py +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from capellacollab.core import pydantic as core_pydantic -from capellacollab.settings.configuration import models as config_models - - -class Metadata(core_pydantic.BaseModel): - version: str - privacy_policy_url: str | None - imprint_url: str | None - provider: str | None - authentication_provider: str | None - environment: str | None - - host: str | None - port: str | None - protocol: str | None - - -class UnifiedConfig(core_pydantic.BaseModel): - metadata: Metadata - feedback: config_models.FeedbackConfiguration - navbar: config_models.NavbarConfiguration - beta: config_models.BetaConfiguration diff --git a/backend/capellacollab/unifiedconfig/routes.py b/backend/capellacollab/unifiedconfig/routes.py deleted file mode 100644 index 71be080786..0000000000 --- a/backend/capellacollab/unifiedconfig/routes.py +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -import fastapi -from sqlalchemy import orm - -from capellacollab.core import database -from capellacollab.settings.configuration import core as config_core -from capellacollab.unifiedconfig import models, util - -router = fastapi.APIRouter() - - -@router.get( - "/unified-configuration", -) -def get_unified_config( - db: orm.Session = fastapi.Depends(database.get_db), -) -> models.UnifiedConfig: - cfg = config_core.get_global_configuration(db) - - return models.UnifiedConfig( - metadata=util.get_metadata(cfg), - feedback=util.get_feedback(cfg), - beta=util.get_beta(cfg), - navbar=util.get_navbar(cfg), - ) diff --git a/backend/capellacollab/unifiedconfig/util.py b/backend/capellacollab/unifiedconfig/util.py deleted file mode 100644 index 67de882387..0000000000 --- a/backend/capellacollab/unifiedconfig/util.py +++ /dev/null @@ -1,47 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -import capellacollab -from capellacollab.config import config -from capellacollab.settings.configuration import models as config_models -from capellacollab.unifiedconfig import models - - -def get_metadata( - global_config: config_models.GlobalConfiguration, -) -> models.Metadata: - return models.Metadata.model_validate( - global_config.metadata.model_dump() - | { - "version": capellacollab.__version__, - "host": config.general.host, - "port": str(config.general.port), - "protocol": config.general.scheme, - } - ) - - -def get_feedback( - global_config: config_models.GlobalConfiguration, -) -> config_models.FeedbackConfiguration: - feedback = global_config.feedback - if not (config.smtp and config.smtp.enabled): - feedback.enabled = False - feedback.after_session = False - feedback.on_footer = False - feedback.on_session_card = False - feedback.interval.enabled = False - - return feedback - - -def get_navbar( - global_config: config_models.GlobalConfiguration, -) -> config_models.NavbarConfiguration: - return global_config.navbar - - -def get_beta( - global_config: config_models.GlobalConfiguration, -) -> config_models.BetaConfiguration: - return global_config.beta diff --git a/backend/tests/test_navbar.py b/backend/tests/test_navbar.py new file mode 100644 index 0000000000..895284522f --- /dev/null +++ b/backend/tests/test_navbar.py @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from fastapi import testclient + +from capellacollab import core + + +@pytest.fixture(name="cluster_development_mode") +def fixture_cluster_development_mode(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(core, "CLUSTER_DEVELOPMENT_MODE", True) + monkeypatch.setattr(core, "DEVELOPMENT_MODE", True) + + +@pytest.fixture(name="local_development_mode") +def fixture_local_development_mode(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(core, "LOCAL_DEVELOPMENT_MODE", True) + monkeypatch.setattr(core, "DEVELOPMENT_MODE", True) + + +@pytest.mark.usefixtures("admin", "cluster_development_mode") +def test_cluster_dev_mode( + client: testclient.TestClient, +): + client.put( + "/api/v1/settings/configurations/global", + json={ + "navbar": { + "badge": {"text": "auto", "variant": "auto", "show": True} + } + }, + ) + response = client.get("/api/v1/settings/configurations/unified") + assert response.status_code == 200 + assert response.json()["navbar"]["badge"]["text"] == "Cluster Development" + assert response.json()["navbar"]["badge"]["variant"] == "warning" + + +@pytest.mark.usefixtures( + "admin", + "local_development_mode", +) +def test_local_dev_mode( + client: testclient.TestClient, +): + client.put( + "/api/v1/settings/configurations/global", + json={ + "navbar": { + "badge": {"text": "auto", "variant": "auto", "show": True} + } + }, + ) + response = client.get("/api/v1/settings/configurations/unified") + assert response.status_code == 200 + assert response.json()["navbar"]["badge"]["text"] == "Local Development" + assert response.json()["navbar"]["badge"]["variant"] == "warning" + + +@pytest.mark.usefixtures("admin") +def test_fallback_env_mode(client: testclient.TestClient): + response = client.put( + "/api/v1/settings/configurations/global", + json={ + "metadata": { + "environment": "Fallback Environment", + }, + "navbar": { + "badge": {"text": "auto", "variant": "auto", "show": True} + }, + }, + ) + + assert response.status_code == 200 + + response = client.get("/api/v1/settings/configurations/unified") + assert response.status_code == 200 + assert response.json()["navbar"]["badge"]["text"] == "Fallback Environment" + assert response.json()["navbar"]["badge"]["variant"] == "success" + + +@pytest.mark.usefixtures("admin") +def test_unknown_env_mode( + client: testclient.TestClient, +): + response = client.put( + "/api/v1/settings/configurations/global", + json={ + "metadata": { + "environment": "", + }, + "navbar": { + "badge": {"text": "auto", "variant": "auto", "show": True} + }, + }, + ) + + assert response.status_code == 200 + + response = client.get("/api/v1/settings/configurations/unified") + assert response.status_code == 200 + assert response.json()["navbar"]["badge"]["text"] == "Unknown Environment" + assert response.json()["navbar"]["badge"]["variant"] == "warning" diff --git a/docs/docs/admin/configure-for-your-org.md b/docs/docs/admin/configure-for-your-org.md index 0ae6cec0dc..78977fc4cb 100644 --- a/docs/docs/admin/configure-for-your-org.md +++ b/docs/docs/admin/configure-for-your-org.md @@ -27,11 +27,11 @@ metadata: environment: '-' ``` -## Navigation Bar +## Logo and Navigation Bar -You can edit the links in the navigation bar. This can be useful if you want to -link to external resources or if you are not using the default monitoring -setup. +You can edit the logo, badge, and links in the navigation bar. This can be +useful to brand the Collaboration Manager for your organization, remind users +which environment they are in, or link to external resources. ```yaml navbar: @@ -45,6 +45,11 @@ navbar: - name: Documentation service: documentation role: user + logo_url: null + badge: + show: true + variant: auto + text: auto ``` In addition to the default service links, you can add your own by using `href` @@ -63,6 +68,15 @@ hide the link from users without the appropriate role, it is not a security feature, and you should make sure that the linked service enforces the necessary access controls. +To show the logo in the navigation bar, set the `logo_url` field to the URL of +the image you want to use. + +The badge can be used to show the environment the user is in. The `variant` +field can be set to `auto` (it will be determined by the environment), +`success`, or `warning`. The `text` field will use the environment name if set +to `auto`, or you can specify a custom text. If you don't want to show the +badge, set `show` to `false`. + ## Feedback !!! info "Configure SMTP server for feedback" diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts index 0f2f946b44..368042f87e 100644 --- a/frontend/.storybook/main.ts +++ b/frontend/.storybook/main.ts @@ -23,6 +23,7 @@ const config: StorybookConfig = { docs: { autodocs: 'tag', }, + staticDirs: [{ from: './test-assets', to: '/test-assets' }], core: { disableTelemetry: true, enableCrashReports: false, diff --git a/frontend/.storybook/test-assets/narrow_logo.svg b/frontend/.storybook/test-assets/narrow_logo.svg new file mode 100644 index 0000000000..2efe7e65f2 --- /dev/null +++ b/frontend/.storybook/test-assets/narrow_logo.svg @@ -0,0 +1,9 @@ + + + diff --git a/frontend/.storybook/test-assets/wide_logo.svg b/frontend/.storybook/test-assets/wide_logo.svg new file mode 100644 index 0000000000..f338a4794e --- /dev/null +++ b/frontend/.storybook/test-assets/wide_logo.svg @@ -0,0 +1,9 @@ + + + diff --git a/frontend/src/app/general/header/header.component.html b/frontend/src/app/general/header/header.component.html index eff6ada2e7..49e40531cd 100644 --- a/frontend/src/app/general/header/header.component.html +++ b/frontend/src/app/general/header/header.component.html @@ -17,11 +17,10 @@