Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow customizing Nav URLs #1696

Merged
merged 1 commit into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)):
zusorio marked this conversation as resolved.
Show resolved Hide resolved
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 @@
<!--
zusorio marked this conversation as resolved.
Show resolved Hide resolved
~ 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
Loading