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 }} + + } } } - } -
+
+ } +