From dc9d416029df9c69127b9831e4b34acd3e4226ae Mon Sep 17 00:00:00 2001 From: Tobias Messner Date: Thu, 8 Aug 2024 15:59:54 +0200 Subject: [PATCH] feat: Allow customizing Nav URLs Allow changing the order and text of the existing nav bar links, as well as add completely custom ones. This can be set in the admin interface by editing the configuration. Closes #1668 --- backend/capellacollab/navbar/routes.py | 25 +++ backend/capellacollab/routes.py | 2 + .../settings/configuration/models.py | 56 +++++++ backend/capellacollab/users/models.py | 2 +- .../settings/test_global_configuration.py | 44 +++++ docs/docs/admin/configure-for-your-org.md | 53 ++++++ docs/mkdocs.yml | 1 + .../app/general/header/header.component.html | 57 ++++--- .../app/general/header/header.component.ts | 2 + .../src/app/general/header/header.stories.ts | 49 ++++++ .../nav-bar-menu/nav-bar-menu.component.html | 2 +- .../nav-bar-menu/nav-bar-menu.component.ts | 3 +- .../app/general/nav-bar/nav-bar.service.ts | 68 +++++--- .../src/app/openapi/.openapi-generator/FILES | 7 + frontend/src/app/openapi/api/api.ts | 4 +- .../src/app/openapi/api/navbar.service.ts | 155 ++++++++++++++++++ .../app/openapi/model/built-in-link-item.ts | 21 +++ .../app/openapi/model/built-in-navbar-link.ts | 30 ++++ .../app/openapi/model/custom-navbar-link.ts | 29 ++++ .../model/global-configuration-input.ts | 2 + .../model/global-configuration-output.ts | 2 + frontend/src/app/openapi/model/models.ts | 6 + ...onfiguration-input-external-links-inner.ts | 36 ++++ .../model/navbar-configuration-input.ts | 21 +++ .../model/navbar-configuration-output.ts | 21 +++ .../configuration-settings.component.ts | 3 + 26 files changed, 645 insertions(+), 56 deletions(-) create mode 100644 backend/capellacollab/navbar/routes.py create mode 100644 docs/docs/admin/configure-for-your-org.md create mode 100644 frontend/src/app/openapi/api/navbar.service.ts create mode 100644 frontend/src/app/openapi/model/built-in-link-item.ts create mode 100644 frontend/src/app/openapi/model/built-in-navbar-link.ts create mode 100644 frontend/src/app/openapi/model/custom-navbar-link.ts create mode 100644 frontend/src/app/openapi/model/navbar-configuration-input-external-links-inner.ts create mode 100644 frontend/src/app/openapi/model/navbar-configuration-input.ts create mode 100644 frontend/src/app/openapi/model/navbar-configuration-output.ts diff --git a/backend/capellacollab/navbar/routes.py b/backend/capellacollab/navbar/routes.py new file mode 100644 index 000000000..f6849d9ad --- /dev/null +++ b/backend/capellacollab/navbar/routes.py @@ -0,0 +1,25 @@ +# 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.settings.configuration import ( + models as settings_config_models, +) +from capellacollab.settings.configuration.models import NavbarConfiguration + +router = fastapi.APIRouter() + + +@router.get( + "/navbar", + response_model=NavbarConfiguration, +) +def get_navbar(db: orm.Session = fastapi.Depends(database.get_db)): + cfg = config_core.get_config(db, "global") + assert isinstance(cfg, settings_config_models.GlobalConfiguration) + + return NavbarConfiguration.model_validate(cfg.navbar.model_dump()) diff --git a/backend/capellacollab/routes.py b/backend/capellacollab/routes.py index 067d939f8..656e7894f 100644 --- a/backend/capellacollab/routes.py +++ b/backend/capellacollab/routes.py @@ -11,6 +11,7 @@ from capellacollab.events import routes as events_router from capellacollab.health import routes as health_routes from capellacollab.metadata import routes as core_metadata +from capellacollab.navbar import routes as navbar_routes from capellacollab.notices import routes as notices_routes from capellacollab.projects import routes as projects_routes from capellacollab.sessions import routes as sessions_routes @@ -29,6 +30,7 @@ tags=["Health"], ) router.include_router(core_metadata.router, tags=["Metadata"]) +router.include_router(navbar_routes.router, tags=["Navbar"]) 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 5404af966..36fc6038c 100644 --- a/backend/capellacollab/settings/configuration/models.py +++ b/backend/capellacollab/settings/configuration/models.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import abc +import enum import typing as t import pydantic @@ -9,6 +10,7 @@ from capellacollab.core import database from capellacollab.core import pydantic as core_pydantic +from capellacollab.users import models as users_models class DatabaseConfiguration(database.Base): @@ -37,6 +39,56 @@ class MetadataConfiguration(core_pydantic.BaseModelStrict): environment: str = pydantic.Field(default="-", description="general") +class BuiltInLinkItem(str, enum.Enum): + GRAFANA = "grafana" + PROMETHEUS = "prometheus" + DOCUMENTATION = "documentation" + + +class NavbarLink(core_pydantic.BaseModelStrict): + name: str + role: users_models.Role = pydantic.Field( + description="Role required to see this link.", + ) + + +class BuiltInNavbarLink(NavbarLink): + service: BuiltInLinkItem = pydantic.Field( + description="Built-in service to link to.", + ) + + +class CustomNavbarLink(NavbarLink): + href: str = pydantic.Field( + description="URL to link to.", + ) + + +class NavbarConfiguration(core_pydantic.BaseModelStrict): + external_links: list[BuiltInNavbarLink | CustomNavbarLink] = ( + pydantic.Field( + default=[ + BuiltInNavbarLink( + name="Grafana", + service=BuiltInLinkItem.GRAFANA, + role=users_models.Role.ADMIN, + ), + BuiltInNavbarLink( + name="Prometheus", + service=BuiltInLinkItem.PROMETHEUS, + role=users_models.Role.ADMIN, + ), + BuiltInNavbarLink( + name="Documentation", + service=BuiltInLinkItem.DOCUMENTATION, + role=users_models.Role.USER, + ), + ], + description="Links to display in the navigation bar.", + ) + ) + + class ConfigurationBase(core_pydantic.BaseModelStrict, abc.ABC): """ Base class for configuration models. Can be used to define new configurations @@ -55,6 +107,10 @@ class GlobalConfiguration(ConfigurationBase): default_factory=MetadataConfiguration ) + navbar: NavbarConfiguration = pydantic.Field( + default_factory=NavbarConfiguration + ) + # All subclasses of ConfigurationBase are automatically registered using this dict. NAME_TO_MODEL_TYPE_MAPPING: dict[str, t.Type[ConfigurationBase]] = { diff --git a/backend/capellacollab/users/models.py b/backend/capellacollab/users/models.py index 0aa250916..1ee1ea146 100644 --- a/backend/capellacollab/users/models.py +++ b/backend/capellacollab/users/models.py @@ -20,7 +20,7 @@ from capellacollab.users.tokens.models import DatabaseUserToken -class Role(enum.Enum): +class Role(str, enum.Enum): USER = "user" ADMIN = "administrator" diff --git a/backend/tests/settings/test_global_configuration.py b/backend/tests/settings/test_global_configuration.py index 9a0655dd0..03437295d 100644 --- a/backend/tests/settings/test_global_configuration.py +++ b/backend/tests/settings/test_global_configuration.py @@ -149,3 +149,47 @@ def get_mock_own_user(): response = client.get("/api/v1/metadata") assert response.status_code == 200 assert response.json()["environment"] == "test" + + +def test_navbar_is_updated( + client: testclient.TestClient, + db: orm.Session, + executor_name: str, +): + admin = users_crud.create_user( + db, executor_name, executor_name, None, users_models.Role.ADMIN + ) + + def get_mock_own_user(): + return admin + + app.dependency_overrides[users_injectables.get_own_user] = ( + get_mock_own_user + ) + + response = client.put( + "/api/v1/settings/configurations/global", + json={ + "navbar": { + "external_links": [ + { + "name": "Example", + "href": "https://example.com", + "role": "user", + } + ] + } + }, + ) + + assert response.status_code == 200 + + del app.dependency_overrides[users_injectables.get_own_user] + + response = client.get("/api/v1/navbar") + assert response.status_code == 200 + assert response.json()["external_links"][0] == { + "name": "Example", + "href": "https://example.com", + "role": "user", + } diff --git a/docs/docs/admin/configure-for-your-org.md b/docs/docs/admin/configure-for-your-org.md new file mode 100644 index 000000000..165898bc8 --- /dev/null +++ b/docs/docs/admin/configure-for-your-org.md @@ -0,0 +1,53 @@ + + +# Configure for your Organization + +When running the Collaboration Manager in production, you may want to provide +information about the team responsible for it, as well as an imprint and +privacy policy. + +You can set this information from the configuration page in the admin +interface. Navigate to _Settings_, then _Configuration_, then edit the file to +your liking. + +Here, you can also edit the links in the navigation bar if you are not using +the default monitoring services. + +```yaml +metadata: + privacy_policy_url: https://example.com/privacy + imprint_url: https://example.com/imprint + provider: Systems Engineering Toolchain team + authentication_provider: OAuth2 + environment: '-' +navbar: + external_links: + - name: Grafana + service: grafana + role: administrator + - name: Prometheus + service: prometheus + role: administrator + - name: Documentation + service: documentation + role: user +``` + +In addition to the default service links, you can add your own by using `href` +instead of `service`. + +```yaml +navbar: + external_links: + - name: Example + href: https://example.com + role: user +``` + +The `role` field and can be one of `user` or `administrator`. While this will +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. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 2c82a132c..863559e3b 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -102,6 +102,7 @@ nav: - Image builder: admin/ci-templates/gitlab/image-builder.md - Kubernetes deployment: admin/ci-templates/gitlab/k8s-deploy.md - Command line tool: admin/cli.md + - Configure for your Organization: admin/configure-for-your-org.md - Troubleshooting: admin/troubleshooting.md - Developer Documentation: - Introduction: development/index.md diff --git a/frontend/src/app/general/header/header.component.html b/frontend/src/app/general/header/header.component.html index 629e833d9..e5615555d 100644 --- a/frontend/src/app/general/header/header.component.html +++ b/frontend/src/app/general/header/header.component.html @@ -20,35 +20,38 @@
Capella Collaboration Manager
-
- @for (item of navBarService.navBarItems; track item.name) { - @if (userService.validateUserRole(item.requiredRole)) { - @if (item.href) { - - {{ item.name }} - @if (item.icon) { - {{ item.icon }} - } - - } @else { - - {{ item.name }} - + @if (navBarService.navbarItems$ | async) { +
+ @for (item of navBarService.navbarItems$ | async; track item.name) { + @if (userService.validateUserRole(item.requiredRole)) { + @if (item.href) { + + {{ item.name }} + @if (item.icon) { + {{ item.icon }} + } + + } @else { + + {{ item.name }} + + } } } - } -
+
+ } +