Skip to content

Commit

Permalink
Merge pull request #1696 from DSD-DBS/custom-nav-links
Browse files Browse the repository at this point in the history
feat: Allow customizing Nav URLs
  • Loading branch information
MoritzWeber0 authored Aug 16, 2024
2 parents a2a1902 + dc9d416 commit 964ae6a
Show file tree
Hide file tree
Showing 26 changed files with 645 additions and 56 deletions.
25 changes: 25 additions & 0 deletions backend/capellacollab/navbar/routes.py
Original file line number Diff line number Diff line change
@@ -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())
2 changes: 2 additions & 0 deletions backend/capellacollab/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down
56 changes: 56 additions & 0 deletions backend/capellacollab/settings/configuration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
# SPDX-License-Identifier: Apache-2.0

import abc
import enum
import typing as t

import pydantic
from sqlalchemy import orm

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):
Expand Down Expand Up @@ -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
Expand All @@ -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]] = {
Expand Down
2 changes: 1 addition & 1 deletion backend/capellacollab/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from capellacollab.users.tokens.models import DatabaseUserToken


class Role(enum.Enum):
class Role(str, enum.Enum):
USER = "user"
ADMIN = "administrator"

Expand Down
44 changes: 44 additions & 0 deletions backend/tests/settings/test_global_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
53 changes: 53 additions & 0 deletions docs/docs/admin/configure-for-your-org.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!--
~ SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
~ SPDX-License-Identifier: Apache-2.0
-->

# 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.
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 30 additions & 27 deletions frontend/src/app/general/header/header.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,38 @@
<div class="ml-5 basis-1/3 select-none text-2xl text-primary">
Capella Collaboration Manager
</div>
<div class="flex basis-1/3 justify-center gap-2">
@for (item of navBarService.navBarItems; track item.name) {
@if (userService.validateUserRole(item.requiredRole)) {
@if (item.href) {
<a
mat-flat-button
color="primary"
[attr.href]="item.href"
[attr.target]="item.target"
class=""
>
{{ item.name }}
@if (item.icon) {
<mat-icon iconPositionEnd>{{ item.icon }}</mat-icon>
}
</a>
} @else {
<a
mat-flat-button
color="primary"
[routerLink]="item.routerLink"
class=""
>
{{ item.name }}
</a>
@if (navBarService.navbarItems$ | async) {
<div class="flex basis-1/3 justify-center gap-2">
@for (item of navBarService.navbarItems$ | async; track item.name) {
@if (userService.validateUserRole(item.requiredRole)) {
@if (item.href) {
<a
mat-flat-button
color="primary"
[attr.href]="item.href"
[attr.target]="item.target"
class=""
>
{{ item.name }}
@if (item.icon) {
<mat-icon iconPositionEnd>{{ item.icon }}</mat-icon>
}
</a>
} @else {
<a
mat-flat-button
color="primary"
[routerLink]="item.routerLink"
class=""
>
{{ item.name }}
</a>
}
}
}
}
</div>
</div>
}

<div class="!mr-5 hidden basis-1/3 items-center justify-end gap-2 xl:flex">
<mat-menu #profileMenu="matMenu" class="flex items-center">
<a
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/general/header/header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { MatIconButton, MatAnchor, MatButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
Expand All @@ -27,6 +28,7 @@ import { BreadcrumbsComponent } from '../breadcrumbs/breadcrumbs.component';
MatButton,
MatMenuTrigger,
BreadcrumbsComponent,
AsyncPipe,
],
})
export class HeaderComponent {
Expand Down
Loading

0 comments on commit 964ae6a

Please sign in to comment.