Skip to content

Commit

Permalink
feat: Allow customizing Nav URLs
Browse files Browse the repository at this point in the history
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
  • Loading branch information
zusorio committed Aug 15, 2024
1 parent d514394 commit 9fa91a9
Show file tree
Hide file tree
Showing 25 changed files with 596 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.models import Role


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: 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=Role.ADMIN,
),
BuiltInNavbarLink(
name="Prometheus",
service=BuiltInLinkItem.PROMETHEUS,
role=Role.ADMIN,
),
BuiltInNavbarLink(
name="Documentation",
service=BuiltInLinkItem.DOCUMENTATION,
role=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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
-->

<mat-list>
@for (item of navBarService.navBarItems; track $index) {
@for (item of navBarService.navbarItems$ | async; track $index) {
<div>
@if (userService.validateUserRole(item.requiredRole)) {
@if (item.href) {
Expand Down
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 { MatDivider } from '@angular/material/divider';
import { MatIcon } from '@angular/material/icon';
Expand All @@ -17,7 +18,7 @@ import { UserWrapperService } from 'src/app/services/user/user.service';
templateUrl: './nav-bar-menu.component.html',
styleUrls: ['./nav-bar-menu.component.css'],
standalone: true,
imports: [MatList, MatListItem, MatIcon, RouterLink, MatDivider],
imports: [MatList, MatListItem, MatIcon, RouterLink, MatDivider, AsyncPipe],
})
export class NavBarMenuComponent {
constructor(
Expand Down
Loading

0 comments on commit 9fa91a9

Please sign in to comment.