diff --git a/backend/capellacollab/configuration/models.py b/backend/capellacollab/configuration/models.py
index 177d49307..2f3bf4c1f 100644
--- a/backend/capellacollab/configuration/models.py
+++ b/backend/capellacollab/configuration/models.py
@@ -12,7 +12,7 @@
from sqlalchemy import orm
from capellacollab import core
-from capellacollab.core import database
+from capellacollab.core import DEVELOPMENT_MODE, database
from capellacollab.core import pydantic as core_pydantic
from capellacollab.users import models as users_models
@@ -69,6 +69,27 @@ class CustomNavbarLink(NavbarLink):
)
+class BadgeVariant(str, enum.Enum):
+ AUTO = "auto"
+ WARNING = "warning"
+ SUCCESS = "success"
+
+
+class Badge(core_pydantic.BaseModelStrict):
+ show: bool = pydantic.Field(
+ default=True,
+ description="Show a badge with the current environment.",
+ )
+ variant: BadgeVariant = pydantic.Field(
+ default=BadgeVariant.AUTO,
+ description="Color of the badge.",
+ )
+ text: str | t.Literal["auto"] = pydantic.Field(
+ default="auto",
+ description="Text to display in the badge. Use 'auto' to display the environment name.",
+ )
+
+
class NavbarConfiguration(core_pydantic.BaseModelStrict):
external_links: collections_abc.Sequence[
BuiltInNavbarLink | CustomNavbarLink
@@ -105,6 +126,14 @@ class NavbarConfiguration(core_pydantic.BaseModelStrict):
),
description="Links to display in the navigation bar.",
)
+ logo_url: str | None = pydantic.Field(
+ default=None,
+ description="URL to a logo to display in the navigation bar.",
+ )
+ badge: Badge = pydantic.Field(
+ default=Badge(show=DEVELOPMENT_MODE),
+ description="Badge to display in the navigation bar.",
+ )
class FeedbackIntervalConfiguration(core_pydantic.BaseModelStrict):
diff --git a/backend/capellacollab/configuration/routes.py b/backend/capellacollab/configuration/routes.py
index bcda9ab5b..afe858b2a 100644
--- a/backend/capellacollab/configuration/routes.py
+++ b/backend/capellacollab/configuration/routes.py
@@ -35,7 +35,7 @@ def get_unified_config(
metadata=util.get_metadata(cfg),
feedback=util.get_feedback(cfg),
beta=cfg.beta,
- navbar=cfg.navbar,
+ navbar=util.get_navbar(cfg),
)
diff --git a/backend/capellacollab/configuration/util.py b/backend/capellacollab/configuration/util.py
index 13051e68b..156d65514 100644
--- a/backend/capellacollab/configuration/util.py
+++ b/backend/capellacollab/configuration/util.py
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: Apache-2.0
import capellacollab
+from capellacollab import core
from capellacollab.configuration.app import config
from . import models
@@ -33,3 +34,29 @@ def get_feedback(
feedback.interval.enabled = False
return feedback
+
+
+def get_navbar(
+ global_config: models.GlobalConfiguration,
+) -> models.NavbarConfiguration:
+ navbar_config = global_config.navbar
+
+ if navbar_config.badge.show:
+ if navbar_config.badge.text == "auto":
+ if core.CLUSTER_DEVELOPMENT_MODE:
+ navbar_config.badge.text = "Cluster Development"
+ elif core.LOCAL_DEVELOPMENT_MODE:
+ navbar_config.badge.text = "Local Development"
+ else:
+ navbar_config.badge.text = (
+ global_config.metadata.environment or "Unknown Environment"
+ )
+
+ if navbar_config.badge.variant == models.BadgeVariant.AUTO:
+ words = ["dev", "development", "unknown", "staging"]
+ if any(word in navbar_config.badge.text.lower() for word in words):
+ navbar_config.badge.variant = models.BadgeVariant.WARNING
+ else:
+ navbar_config.badge.variant = models.BadgeVariant.SUCCESS
+
+ return navbar_config
diff --git a/backend/tests/test_navbar.py b/backend/tests/test_navbar.py
new file mode 100644
index 000000000..7ab40f4a0
--- /dev/null
+++ b/backend/tests/test_navbar.py
@@ -0,0 +1,104 @@
+# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+# SPDX-License-Identifier: Apache-2.0
+
+import pytest
+from fastapi import testclient
+
+from capellacollab import core
+
+
+@pytest.fixture(name="cluster_development_mode")
+def fixture_cluster_development_mode(monkeypatch: pytest.MonkeyPatch):
+ monkeypatch.setattr(core, "CLUSTER_DEVELOPMENT_MODE", True)
+ monkeypatch.setattr(core, "DEVELOPMENT_MODE", True)
+
+
+@pytest.fixture(name="local_development_mode")
+def fixture_local_development_mode(monkeypatch: pytest.MonkeyPatch):
+ monkeypatch.setattr(core, "LOCAL_DEVELOPMENT_MODE", True)
+ monkeypatch.setattr(core, "DEVELOPMENT_MODE", True)
+
+
+@pytest.mark.usefixtures("admin", "cluster_development_mode")
+def test_cluster_dev_mode(
+ client: testclient.TestClient,
+):
+ client.put(
+ "/api/v1/configurations/global",
+ json={
+ "navbar": {
+ "badge": {"text": "auto", "variant": "auto", "show": True}
+ }
+ },
+ )
+ response = client.get("/api/v1/configurations/unified")
+ assert response.status_code == 200
+ assert response.json()["navbar"]["badge"]["text"] == "Cluster Development"
+ assert response.json()["navbar"]["badge"]["variant"] == "warning"
+
+
+@pytest.mark.usefixtures(
+ "admin",
+ "local_development_mode",
+)
+def test_local_dev_mode(
+ client: testclient.TestClient,
+):
+ client.put(
+ "/api/v1/configurations/global",
+ json={
+ "navbar": {
+ "badge": {"text": "auto", "variant": "auto", "show": True}
+ }
+ },
+ )
+ response = client.get("/api/v1/configurations/unified")
+ assert response.status_code == 200
+ assert response.json()["navbar"]["badge"]["text"] == "Local Development"
+ assert response.json()["navbar"]["badge"]["variant"] == "warning"
+
+
+@pytest.mark.usefixtures("admin")
+def test_fallback_env_mode(client: testclient.TestClient):
+ response = client.put(
+ "/api/v1/configurations/global",
+ json={
+ "metadata": {
+ "environment": "Fallback Environment",
+ },
+ "navbar": {
+ "badge": {"text": "auto", "variant": "auto", "show": True}
+ },
+ },
+ )
+
+ assert response.status_code == 200
+
+ response = client.get("/api/v1/configurations/unified")
+ assert response.status_code == 200
+ assert response.json()["navbar"]["badge"]["text"] == "Fallback Environment"
+ assert response.json()["navbar"]["badge"]["variant"] == "success"
+
+
+@pytest.mark.usefixtures("admin")
+def test_unknown_env_mode(
+ client: testclient.TestClient,
+):
+ response = client.put(
+ "/api/v1/configurations/global",
+ json={
+ "metadata": {
+ "environment": "",
+ },
+ "navbar": {
+ "badge": {"text": "auto", "variant": "auto", "show": True}
+ },
+ },
+ )
+
+ assert response.status_code == 200
+
+ response = client.get("/api/v1/configurations/unified")
+ assert response.status_code == 200
+ assert response.json()["navbar"]["badge"]["text"] == "Unknown Environment"
+ assert response.json()["navbar"]["badge"]["variant"] == "warning"
diff --git a/docs/docs/admin/configure-for-your-org.md b/docs/docs/admin/configure-for-your-org.md
index 0ae6cec0d..78977fc4c 100644
--- a/docs/docs/admin/configure-for-your-org.md
+++ b/docs/docs/admin/configure-for-your-org.md
@@ -27,11 +27,11 @@ metadata:
environment: '-'
```
-## Navigation Bar
+## Logo and Navigation Bar
-You can edit the links in the navigation bar. This can be useful if you want to
-link to external resources or if you are not using the default monitoring
-setup.
+You can edit the logo, badge, and links in the navigation bar. This can be
+useful to brand the Collaboration Manager for your organization, remind users
+which environment they are in, or link to external resources.
```yaml
navbar:
@@ -45,6 +45,11 @@ navbar:
- name: Documentation
service: documentation
role: user
+ logo_url: null
+ badge:
+ show: true
+ variant: auto
+ text: auto
```
In addition to the default service links, you can add your own by using `href`
@@ -63,6 +68,15 @@ 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.
+To show the logo in the navigation bar, set the `logo_url` field to the URL of
+the image you want to use.
+
+The badge can be used to show the environment the user is in. The `variant`
+field can be set to `auto` (it will be determined by the environment),
+`success`, or `warning`. The `text` field will use the environment name if set
+to `auto`, or you can specify a custom text. If you don't want to show the
+badge, set `show` to `false`.
+
## Feedback
!!! info "Configure SMTP server for feedback"
diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts
index 4dd5664be..b32002f74 100644
--- a/frontend/.storybook/main.ts
+++ b/frontend/.storybook/main.ts
@@ -20,6 +20,7 @@ const config: StorybookConfig = {
name: '@storybook/angular',
options: {},
},
+ staticDirs: [{ from: './test-assets', to: '/test-assets' }],
core: {
disableTelemetry: true,
enableCrashReports: false,
diff --git a/frontend/.storybook/test-assets/narrow_logo.svg b/frontend/.storybook/test-assets/narrow_logo.svg
new file mode 100644
index 000000000..2efe7e65f
--- /dev/null
+++ b/frontend/.storybook/test-assets/narrow_logo.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/frontend/.storybook/test-assets/wide_logo.svg b/frontend/.storybook/test-assets/wide_logo.svg
new file mode 100644
index 000000000..f338a4794
--- /dev/null
+++ b/frontend/.storybook/test-assets/wide_logo.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/frontend/src/app/general/header/header.component.html b/frontend/src/app/general/header/header.component.html
index eff6ada2e..c94e00f93 100644
--- a/frontend/src/app/general/header/header.component.html
+++ b/frontend/src/app/general/header/header.component.html
@@ -16,12 +16,12 @@
-
-
- Capella Collaboration Manager
-
+
+
@if (navBarService.navbarItems$ | async) {
-
+
@for (item of navBarService.navbarItems$ | async; track item.name) {
@if (userService.validateUserRole(item.requiredRole)) {
@if (item.href) {
@@ -52,7 +52,7 @@
}
-
+
{
href: '#',
},
]);
+ readonly logoUrl$ = of(undefined);
+ readonly badge$ = of({
+ variant: 'warning',
+ text: 'Storybook',
+ show: true,
+ });
}
export const NormalUser: Story = {
diff --git a/frontend/src/app/general/logo/logo.component.html b/frontend/src/app/general/logo/logo.component.html
new file mode 100644
index 000000000..252d40d58
--- /dev/null
+++ b/frontend/src/app/general/logo/logo.component.html
@@ -0,0 +1,24 @@
+
+
+
+ @if (navBarService.logoUrl$ | async; as logoUrl) {
+
+ }
+
+
CCM
+ @if ((navBarService.badge$ | async)?.show) {
+
+ {{ (navBarService.badge$ | async)?.text }}
+
+ }
+
diff --git a/frontend/src/app/general/logo/logo.component.ts b/frontend/src/app/general/logo/logo.component.ts
new file mode 100644
index 000000000..c4270ed45
--- /dev/null
+++ b/frontend/src/app/general/logo/logo.component.ts
@@ -0,0 +1,17 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { AsyncPipe } from '@angular/common';
+import { Component } from '@angular/core';
+import { NavBarService } from '../nav-bar/nav-bar.service';
+
+@Component({
+ selector: 'app-logo',
+ standalone: true,
+ templateUrl: './logo.component.html',
+ imports: [AsyncPipe],
+})
+export class LogoComponent {
+ constructor(public navBarService: NavBarService) {}
+}
diff --git a/frontend/src/app/general/logo/logo.stories.ts b/frontend/src/app/general/logo/logo.stories.ts
new file mode 100644
index 000000000..d3844f79c
--- /dev/null
+++ b/frontend/src/app/general/logo/logo.stories.ts
@@ -0,0 +1,87 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
+import { BehaviorSubject } from 'rxjs';
+import { BadgeOutput } from '../../openapi';
+import { NavBarService } from '../nav-bar/nav-bar.service';
+import { LogoComponent } from './logo.component';
+
+const meta: Meta = {
+ title: 'General Components/Logo',
+ component: LogoComponent,
+};
+
+export default meta;
+type Story = StoryObj;
+
+class MockNavbarService implements Partial {
+ readonly logoUrl$ = new BehaviorSubject(undefined);
+ readonly badge$ = new BehaviorSubject(undefined);
+
+ constructor(logoUrl: string | undefined, badge: BadgeOutput | undefined) {
+ this.logoUrl$.next(logoUrl);
+ this.badge$.next(badge);
+ }
+}
+
+export const BaseLogo: Story = {
+ args: {},
+};
+
+export const BaseWithStaging: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ {
+ provide: NavBarService,
+ useFactory: () =>
+ new MockNavbarService(undefined, {
+ show: true,
+ variant: 'warning',
+ text: 'Staging',
+ }),
+ },
+ ],
+ }),
+ ],
+};
+
+export const NarrowLogo: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ {
+ provide: NavBarService,
+ useFactory: () =>
+ new MockNavbarService('/test-assets/narrow_logo.svg', {
+ show: true,
+ variant: 'success',
+ text: 'Production',
+ }),
+ },
+ ],
+ }),
+ ],
+};
+
+export const WideLogo: Story = {
+ decorators: [
+ moduleMetadata({
+ providers: [
+ {
+ provide: NavBarService,
+ useFactory: () =>
+ new MockNavbarService('/test-assets/wide_logo.svg', {
+ show: true,
+ variant: 'success',
+ text: 'Production',
+ }),
+ },
+ ],
+ }),
+ ],
+};
diff --git a/frontend/src/app/general/nav-bar/nav-bar.service.ts b/frontend/src/app/general/nav-bar/nav-bar.service.ts
index 3eaad219b..b73148c0c 100644
--- a/frontend/src/app/general/nav-bar/nav-bar.service.ts
+++ b/frontend/src/app/general/nav-bar/nav-bar.service.ts
@@ -36,6 +36,13 @@ export class NavBarService {
map((items) => [...this.internalNavbarItems, ...items]),
);
+ readonly logoUrl$ = this.unifiedConfigWrapperService.unifiedConfig$.pipe(
+ map((unifiedConfig) => unifiedConfig?.navbar?.logo_url),
+ );
+
+ readonly badge$ = this.unifiedConfigWrapperService.unifiedConfig$.pipe(
+ map((unifiedConfig) => unifiedConfig?.navbar?.badge),
+ );
sidenav?: MatSidenav;
toggle(): void {
this.sidenav?.toggle();
diff --git a/frontend/src/app/openapi/.openapi-generator/FILES b/frontend/src/app/openapi/.openapi-generator/FILES
index 20e81fab7..bd30a4a58 100644
--- a/frontend/src/app/openapi/.openapi-generator/FILES
+++ b/frontend/src/app/openapi/.openapi-generator/FILES
@@ -35,6 +35,9 @@ model/anonymized-session.ts
model/authorization-response.ts
model/backup-pipeline-run.ts
model/backup.ts
+model/badge-input.ts
+model/badge-output.ts
+model/badge-variant.ts
model/base-user.ts
model/beta-configuration-input.ts
model/beta-configuration-output.ts
@@ -185,6 +188,7 @@ model/t4-c-license-server-usage.ts
model/t4-c-license-server.ts
model/t4-c-repository-status.ts
model/t4-c-repository.ts
+model/text.ts
model/token-request.ts
model/tool-backup-configuration-input.ts
model/tool-backup-configuration-output.ts
diff --git a/frontend/src/app/openapi/model/badge-input.ts b/frontend/src/app/openapi/model/badge-input.ts
new file mode 100644
index 000000000..4a523572a
--- /dev/null
+++ b/frontend/src/app/openapi/model/badge-input.ts
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Capella Collaboration
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * Do not edit the class manually.
+ + To generate a new version, run `make openapi` in the root directory of this repository.
+ */
+
+import { Text } from './text';
+import { BadgeVariant } from './badge-variant';
+
+
+export interface BadgeInput {
+ /**
+ * Show a badge with the current environment.
+ */
+ show?: boolean;
+ /**
+ * Color of the badge.
+ */
+ variant?: BadgeVariant;
+ text?: Text;
+}
+export namespace BadgeInput {
+}
+
+
diff --git a/frontend/src/app/openapi/model/badge-output.ts b/frontend/src/app/openapi/model/badge-output.ts
new file mode 100644
index 000000000..a9d9aee5c
--- /dev/null
+++ b/frontend/src/app/openapi/model/badge-output.ts
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Capella Collaboration
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * Do not edit the class manually.
+ + To generate a new version, run `make openapi` in the root directory of this repository.
+ */
+
+import { Text } from './text';
+import { BadgeVariant } from './badge-variant';
+
+
+export interface BadgeOutput {
+ /**
+ * Show a badge with the current environment.
+ */
+ show: boolean;
+ /**
+ * Color of the badge.
+ */
+ variant: BadgeVariant;
+ text: Text;
+}
+export namespace BadgeOutput {
+}
+
+
diff --git a/frontend/src/app/openapi/model/badge-variant.ts b/frontend/src/app/openapi/model/badge-variant.ts
new file mode 100644
index 000000000..ad3ec7676
--- /dev/null
+++ b/frontend/src/app/openapi/model/badge-variant.ts
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Capella Collaboration
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * Do not edit the class manually.
+ + To generate a new version, run `make openapi` in the root directory of this repository.
+ */
+
+
+
+export type BadgeVariant = 'auto' | 'warning' | 'success';
+
+export const BadgeVariant = {
+ Auto: 'auto' as BadgeVariant,
+ Warning: 'warning' as BadgeVariant,
+ Success: 'success' as BadgeVariant
+};
+
diff --git a/frontend/src/app/openapi/model/models.ts b/frontend/src/app/openapi/model/models.ts
index 2bb091f6c..4b0fe80b7 100644
--- a/frontend/src/app/openapi/model/models.ts
+++ b/frontend/src/app/openapi/model/models.ts
@@ -13,6 +13,9 @@ export * from './anonymized-session';
export * from './authorization-response';
export * from './backup';
export * from './backup-pipeline-run';
+export * from './badge-input';
+export * from './badge-output';
+export * from './badge-variant';
export * from './base-user';
export * from './beta-configuration-input';
export * from './beta-configuration-output';
@@ -162,6 +165,7 @@ export * from './t4-c-license-server-base';
export * from './t4-c-license-server-usage';
export * from './t4-c-repository';
export * from './t4-c-repository-status';
+export * from './text';
export * from './token-request';
export * from './tool';
export * from './tool-backup-configuration-input';
diff --git a/frontend/src/app/openapi/model/navbar-configuration-input.ts b/frontend/src/app/openapi/model/navbar-configuration-input.ts
index bda864ba2..b4148fc52 100644
--- a/frontend/src/app/openapi/model/navbar-configuration-input.ts
+++ b/frontend/src/app/openapi/model/navbar-configuration-input.ts
@@ -9,6 +9,7 @@
+ To generate a new version, run `make openapi` in the root directory of this repository.
*/
+import { BadgeInput } from './badge-input';
import { NavbarConfigurationInputExternalLinksInner } from './navbar-configuration-input-external-links-inner';
@@ -17,5 +18,10 @@ export interface NavbarConfigurationInput {
* Links to display in the navigation bar.
*/
external_links?: Array;
+ logo_url?: string | null;
+ /**
+ * Badge to display in the navigation bar.
+ */
+ badge?: BadgeInput;
}
diff --git a/frontend/src/app/openapi/model/navbar-configuration-output.ts b/frontend/src/app/openapi/model/navbar-configuration-output.ts
index ac7d08408..b7eb491c8 100644
--- a/frontend/src/app/openapi/model/navbar-configuration-output.ts
+++ b/frontend/src/app/openapi/model/navbar-configuration-output.ts
@@ -10,6 +10,7 @@
*/
import { NavbarConfigurationInputExternalLinksInner } from './navbar-configuration-input-external-links-inner';
+import { BadgeOutput } from './badge-output';
export interface NavbarConfigurationOutput {
@@ -17,5 +18,10 @@ export interface NavbarConfigurationOutput {
* Links to display in the navigation bar.
*/
external_links: Array;
+ logo_url: string | null;
+ /**
+ * Badge to display in the navigation bar.
+ */
+ badge: BadgeOutput;
}
diff --git a/frontend/src/app/openapi/model/text.ts b/frontend/src/app/openapi/model/text.ts
new file mode 100644
index 000000000..6cf82a19f
--- /dev/null
+++ b/frontend/src/app/openapi/model/text.ts
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Capella Collaboration
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * Do not edit the class manually.
+ + To generate a new version, run `make openapi` in the root directory of this repository.
+ */
+
+
+
+/**
+ * Text to display in the badge. Use \'auto\' to display the environment name.
+ */
+export interface Text {
+}
+
diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs
index d46595273..8bef436c0 100644
--- a/frontend/tailwind.config.cjs
+++ b/frontend/tailwind.config.cjs
@@ -37,6 +37,10 @@ module.exports = {
screens: {
tall: { raw: "(min-height: 945px)" },
},
+ backgroundImage: {
+ hazard:
+ "repeating-linear-gradient(-55deg, #000, #000 10px, #966708 10px, #966708 20px)",
+ },
},
},
plugins: [],