diff --git a/backend/capellacollab/alembic/versions/ca9ce61491a7_make_announcements_dismissible.py b/backend/capellacollab/alembic/versions/ca9ce61491a7_make_announcements_dismissible.py
new file mode 100644
index 0000000000..049ca14c82
--- /dev/null
+++ b/backend/capellacollab/alembic/versions/ca9ce61491a7_make_announcements_dismissible.py
@@ -0,0 +1,29 @@
+# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+# SPDX-License-Identifier: Apache-2.0
+
+"""Make announcements dismissible
+
+Revision ID: ca9ce61491a7
+Revises: 8731ac0b284e
+Create Date: 2025-02-07 19:42:01.723097
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "ca9ce61491a7"
+down_revision = "8731ac0b284e"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.rename_table("notices", "announcements")
+
+ op.add_column(
+ "announcements", sa.Column("dismissible", sa.Boolean(), nullable=True)
+ )
+ op.execute("UPDATE announcements SET dismissible = false")
+ op.alter_column("announcements", "dismissible", nullable=False)
diff --git a/backend/capellacollab/notices/__init__.py b/backend/capellacollab/announcements/__init__.py
similarity index 100%
rename from backend/capellacollab/notices/__init__.py
rename to backend/capellacollab/announcements/__init__.py
diff --git a/backend/capellacollab/announcements/crud.py b/backend/capellacollab/announcements/crud.py
new file mode 100644
index 0000000000..dae68c4396
--- /dev/null
+++ b/backend/capellacollab/announcements/crud.py
@@ -0,0 +1,53 @@
+# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+# SPDX-License-Identifier: Apache-2.0
+
+
+from collections import abc
+
+import sqlalchemy as sa
+from sqlalchemy import orm
+
+from capellacollab.announcements import models
+
+
+def get_announcements(
+ db: orm.Session,
+) -> abc.Sequence[models.DatabaseAnnouncement]:
+ return db.execute(sa.select(models.DatabaseAnnouncement)).scalars().all()
+
+
+def get_announcement_by_id(
+ db: orm.Session, announcement_id: int
+) -> models.DatabaseAnnouncement | None:
+ return db.execute(
+ sa.select(models.DatabaseAnnouncement).where(
+ models.DatabaseAnnouncement.id == announcement_id
+ )
+ ).scalar_one_or_none()
+
+
+def create_announcement(
+ db: orm.Session, body: models.CreateAnnouncementRequest
+) -> models.DatabaseAnnouncement:
+ announcement = models.DatabaseAnnouncement(**body.model_dump())
+ db.add(announcement)
+ db.commit()
+ return announcement
+
+
+def update_announcement(
+ db: orm.Session,
+ announcement: models.DatabaseAnnouncement,
+ body: models.CreateAnnouncementRequest,
+) -> models.DatabaseAnnouncement:
+ for field, value in body.model_dump().items():
+ setattr(announcement, field, value)
+ db.commit()
+ return announcement
+
+
+def delete_announcement(
+ db: orm.Session, announcement: models.DatabaseAnnouncement
+) -> None:
+ db.delete(announcement)
+ db.commit()
diff --git a/backend/capellacollab/notices/exceptions.py b/backend/capellacollab/announcements/exceptions.py
similarity index 80%
rename from backend/capellacollab/notices/exceptions.py
rename to backend/capellacollab/announcements/exceptions.py
index 2c278ff657..2e6b229149 100644
--- a/backend/capellacollab/notices/exceptions.py
+++ b/backend/capellacollab/announcements/exceptions.py
@@ -7,12 +7,12 @@
class AnnouncementNotFoundError(core_exceptions.BaseError):
- def __init__(self, notice_id: int):
+ def __init__(self, announcement_id: int):
super().__init__(
status_code=status.HTTP_404_NOT_FOUND,
err_code="ANNOUNCEMENT_NOT_FOUND",
title="Announcement not found",
- reason=f"The announcement with ID {notice_id} doesn't exist",
+ reason=f"The announcement with ID {announcement_id} doesn't exist",
)
@classmethod
diff --git a/backend/capellacollab/announcements/injectables.py b/backend/capellacollab/announcements/injectables.py
new file mode 100644
index 0000000000..05261fc717
--- /dev/null
+++ b/backend/capellacollab/announcements/injectables.py
@@ -0,0 +1,20 @@
+# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+# SPDX-License-Identifier: Apache-2.0
+
+import fastapi
+from sqlalchemy import orm
+
+from capellacollab.announcements import crud, models
+from capellacollab.core import database
+
+from . import exceptions
+
+
+def get_existing_announcement(
+ announcement_id: int,
+ db: orm.Session = fastapi.Depends(database.get_db),
+) -> models.DatabaseAnnouncement:
+ if announcement := crud.get_announcement_by_id(db, announcement_id):
+ return announcement
+
+ raise exceptions.AnnouncementNotFoundError(announcement_id)
diff --git a/backend/capellacollab/notices/models.py b/backend/capellacollab/announcements/models.py
similarity index 61%
rename from backend/capellacollab/notices/models.py
rename to backend/capellacollab/announcements/models.py
index ae0c20fdf2..d2fa8414d2 100644
--- a/backend/capellacollab/notices/models.py
+++ b/backend/capellacollab/announcements/models.py
@@ -9,7 +9,7 @@
from capellacollab.core import pydantic as core_pydantic
-class NoticeLevel(enum.Enum):
+class AnnouncementLevel(enum.Enum):
PRIMARY = "primary"
SECONDARY = "secondary"
SUCCESS = "success"
@@ -19,22 +19,24 @@ class NoticeLevel(enum.Enum):
ALERT = "alert"
-class CreateNoticeRequest(core_pydantic.BaseModel):
- level: NoticeLevel
+class CreateAnnouncementRequest(core_pydantic.BaseModel):
+ level: AnnouncementLevel
title: str
message: str
+ dismissible: bool
-class NoticeResponse(CreateNoticeRequest):
+class AnnouncementResponse(CreateAnnouncementRequest):
id: int
-class DatabaseNotice(database.Base):
- __tablename__ = "notices"
+class DatabaseAnnouncement(database.Base):
+ __tablename__ = "announcements"
id: orm.Mapped[int] = orm.mapped_column(
init=False, primary_key=True, index=True
)
title: orm.Mapped[str]
message: orm.Mapped[str]
- level: orm.Mapped[NoticeLevel]
+ level: orm.Mapped[AnnouncementLevel]
+ dismissible: orm.Mapped[bool] = orm.mapped_column(default=False)
diff --git a/backend/capellacollab/announcements/routes.py b/backend/capellacollab/announcements/routes.py
new file mode 100644
index 0000000000..eabcf7df7c
--- /dev/null
+++ b/backend/capellacollab/announcements/routes.py
@@ -0,0 +1,99 @@
+# 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.permissions import injectables as permissions_injectables
+from capellacollab.permissions import models as permissions_models
+
+from . import crud, injectables, models
+
+router = fastapi.APIRouter()
+
+
+@router.get(
+ "",
+ response_model=list[models.AnnouncementResponse],
+)
+def get_announcements(db: orm.Session = fastapi.Depends(database.get_db)):
+ return crud.get_announcements(db)
+
+
+@router.get("/{announcement_id}")
+def get_announcement_by_id(
+ announcement: models.DatabaseAnnouncement = fastapi.Depends(
+ injectables.get_existing_announcement
+ ),
+):
+ return announcement
+
+
+@router.post(
+ "",
+ dependencies=[
+ fastapi.Depends(
+ permissions_injectables.PermissionValidation(
+ required_scope=permissions_models.GlobalScopes(
+ admin=permissions_models.AdminScopes(
+ announcements={permissions_models.UserTokenVerb.CREATE}
+ )
+ )
+ ),
+ )
+ ],
+)
+def create_announcement(
+ post_announcement: models.CreateAnnouncementRequest,
+ db: orm.Session = fastapi.Depends(database.get_db),
+):
+ return crud.create_announcement(db, post_announcement)
+
+
+@router.patch(
+ "/{announcement_id}",
+ dependencies=[
+ fastapi.Depends(
+ permissions_injectables.PermissionValidation(
+ required_scope=permissions_models.GlobalScopes(
+ admin=permissions_models.AdminScopes(
+ announcements={permissions_models.UserTokenVerb.UPDATE}
+ )
+ )
+ ),
+ )
+ ],
+)
+def update_announcement(
+ post_announcement: models.CreateAnnouncementRequest,
+ announcement: models.DatabaseAnnouncement = fastapi.Depends(
+ injectables.get_existing_announcement
+ ),
+ db: orm.Session = fastapi.Depends(database.get_db),
+):
+ return crud.update_announcement(db, announcement, post_announcement)
+
+
+@router.delete(
+ "/{announcement_id}",
+ status_code=204,
+ dependencies=[
+ fastapi.Depends(
+ permissions_injectables.PermissionValidation(
+ required_scope=permissions_models.GlobalScopes(
+ admin=permissions_models.AdminScopes(
+ announcements={permissions_models.UserTokenVerb.DELETE}
+ )
+ )
+ ),
+ )
+ ],
+)
+def delete_announcement(
+ announcement: models.DatabaseAnnouncement = fastapi.Depends(
+ injectables.get_existing_announcement
+ ),
+ db: orm.Session = fastapi.Depends(database.get_db),
+):
+ crud.delete_announcement(db, announcement)
diff --git a/backend/capellacollab/core/database/migration.py b/backend/capellacollab/core/database/migration.py
index d566430518..75d8032ba9 100644
--- a/backend/capellacollab/core/database/migration.py
+++ b/backend/capellacollab/core/database/migration.py
@@ -11,6 +11,8 @@
from sqlalchemy import orm
from capellacollab import core
+from capellacollab.announcements import crud as announcements_crud
+from capellacollab.announcements import models as announcements_models
from capellacollab.configuration.app import config
from capellacollab.core import database
from capellacollab.events import crud as events_crud
@@ -82,6 +84,7 @@ def migrate_db(engine, database_url: str):
LOGGER.info("Database structure creation successful")
command.stamp(alembic_cfg, "head")
initialize_admin_user(session)
+ create_welcome_announcement(session)
create_tools(session)
initialize_capellambse_test_project(session)
@@ -105,6 +108,19 @@ def initialize_admin_user(db: orm.Session):
LOGGER.info("Initialized admin user %s", config.initial.admin)
+def create_welcome_announcement(db: orm.Session):
+ welcome_announcement = announcements_crud.create_announcement(
+ db,
+ announcements_models.CreateAnnouncementRequest(
+ title="Welcome to the Capella Collaboration Manager",
+ message="Make sure to check out our documentation to learn more",
+ level=announcements_models.AnnouncementLevel.PRIMARY,
+ dismissible=True,
+ ),
+ )
+ LOGGER.info("Initialized welcome announcement %s", welcome_announcement.id)
+
+
def initialize_capellambse_test_project(db: orm.Session):
project = projects_crud.create_project(
db=db,
diff --git a/backend/capellacollab/core/database/models.py b/backend/capellacollab/core/database/models.py
index dff0b2c4b5..dd53c28fb9 100644
--- a/backend/capellacollab/core/database/models.py
+++ b/backend/capellacollab/core/database/models.py
@@ -4,10 +4,10 @@
# pylint: disable=unused-import
# These import statements of the models are required and should not be removed! (SQLAlchemy will not load the models otherwise)
+import capellacollab.announcements.models
import capellacollab.configuration.models
import capellacollab.events.models
import capellacollab.feedback.models
-import capellacollab.notices.models
import capellacollab.projects.models
import capellacollab.projects.permissions.models
import capellacollab.projects.toolmodels.backups.models
diff --git a/backend/capellacollab/notices/crud.py b/backend/capellacollab/notices/crud.py
deleted file mode 100644
index 4a678186d9..0000000000
--- a/backend/capellacollab/notices/crud.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
-# SPDX-License-Identifier: Apache-2.0
-
-
-from collections import abc
-
-import sqlalchemy as sa
-from sqlalchemy import orm
-
-from capellacollab.notices import models
-
-
-def get_notices(db: orm.Session) -> abc.Sequence[models.DatabaseNotice]:
- return db.execute(sa.select(models.DatabaseNotice)).scalars().all()
-
-
-def get_notice_by_id(
- db: orm.Session, notice_id: int
-) -> models.DatabaseNotice | None:
- return db.execute(
- sa.select(models.DatabaseNotice).where(
- models.DatabaseNotice.id == notice_id
- )
- ).scalar_one_or_none()
-
-
-def create_notice(
- db: orm.Session, body: models.CreateNoticeRequest
-) -> models.DatabaseNotice:
- notice = models.DatabaseNotice(**body.model_dump())
- db.add(notice)
- db.commit()
- return notice
-
-
-def delete_notice(db: orm.Session, notice: models.DatabaseNotice) -> None:
- db.delete(notice)
- db.commit()
diff --git a/backend/capellacollab/notices/injectables.py b/backend/capellacollab/notices/injectables.py
deleted file mode 100644
index 75ed721d2a..0000000000
--- a/backend/capellacollab/notices/injectables.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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.notices import crud, models
-
-from . import exceptions
-
-
-def get_existing_notice(
- notice_id: int,
- db: orm.Session = fastapi.Depends(database.get_db),
-) -> models.DatabaseNotice:
- if notice := crud.get_notice_by_id(db, notice_id):
- return notice
-
- raise exceptions.AnnouncementNotFoundError(notice_id)
diff --git a/backend/capellacollab/notices/routes.py b/backend/capellacollab/notices/routes.py
deleted file mode 100644
index fb3a0b7286..0000000000
--- a/backend/capellacollab/notices/routes.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
-# SPDX-License-Identifier: Apache-2.0
-
-import fastapi
-from sqlalchemy import orm
-
-import capellacollab.notices.crud as notices
-from capellacollab.core import database
-from capellacollab.notices.injectables import get_existing_notice
-from capellacollab.notices.models import (
- CreateNoticeRequest,
- DatabaseNotice,
- NoticeResponse,
-)
-from capellacollab.permissions import injectables as permissions_injectables
-from capellacollab.permissions import models as permissions_models
-
-router = fastapi.APIRouter()
-
-
-@router.get(
- "",
- response_model=list[NoticeResponse],
-)
-def get_notices(db: orm.Session = fastapi.Depends(database.get_db)):
- return notices.get_notices(db)
-
-
-@router.get("/{notice_id}")
-def get_notice_by_id(
- notice: DatabaseNotice = fastapi.Depends(get_existing_notice),
-):
- return notice
-
-
-@router.post(
- "",
- dependencies=[
- fastapi.Depends(
- permissions_injectables.PermissionValidation(
- required_scope=permissions_models.GlobalScopes(
- admin=permissions_models.AdminScopes(
- announcements={permissions_models.UserTokenVerb.CREATE}
- )
- )
- ),
- )
- ],
-)
-def create_notice(
- post_notice: CreateNoticeRequest,
- db: orm.Session = fastapi.Depends(database.get_db),
-):
- return notices.create_notice(db, post_notice)
-
-
-@router.delete(
- "/{notice_id}",
- status_code=204,
- dependencies=[
- fastapi.Depends(
- permissions_injectables.PermissionValidation(
- required_scope=permissions_models.GlobalScopes(
- admin=permissions_models.AdminScopes(
- announcements={permissions_models.UserTokenVerb.DELETE}
- )
- )
- ),
- )
- ],
-)
-def delete_notice(
- notice: DatabaseNotice = fastapi.Depends(get_existing_notice),
- db: orm.Session = fastapi.Depends(database.get_db),
-):
- notices.delete_notice(db, notice)
diff --git a/backend/capellacollab/permissions/models.py b/backend/capellacollab/permissions/models.py
index 81922559f2..5a2d7a1d8c 100644
--- a/backend/capellacollab/permissions/models.py
+++ b/backend/capellacollab/permissions/models.py
@@ -101,7 +101,9 @@ class AdminScopes(core_pydantic.BaseModelStrict):
description="Manage all tools, including its versions and natures",
)
announcements: set[
- t.Literal[UserTokenVerb.CREATE, UserTokenVerb.DELETE]
+ t.Literal[
+ UserTokenVerb.CREATE, UserTokenVerb.UPDATE, UserTokenVerb.DELETE
+ ]
] = pydantic.Field(
default_factory=set,
title="Announcements",
diff --git a/backend/capellacollab/routes.py b/backend/capellacollab/routes.py
index 4b6ee07f4a..a3a823f17b 100644
--- a/backend/capellacollab/routes.py
+++ b/backend/capellacollab/routes.py
@@ -6,12 +6,12 @@
import fastapi
+from capellacollab.announcements import routes as announcements_routes
from capellacollab.configuration import routes as configuration_routes
from capellacollab.core.authentication import routes as authentication_routes
from capellacollab.events import routes as events_router
from capellacollab.feedback import routes as feedback_routes
from capellacollab.health import routes as health_routes
-from capellacollab.notices import routes as notices_routes
from capellacollab.permissions import routes as permissions_routes
from capellacollab.projects import routes as projects_routes
from capellacollab.sessions import routes as sessions_routes
@@ -60,7 +60,9 @@
tags=["Events"],
)
router.include_router(
- notices_routes.router, prefix="/notices", tags=["Notices"]
+ announcements_routes.router,
+ prefix="/announcements",
+ tags=["Announcements"],
)
router.include_router(
settings_routes.router,
diff --git a/backend/capellacollab/users/models.py b/backend/capellacollab/users/models.py
index 0c95e6d4f9..dbd3270cc6 100644
--- a/backend/capellacollab/users/models.py
+++ b/backend/capellacollab/users/models.py
@@ -70,6 +70,7 @@ class Role(str, enum.Enum):
},
announcements={
models.UserTokenVerb.CREATE,
+ models.UserTokenVerb.UPDATE,
models.UserTokenVerb.DELETE,
},
monitoring={
diff --git a/backend/tests/settings/test_alerts.py b/backend/tests/settings/test_alerts.py
deleted file mode 100644
index 92ec26160d..0000000000
--- a/backend/tests/settings/test_alerts.py
+++ /dev/null
@@ -1,95 +0,0 @@
-# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
-# SPDX-License-Identifier: Apache-2.0
-
-from collections import abc
-
-from fastapi.testclient import TestClient
-from sqlalchemy import orm
-
-from capellacollab.notices import crud as notices_crud
-from capellacollab.notices import models as notices_models
-from capellacollab.users import crud as users_crud
-from capellacollab.users import models as users_models
-
-
-def test_get_alerts(client: TestClient, db: orm.Session, executor_name: str):
- users_crud.create_user(
- db, executor_name, executor_name, None, users_models.Role.USER
- )
- notice = notices_crud.create_notice(
- db,
- notices_models.CreateNoticeRequest(
- level=notices_models.NoticeLevel.INFO,
- title="test title",
- message="test message",
- ),
- )
-
- response = client.get("/api/v1/notices")
-
- assert response.status_code == 200
- assert len(response.json()) == 1
- assert {
- "level": "info",
- "title": "test title",
- "message": "test message",
- }.items() <= response.json()[0].items()
-
- single_response = client.get(f"/api/v1/notices/{notice.id}")
- assert single_response.status_code == 200
- assert {
- "level": "info",
- "title": "test title",
- "message": "test message",
- }.items() <= single_response.json().items()
-
-
-def test_get_missing_alert(
- client: TestClient, db: orm.Session, executor_name: str
-):
- response = client.get("/api/v1/notices/1")
-
- assert response.status_code == 404
- assert response.json()["detail"]["err_code"] == "ANNOUNCEMENT_NOT_FOUND"
-
-
-def test_create_alert2(
- client: TestClient, db: orm.Session, executor_name: str
-):
- users_crud.create_user(
- db, executor_name, executor_name, None, users_models.Role.ADMIN
- )
-
- response = client.post(
- "/api/v1/notices",
- json={"title": "test", "message": "test", "level": "success"},
- )
-
- assert response.status_code == 200
-
- notices: abc.Sequence[notices_models.DatabaseNotice] = (
- notices_crud.get_notices(db)
- )
- assert len(notices) == 1
- assert notices[0].title == "test"
- assert notices[0].message == "test"
- assert notices[0].level == notices_models.NoticeLevel.SUCCESS
-
-
-def test_delete_alert(client: TestClient, db: orm.Session, executor_name: str):
- users_crud.create_user(
- db, executor_name, executor_name, None, users_models.Role.ADMIN
- )
- alert = notices_crud.create_notice(
- db,
- notices_models.CreateNoticeRequest(
- level=notices_models.NoticeLevel.INFO,
- title="test title",
- message="test message",
- ),
- )
-
- response = client.delete(f"/api/v1/notices/{alert.id}")
-
- assert response.status_code == 204
- assert not notices_crud.get_notices(db)
diff --git a/backend/tests/settings/test_announcements.py b/backend/tests/settings/test_announcements.py
new file mode 100644
index 0000000000..8d3e2fb0a8
--- /dev/null
+++ b/backend/tests/settings/test_announcements.py
@@ -0,0 +1,146 @@
+# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+# SPDX-License-Identifier: Apache-2.0
+
+from fastapi import testclient
+from sqlalchemy import orm
+
+from capellacollab.announcements import crud as announcements_crud
+from capellacollab.announcements import models as announcements_models
+from capellacollab.users import models as users_models
+
+
+def test_get_announcements(
+ client: testclient.TestClient,
+ db: orm.Session,
+):
+ announcement = announcements_crud.create_announcement(
+ db,
+ announcements_models.CreateAnnouncementRequest(
+ level=announcements_models.AnnouncementLevel.INFO,
+ title="test title",
+ message="test message",
+ dismissible=True,
+ ),
+ )
+
+ response = client.get("/api/v1/announcements")
+
+ assert response.status_code == 200
+ assert len(response.json()) == 2
+ assert {
+ "level": "info",
+ "title": "test title",
+ "message": "test message",
+ "dismissible": True,
+ }.items() <= response.json()[1].items()
+
+ single_response = client.get(f"/api/v1/announcements/{announcement.id}")
+ assert single_response.status_code == 200
+ assert {
+ "level": "info",
+ "title": "test title",
+ "message": "test message",
+ "dismissible": True,
+ }.items() <= single_response.json().items()
+
+
+def test_get_missing_announcement(
+ client: testclient.TestClient,
+ db: orm.Session,
+):
+ response = client.get("/api/v1/announcements/99")
+
+ assert response.status_code == 404
+ assert response.json()["detail"]["err_code"] == "ANNOUNCEMENT_NOT_FOUND"
+
+
+def test_create_announcement(
+ client: testclient.TestClient,
+ admin: users_models.DatabaseUser,
+ db: orm.Session,
+):
+ response = client.post(
+ "/api/v1/announcements",
+ json={
+ "title": "test",
+ "message": "test",
+ "level": "success",
+ "dismissible": True,
+ },
+ )
+
+ assert response.status_code == 200
+
+ announcements = announcements_crud.get_announcements(db)
+
+ assert len(announcements) == 2
+ assert announcements[1].title == "test"
+ assert announcements[1].message == "test"
+ assert (
+ announcements[1].level
+ == announcements_models.AnnouncementLevel.SUCCESS
+ )
+
+
+def test_update_announcement(
+ client: testclient.TestClient,
+ admin: users_models.DatabaseUser,
+ db: orm.Session,
+):
+ announcement = announcements_crud.create_announcement(
+ db,
+ announcements_models.CreateAnnouncementRequest(
+ level=announcements_models.AnnouncementLevel.INFO,
+ title="original title",
+ message="original message",
+ dismissible=True,
+ ),
+ )
+
+ update_data = {
+ "title": "updated title",
+ "message": "updated message",
+ "level": "warning",
+ "dismissible": False,
+ }
+
+ response = client.patch(
+ f"/api/v1/announcements/{announcement.id}", json=update_data
+ )
+
+ assert response.status_code == 200
+
+ updated_announcement = announcements_crud.get_announcement_by_id(
+ db, announcement.id
+ )
+
+ assert updated_announcement.title == "updated title"
+ assert updated_announcement.message == "updated message"
+ assert (
+ updated_announcement.level
+ == announcements_models.AnnouncementLevel.WARNING
+ )
+ assert updated_announcement.dismissible is False
+
+
+def test_delete_announcement(
+ client: testclient.TestClient,
+ admin: users_models.DatabaseUser,
+ db: orm.Session,
+):
+ announcement = announcements_crud.create_announcement(
+ db,
+ announcements_models.CreateAnnouncementRequest(
+ level=announcements_models.AnnouncementLevel.INFO,
+ title="test title",
+ message="test message",
+ dismissible=True,
+ ),
+ )
+
+ response = client.delete(f"/api/v1/announcements/{announcement.id}")
+
+ assert response.status_code == 204
+
+ announcements = announcements_crud.get_announcements(db)
+ assert len(announcements) == 1
diff --git a/docs/docs/admin/alerts/create.md b/docs/docs/admin/alerts/create.md
deleted file mode 100644
index 84fb9c104e..0000000000
--- a/docs/docs/admin/alerts/create.md
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-Alerts can be used to inform users about changes, news or maintenance work. The
-alerts are displayed to each user.
-
-1. Navigate to `Menu` → `Settings`
-2. Fill in all required fields in the `Create an alert` form.
- 
-
- !!! Question "What does the alert level mean?"
-
- The alert level specifies
- the background color of the alert. You can choose one of the following
- options:
-
- :material-checkbox-blank-circle:{ style="color: #004085 " } `primary`
- :material-checkbox-blank-circle:{ style="color: #383d41 " } `secondary`
- :material-checkbox-blank-circle:{ style="color: #155724 " } `success`
- :material-checkbox-blank-circle:{ style="color: #721c24 " } `danger`
- :material-checkbox-blank-circle:{ style="color: #fff3cd " } `warning`
- :material-checkbox-blank-circle:{ style="color: #d1ecf1 " } `info`
-
- !!! Question "Which scopes are available?"
-
- Currently, there is only one scope.
- Please enter `t4c` in the scope field.
-
- !!! hint
-
- Simple HTML tags can be used in the alerts description. For
- example, a link can be created with:
-
- ``` html
- Link description
- ```
-
-3. The alert is now created and is displayed to all users:
- 
diff --git a/docs/docs/admin/alerts/create.png b/docs/docs/admin/alerts/create.png
deleted file mode 100644
index 87fb4de4a9..0000000000
Binary files a/docs/docs/admin/alerts/create.png and /dev/null differ
diff --git a/docs/docs/admin/alerts/success_alert.png b/docs/docs/admin/alerts/success_alert.png
deleted file mode 100644
index acd083dac5..0000000000
Binary files a/docs/docs/admin/alerts/success_alert.png and /dev/null differ
diff --git a/docs/docs/admin/announcements/create.md b/docs/docs/admin/announcements/create.md
new file mode 100644
index 0000000000..c90dea9c37
--- /dev/null
+++ b/docs/docs/admin/announcements/create.md
@@ -0,0 +1,42 @@
+
+
+Announcements can be used to inform users about changes, news or maintenance
+work. Announcements are displayed to all users, no matter their role.
+
+1. Navigate to `Menu` → `Settings`
+2. Fill in all required fields in the `Create new announcement` form.
+ 
+
+ !!! Question "What does the announcement level mean?"
+
+ The announcement level specifies
+ the background color of the announcement. You can choose one of the following
+ options:
+
+ :material-checkbox-blank-circle:{ style="color: #004085 " } `primary`
+ :material-checkbox-blank-circle:{ style="color: #383d41 " } `secondary`
+ :material-checkbox-blank-circle:{ style="color: #155724 " } `success`
+ :material-checkbox-blank-circle:{ style="color: #721c24 " } `danger`
+ :material-checkbox-blank-circle:{ style="color: #fff3cd " } `warning`
+ :material-checkbox-blank-circle:{ style="color: #d1ecf1 " } `info`
+
+ !!! hint
+
+ The announcement description supports markdown syntax.
+
+ ```markdown
+ You can use __bold__, *italic*, ~~strikethrough~~, and `code` text.
+ ```
+ Check the [Markdown Guide](https://www.markdownguide.org/basic-syntax/) for more information.
+
+ Additionally, you can use HTML tags in the announcement description.
+
+ !!! hint
+
+ By default, announcements can be dismissed by users. If you want to create an announcement that cannot be dismissed, you can toggle the `Dismissable` checkbox.
+
+3. The announcement is now created and is displayed to all users:
+ 
diff --git a/docs/docs/admin/announcements/create.png b/docs/docs/admin/announcements/create.png
new file mode 100644
index 0000000000..cb93bebcbf
Binary files /dev/null and b/docs/docs/admin/announcements/create.png differ
diff --git a/docs/docs/admin/alerts/create.png.license b/docs/docs/admin/announcements/create.png.license
similarity index 100%
rename from docs/docs/admin/alerts/create.png.license
rename to docs/docs/admin/announcements/create.png.license
diff --git a/docs/docs/admin/announcements/success_announcement.png b/docs/docs/admin/announcements/success_announcement.png
new file mode 100644
index 0000000000..a02ffc4e41
Binary files /dev/null and b/docs/docs/admin/announcements/success_announcement.png differ
diff --git a/docs/docs/admin/alerts/success_alert.png.license b/docs/docs/admin/announcements/success_announcement.png.license
similarity index 100%
rename from docs/docs/admin/alerts/success_alert.png.license
rename to docs/docs/admin/announcements/success_announcement.png.license
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 880aa69314..27c9ad30d5 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -117,7 +117,7 @@ nav:
- Tools:
- General: admin/settings/tools/index.md
- Configuration: admin/tools/configuration.md
- - Alerts: admin/alerts/create.md
+ - Announcements: admin/announcements/create.md
- CI templates:
- Gitlab CI/CD:
- Image builder: admin/ci-templates/gitlab/image-builder.md
diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index 57682eb0db..d7468abf67 100644
--- a/frontend/src/app/app-routing.module.ts
+++ b/frontend/src/app/app-routing.module.ts
@@ -42,7 +42,7 @@ import { ProjectOverviewComponent } from './projects/project-overview/project-ov
import { ProjectWrapperComponent } from './projects/project-wrapper/project-wrapper.component';
import { SessionOverviewComponent } from './sessions/session-overview/session-overview.component';
import { SessionsComponent } from './sessions/sessions.component';
-import { AlertSettingsComponent } from './settings/core/alert-settings/alert-settings.component';
+import { AnnouncementSettingsComponent } from './settings/core/announcement-settings/announcement-settings.component';
import { ToolDetailsComponent } from './settings/core/tools-settings/tool-details/tool-details.component';
import { ToolsSettingsComponent } from './settings/core/tools-settings/tools-settings.component';
import { PureVariantsComponent } from './settings/integrations/pure-variants/pure-variants.component';
@@ -361,9 +361,9 @@ export const routes: Routes = [
component: UserSettingsComponent,
},
{
- path: 'alerts',
- data: { breadcrumb: 'Alerts' },
- component: AlertSettingsComponent,
+ path: 'announcements',
+ data: { breadcrumb: 'Announcements' },
+ component: AnnouncementSettingsComponent,
},
{
path: 'pipelines',
diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html
index 43c30bea4a..e5b75454e2 100644
--- a/frontend/src/app/app.component.html
+++ b/frontend/src/app/app.component.html
@@ -25,9 +25,9 @@
>
@if (
(fullscreenService.isFullscreen$ | async) === false &&
- pageLayoutService.showNotice
+ pageLayoutService.showAnnouncement
) {
-
+
}
diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts
index 7ec0c0cc10..9382527613 100644
--- a/frontend/src/app/app.component.ts
+++ b/frontend/src/app/app.component.ts
@@ -13,10 +13,10 @@ import {
import { RouterOutlet } from '@angular/router';
import slugify from 'slugify';
import { NavBarService } from 'src/app/general/nav-bar/nav-bar.service';
+import { AnnouncementListComponent } from './general/announcement/announcement-list.component';
import { FooterComponent } from './general/footer/footer.component';
import { HeaderComponent } from './general/header/header.component';
import { NavBarMenuComponent } from './general/nav-bar-menu/nav-bar-menu.component';
-import { NoticeComponent } from './general/notice/notice.component';
import { PageLayoutService } from './page-layout/page-layout.service';
import { FeedbackWrapperService } from './sessions/feedback/feedback.service';
import { FullscreenService } from './sessions/service/fullscreen.service';
@@ -31,7 +31,7 @@ import { FullscreenService } from './sessions/service/fullscreen.service';
MatDrawerContent,
HeaderComponent,
NgClass,
- NoticeComponent,
+ AnnouncementListComponent,
RouterOutlet,
FooterComponent,
AsyncPipe,
diff --git a/frontend/src/app/general/announcement/announcement-list.component.html b/frontend/src/app/general/announcement/announcement-list.component.html
new file mode 100644
index 0000000000..2fe5ebaa08
--- /dev/null
+++ b/frontend/src/app/general/announcement/announcement-list.component.html
@@ -0,0 +1,13 @@
+
+
+
+ @for (
+ announcement of announcementWrapperService.visibleAnnouncements();
+ track announcement.id
+ ) {
+
+ }
+
diff --git a/frontend/src/app/general/announcement/announcement-list.component.ts b/frontend/src/app/general/announcement/announcement-list.component.ts
new file mode 100644
index 0000000000..d57ad526ed
--- /dev/null
+++ b/frontend/src/app/general/announcement/announcement-list.component.ts
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { Component } from '@angular/core';
+import { AnnouncementWrapperService } from 'src/app/general/announcement/announcement.service';
+import { AnnouncementComponent } from './announcement/announcement.component';
+
+@Component({
+ selector: 'app-announcement-list',
+ templateUrl: './announcement-list.component.html',
+ imports: [AnnouncementComponent],
+})
+export class AnnouncementListComponent {
+ constructor(public announcementWrapperService: AnnouncementWrapperService) {}
+}
diff --git a/frontend/src/app/general/announcement/announcement.service.ts b/frontend/src/app/general/announcement/announcement.service.ts
new file mode 100644
index 0000000000..89a0bf7285
--- /dev/null
+++ b/frontend/src/app/general/announcement/announcement.service.ts
@@ -0,0 +1,84 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { computed, inject, Injectable, signal } from '@angular/core';
+import { AnnouncementResponse, AnnouncementsService } from 'src/app/openapi';
+import { z } from 'zod';
+
+const dismissedAnnouncementsSchema = z.array(
+ z.object({
+ id: z.number(),
+ date: z.coerce.date(),
+ }),
+);
+
+export type DismissedAnnouncements = z.infer<
+ typeof dismissedAnnouncementsSchema
+>;
+
+@Injectable({
+ providedIn: 'root',
+})
+export class AnnouncementWrapperService {
+ LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY = 'dismissedAnnouncements';
+
+ public announcements = signal(undefined);
+ public dismissedAnnouncements = signal([]);
+ private announcementsService: AnnouncementsService =
+ inject(AnnouncementsService);
+ public visibleAnnouncements = computed(() => {
+ const announcements = this.announcements();
+ if (!announcements) return [];
+
+ return announcements.filter(
+ (announcement) =>
+ !this.dismissedAnnouncements().find((dismissedAnnouncement) => {
+ return (
+ dismissedAnnouncement.id === announcement.id &&
+ announcement.dismissible
+ );
+ }),
+ );
+ });
+
+ constructor() {
+ this.refreshAnnouncements();
+ }
+
+ dismissAnnouncement(announcementId: number): void {
+ this.dismissedAnnouncements.set([
+ ...this.dismissedAnnouncements(),
+ { id: announcementId, date: new Date() },
+ ]);
+ localStorage.setItem(
+ this.LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY,
+ JSON.stringify(this.dismissedAnnouncements()),
+ );
+ }
+
+ resetDismissedAnnouncements(): void {
+ this.dismissedAnnouncements.set([]);
+ localStorage.removeItem(this.LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY);
+ }
+
+ refreshAnnouncements(): void {
+ const localDismissedAnnouncements = localStorage.getItem(
+ this.LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY,
+ );
+ try {
+ const result = dismissedAnnouncementsSchema.safeParse(
+ JSON.parse(localDismissedAnnouncements ?? '[]'),
+ );
+ if (result.success) {
+ this.dismissedAnnouncements.set(result.data);
+ }
+ } catch (e) {
+ console.error(e);
+ localStorage.removeItem(this.LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY);
+ }
+ this.announcementsService.getAnnouncements().subscribe((res) => {
+ this.announcements.set(res);
+ });
+ }
+}
diff --git a/frontend/src/app/general/announcement/announcement.stories.ts b/frontend/src/app/general/announcement/announcement.stories.ts
new file mode 100644
index 0000000000..f5edd877ab
--- /dev/null
+++ b/frontend/src/app/general/announcement/announcement.stories.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
+import { userEvent, within } from '@storybook/test';
+import { AnnouncementLevel } from 'src/app/openapi';
+import {
+ mockAnnouncement,
+ mockAnnouncementWrapperServiceProvider,
+} from 'src/storybook/announcements';
+import { AnnouncementListComponent } from './announcement-list.component';
+
+const meta: Meta = {
+ title: 'General Components/Announcements',
+ component: AnnouncementListComponent,
+ parameters: {
+ screenshot: {
+ delay: 1000,
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const AllLevels: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ mockAnnouncementWrapperServiceProvider(
+ Object.values(AnnouncementLevel).map((level, i) => ({
+ ...mockAnnouncement,
+ title: 'This is an example announcement with level ' + level,
+ level,
+ id: i,
+ })),
+ [],
+ ),
+ ],
+ }),
+ ],
+};
+
+export const DismissedAnnouncements: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ mockAnnouncementWrapperServiceProvider(
+ [
+ mockAnnouncement,
+ {
+ ...mockAnnouncement,
+ message: 'This announcement is not visible',
+ id: 2,
+ },
+ {
+ ...mockAnnouncement,
+ message:
+ "This announcement is visible despite being dismissed, because it's not dismissible",
+ dismissible: false,
+ id: 3,
+ },
+ ],
+ [{ id: 3, date: new Date() }],
+ ),
+ ],
+ }),
+ ],
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const dismissButton = canvas.getByTestId('announcement-2');
+ await userEvent.click(dismissButton);
+ },
+};
diff --git a/frontend/src/app/general/notice/notice.component.css b/frontend/src/app/general/announcement/announcement/announcement.component.css
similarity index 100%
rename from frontend/src/app/general/notice/notice.component.css
rename to frontend/src/app/general/announcement/announcement/announcement.component.css
diff --git a/frontend/src/app/general/announcement/announcement/announcement.component.html b/frontend/src/app/general/announcement/announcement/announcement.component.html
new file mode 100644
index 0000000000..1717397a54
--- /dev/null
+++ b/frontend/src/app/general/announcement/announcement/announcement.component.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ @if (announcement().dismissible) {
+
+ }
+
diff --git a/frontend/src/app/general/announcement/announcement/announcement.component.ts b/frontend/src/app/general/announcement/announcement/announcement.component.ts
new file mode 100644
index 0000000000..bb9678fad1
--- /dev/null
+++ b/frontend/src/app/general/announcement/announcement/announcement.component.ts
@@ -0,0 +1,47 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {
+ animate,
+ state,
+ style,
+ transition,
+ trigger,
+} from '@angular/animations';
+import { Component, inject, input } from '@angular/core';
+import { MatIconButton } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { MarkdownComponent, provideMarkdown } from 'ngx-markdown';
+import { AnnouncementResponse } from '../../../openapi';
+import { AnnouncementWrapperService } from '../announcement.service';
+
+@Component({
+ selector: 'app-announcement',
+ imports: [MatIconButton, MatIconModule, MarkdownComponent],
+ templateUrl: './announcement.component.html',
+ styleUrls: ['./announcement.component.css'],
+ providers: [provideMarkdown()],
+ animations: [
+ trigger('announcementAnimation', [
+ state('void', style({ height: '0' })),
+ state('*', style({ height: '*' })),
+ transition('void <=> *', animate('150ms ease-in-out')),
+ ]),
+ ],
+})
+export class AnnouncementComponent {
+ announcement = input.required();
+ isFake = input(false);
+ announcementWrapperService: AnnouncementWrapperService = inject(
+ AnnouncementWrapperService,
+ );
+
+ dismissAnnouncement() {
+ if (!this.isFake()) {
+ this.announcementWrapperService.dismissAnnouncement(
+ this.announcement().id,
+ );
+ }
+ }
+}
diff --git a/frontend/src/app/general/auth/auth/auth.component.html b/frontend/src/app/general/auth/auth/auth.component.html
index 2897553366..f95c19c935 100644
--- a/frontend/src/app/general/auth/auth/auth.component.html
+++ b/frontend/src/app/general/auth/auth/auth.component.html
@@ -4,10 +4,6 @@
-->
-
-
@if (params["error"]) {
diff --git a/frontend/src/app/general/auth/auth/auth.component.ts b/frontend/src/app/general/auth/auth/auth.component.ts
index 25df7016bb..21b5586ce3 100644
--- a/frontend/src/app/general/auth/auth/auth.component.ts
+++ b/frontend/src/app/general/auth/auth/auth.component.ts
@@ -12,18 +12,11 @@ import { DOCS_URL } from 'src/app/environment';
import { MetadataService } from 'src/app/general/metadata/metadata.service';
import { PageLayoutService } from 'src/app/page-layout/page-layout.service';
import { AuthenticationWrapperService } from 'src/app/services/auth/auth.service';
-import { WelcomeComponent } from '../../welcome/welcome.component';
@Component({
selector: 'app-auth',
templateUrl: './auth.component.html',
- imports: [
- MatButton,
- AsyncPipe,
- WelcomeComponent,
- MatIconModule,
- MatButtonModule,
- ],
+ imports: [MatButton, AsyncPipe, MatIconModule, MatButtonModule],
})
export class AuthComponent implements OnInit {
public params = {} as Params;
diff --git a/frontend/src/app/general/notice/notice.component.html b/frontend/src/app/general/notice/notice.component.html
deleted file mode 100644
index 5f29a6ad83..0000000000
--- a/frontend/src/app/general/notice/notice.component.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
0,
- }"
->
- @for (notice of noticesWrapperService.notices$ | async; track notice.id) {
-
- }
-
diff --git a/frontend/src/app/general/notice/notice.component.ts b/frontend/src/app/general/notice/notice.component.ts
deleted file mode 100644
index 9f8963304a..0000000000
--- a/frontend/src/app/general/notice/notice.component.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-import { AsyncPipe, NgClass } from '@angular/common';
-import { Component } from '@angular/core';
-import { NoticeWrapperService } from 'src/app/general/notice/notice.service';
-
-@Component({
- selector: 'app-notice',
- templateUrl: './notice.component.html',
- styleUrls: ['./notice.component.css'],
- imports: [NgClass, AsyncPipe],
-})
-export class NoticeComponent {
- constructor(public noticesWrapperService: NoticeWrapperService) {}
-}
diff --git a/frontend/src/app/general/notice/notice.service.ts b/frontend/src/app/general/notice/notice.service.ts
deleted file mode 100644
index 9c0012a936..0000000000
--- a/frontend/src/app/general/notice/notice.service.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-import { Injectable } from '@angular/core';
-import { BehaviorSubject } from 'rxjs';
-import { NoticeResponse, NoticesService } from 'src/app/openapi';
-
-@Injectable({
- providedIn: 'root',
-})
-export class NoticeWrapperService {
- private _notices = new BehaviorSubject
(
- undefined,
- );
- public readonly notices$ = this._notices.asObservable();
-
- constructor(private noticesService: NoticesService) {
- this.refreshNotices();
- }
-
- refreshNotices(): void {
- this._notices.next(undefined);
- this.noticesService.getNotices().subscribe((res) => {
- this._notices.next(res);
- });
- }
-}
diff --git a/frontend/src/app/general/notice/notice.stories.ts b/frontend/src/app/general/notice/notice.stories.ts
deleted file mode 100644
index 6b278e873a..0000000000
--- a/frontend/src/app/general/notice/notice.stories.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
-import { NoticeLevel } from 'src/app/openapi';
-import {
- mockNotice,
- mockNoticeWrapperServiceProvider,
-} from 'src/storybook/notices';
-import { NoticeComponent } from './notice.component';
-
-const meta: Meta = {
- title: 'General Components/Notices',
- component: NoticeComponent,
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const AllLevels: Story = {
- args: {},
- decorators: [
- moduleMetadata({
- providers: [
- mockNoticeWrapperServiceProvider(
- Object.values(NoticeLevel).map((level) => ({
- ...mockNotice,
- title: 'This is an example notice with level ' + level,
- level,
- })),
- ),
- ],
- }),
- ],
-};
diff --git a/frontend/src/app/general/welcome/welcome.component.html b/frontend/src/app/general/welcome/welcome.component.html
deleted file mode 100644
index f5e7820f88..0000000000
--- a/frontend/src/app/general/welcome/welcome.component.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
- Welcome to the
-
Capella Collaboration Manager
-
diff --git a/frontend/src/app/general/welcome/welcome.component.ts b/frontend/src/app/general/welcome/welcome.component.ts
deleted file mode 100644
index 161f8ff2e3..0000000000
--- a/frontend/src/app/general/welcome/welcome.component.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-import { CommonModule } from '@angular/common';
-import { ChangeDetectionStrategy, Component } from '@angular/core';
-
-@Component({
- selector: 'app-welcome',
- imports: [CommonModule],
- templateUrl: './welcome.component.html',
- changeDetection: ChangeDetectionStrategy.OnPush,
-})
-export class WelcomeComponent {}
diff --git a/frontend/src/app/openapi/.openapi-generator/FILES b/frontend/src/app/openapi/.openapi-generator/FILES
index da2c904078..4a87844101 100644
--- a/frontend/src/app/openapi/.openapi-generator/FILES
+++ b/frontend/src/app/openapi/.openapi-generator/FILES
@@ -1,4 +1,5 @@
api.module.ts
+api/announcements.service.ts
api/api.ts
api/authentication.service.ts
api/configuration.service.ts
@@ -6,7 +7,6 @@ api/events.service.ts
api/feedback.service.ts
api/health.service.ts
api/integrations-pure-variants.service.ts
-api/notices.service.ts
api/permissions.service.ts
api/projects-events.service.ts
api/projects-models-backups.service.ts
@@ -36,6 +36,8 @@ encoder.ts
index.ts
model/admin-scopes-input.ts
model/admin-scopes-output.ts
+model/announcement-level.ts
+model/announcement-response.ts
model/anonymized-session.ts
model/authorization-response.ts
model/backup-pipeline-run.ts
@@ -50,8 +52,8 @@ model/built-in-link-item.ts
model/built-in-navbar-link.ts
model/cpu-resources-input.ts
model/cpu-resources-output.ts
+model/create-announcement-request.ts
model/create-backup.ts
-model/create-notice-request.ts
model/create-t4-c-instance.ts
model/create-t4-c-repository.ts
model/create-tool-input.ts
@@ -112,8 +114,6 @@ model/models.ts
model/navbar-configuration-input-external-links-inner.ts
model/navbar-configuration-input.ts
model/navbar-configuration-output.ts
-model/notice-level.ts
-model/notice-response.ts
model/page-history-event.ts
model/page-pipeline-run.ts
model/patch-project-user.ts
diff --git a/frontend/src/app/openapi/api/notices.service.ts b/frontend/src/app/openapi/api/announcements.service.ts
similarity index 59%
rename from frontend/src/app/openapi/api/notices.service.ts
rename to frontend/src/app/openapi/api/announcements.service.ts
index c614dbcc62..9a64769c69 100644
--- a/frontend/src/app/openapi/api/notices.service.ts
+++ b/frontend/src/app/openapi/api/announcements.service.ts
@@ -19,11 +19,11 @@ import { CustomHttpParameterCodec } from '../encoder';
import { Observable } from 'rxjs';
// @ts-ignore
-import { CreateNoticeRequest } from '../model/create-notice-request';
+import { AnnouncementResponse } from '../model/announcement-response';
// @ts-ignore
-import { HTTPValidationError } from '../model/http-validation-error';
+import { CreateAnnouncementRequest } from '../model/create-announcement-request';
// @ts-ignore
-import { NoticeResponse } from '../model/notice-response';
+import { HTTPValidationError } from '../model/http-validation-error';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
@@ -34,7 +34,7 @@ import { Configuration } from '../configurat
@Injectable({
providedIn: 'root'
})
-export class NoticesService {
+export class AnnouncementsService {
protected basePath = 'http://localhost';
public defaultHeaders = new HttpHeaders();
@@ -97,18 +97,18 @@ export class NoticesService {
}
/**
- * Create Notice
+ * Create Announcement
* This route requires the following permissions: `admin.announcements:create`
- * @param createNoticeRequest
+ * @param createAnnouncementRequest
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
- public createNotice(createNoticeRequest: CreateNoticeRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable;
- public createNotice(createNoticeRequest: CreateNoticeRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
- public createNotice(createNoticeRequest: CreateNoticeRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
- public createNotice(createNoticeRequest: CreateNoticeRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
- if (createNoticeRequest === null || createNoticeRequest === undefined) {
- throw new Error('Required parameter createNoticeRequest was null or undefined when calling createNotice.');
+ public createAnnouncement(createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable;
+ public createAnnouncement(createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public createAnnouncement(createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public createAnnouncement(createAnnouncementRequest: CreateAnnouncementRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
+ if (createAnnouncementRequest === null || createAnnouncementRequest === undefined) {
+ throw new Error('Required parameter createAnnouncementRequest was null or undefined when calling createAnnouncement.');
}
let localVarHeaders = this.defaultHeaders;
@@ -168,11 +168,11 @@ export class NoticesService {
}
}
- let localVarPath = `/api/v1/notices`;
+ let localVarPath = `/api/v1/announcements`;
return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
- body: createNoticeRequest,
+ body: createAnnouncementRequest,
responseType: responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
@@ -184,18 +184,18 @@ export class NoticesService {
}
/**
- * Delete Notice
+ * Delete Announcement
* This route requires the following permissions: `admin.announcements:delete`
- * @param noticeId
+ * @param announcementId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
- public deleteNotice(noticeId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable;
- public deleteNotice(noticeId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
- public deleteNotice(noticeId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
- public deleteNotice(noticeId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
- if (noticeId === null || noticeId === undefined) {
- throw new Error('Required parameter noticeId was null or undefined when calling deleteNotice.');
+ public deleteAnnouncement(announcementId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable;
+ public deleteAnnouncement(announcementId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public deleteAnnouncement(announcementId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public deleteAnnouncement(announcementId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
+ if (announcementId === null || announcementId === undefined) {
+ throw new Error('Required parameter announcementId was null or undefined when calling deleteAnnouncement.');
}
let localVarHeaders = this.defaultHeaders;
@@ -246,7 +246,7 @@ export class NoticesService {
}
}
- let localVarPath = `/api/v1/notices/${this.configuration.encodeParam({name: "noticeId", value: noticeId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`;
+ let localVarPath = `/api/v1/announcements/${this.configuration.encodeParam({name: "announcementId", value: announcementId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`;
return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
@@ -261,17 +261,17 @@ export class NoticesService {
}
/**
- * Get Notice By Id
- * @param noticeId
+ * Get Announcement By Id
+ * @param announcementId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
- public getNoticeById(noticeId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable;
- public getNoticeById(noticeId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
- public getNoticeById(noticeId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
- public getNoticeById(noticeId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
- if (noticeId === null || noticeId === undefined) {
- throw new Error('Required parameter noticeId was null or undefined when calling getNoticeById.');
+ public getAnnouncementById(announcementId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable;
+ public getAnnouncementById(announcementId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public getAnnouncementById(announcementId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public getAnnouncementById(announcementId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
+ if (announcementId === null || announcementId === undefined) {
+ throw new Error('Required parameter announcementId was null or undefined when calling getAnnouncementById.');
}
let localVarHeaders = this.defaultHeaders;
@@ -310,7 +310,7 @@ export class NoticesService {
}
}
- let localVarPath = `/api/v1/notices/${this.configuration.encodeParam({name: "noticeId", value: noticeId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`;
+ let localVarPath = `/api/v1/announcements/${this.configuration.encodeParam({name: "announcementId", value: announcementId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`;
return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
@@ -325,17 +325,98 @@ export class NoticesService {
}
/**
- * Get Notices
+ * Get Announcements
+ * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
+ * @param reportProgress flag to report request and response progress.
+ */
+ public getAnnouncements(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public getAnnouncements(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>;
+ public getAnnouncements(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>;
+ public getAnnouncements(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
+
+ let localVarHeaders = this.defaultHeaders;
+
+ let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
+ if (localVarHttpHeaderAcceptSelected === undefined) {
+ // to determine the Accept header
+ const httpHeaderAccepts: string[] = [
+ 'application/json'
+ ];
+ localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
+ }
+ if (localVarHttpHeaderAcceptSelected !== undefined) {
+ localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
+ }
+
+ let localVarHttpContext: HttpContext | undefined = options && options.context;
+ if (localVarHttpContext === undefined) {
+ localVarHttpContext = new HttpContext();
+ }
+
+ let localVarTransferCache: boolean | undefined = options && options.transferCache;
+ if (localVarTransferCache === undefined) {
+ localVarTransferCache = true;
+ }
+
+
+ let responseType_: 'text' | 'json' | 'blob' = 'json';
+ if (localVarHttpHeaderAcceptSelected) {
+ if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
+ responseType_ = 'text';
+ } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
+ responseType_ = 'json';
+ } else {
+ responseType_ = 'blob';
+ }
+ }
+
+ let localVarPath = `/api/v1/announcements`;
+ return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`,
+ {
+ context: localVarHttpContext,
+ responseType: responseType_,
+ withCredentials: this.configuration.withCredentials,
+ headers: localVarHeaders,
+ observe: observe,
+ transferCache: localVarTransferCache,
+ reportProgress: reportProgress
+ }
+ );
+ }
+
+ /**
+ * Update Announcement
+ * This route requires the following permissions: `admin.announcements:update`
+ * @param announcementId
+ * @param createAnnouncementRequest
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
- public getNotices(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
- public getNotices(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>;
- public getNotices(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>;
- public getNotices(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
+ public updateAnnouncement(announcementId: number, createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable;
+ public updateAnnouncement(announcementId: number, createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public updateAnnouncement(announcementId: number, createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public updateAnnouncement(announcementId: number, createAnnouncementRequest: CreateAnnouncementRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
+ if (announcementId === null || announcementId === undefined) {
+ throw new Error('Required parameter announcementId was null or undefined when calling updateAnnouncement.');
+ }
+ if (createAnnouncementRequest === null || createAnnouncementRequest === undefined) {
+ throw new Error('Required parameter createAnnouncementRequest was null or undefined when calling updateAnnouncement.');
+ }
let localVarHeaders = this.defaultHeaders;
+ let localVarCredential: string | undefined;
+ // authentication (Cookie) required
+ localVarCredential = this.configuration.lookupCredential('Cookie');
+ if (localVarCredential) {
+ }
+
+ // authentication (PersonalAccessToken) required
+ localVarCredential = this.configuration.lookupCredential('PersonalAccessToken');
+ if (localVarCredential) {
+ localVarHeaders = localVarHeaders.set('Authorization', 'Basic ' + localVarCredential);
+ }
+
let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
@@ -359,6 +440,15 @@ export class NoticesService {
}
+ // to determine the Content-Type header
+ const consumes: string[] = [
+ 'application/json'
+ ];
+ const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
+ if (httpContentTypeSelected !== undefined) {
+ localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected);
+ }
+
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
@@ -370,10 +460,11 @@ export class NoticesService {
}
}
- let localVarPath = `/api/v1/notices`;
- return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`,
+ let localVarPath = `/api/v1/announcements/${this.configuration.encodeParam({name: "announcementId", value: announcementId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`;
+ return this.httpClient.request('patch', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
+ body: createAnnouncementRequest,
responseType: responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
diff --git a/frontend/src/app/openapi/api/api.ts b/frontend/src/app/openapi/api/api.ts
index e8d473e35a..fff564883e 100644
--- a/frontend/src/app/openapi/api/api.ts
+++ b/frontend/src/app/openapi/api/api.ts
@@ -9,6 +9,8 @@
+ To generate a new version, run `make openapi` in the root directory of this repository.
*/
+export * from './announcements.service';
+import { AnnouncementsService } from './announcements.service';
export * from './authentication.service';
import { AuthenticationService } from './authentication.service';
export * from './configuration.service';
@@ -21,8 +23,6 @@ export * from './health.service';
import { HealthService } from './health.service';
export * from './integrations-pure-variants.service';
import { IntegrationsPureVariantsService } from './integrations-pure-variants.service';
-export * from './notices.service';
-import { NoticesService } from './notices.service';
export * from './permissions.service';
import { PermissionsService } from './permissions.service';
export * from './projects.service';
@@ -71,4 +71,4 @@ export * from './users-token.service';
import { UsersTokenService } from './users-token.service';
export * from './users-workspaces.service';
import { UsersWorkspacesService } from './users-workspaces.service';
-export const APIS = [AuthenticationService, ConfigurationService, EventsService, FeedbackService, HealthService, IntegrationsPureVariantsService, NoticesService, PermissionsService, ProjectsService, ProjectsEventsService, ProjectsModelsService, ProjectsModelsBackupsService, ProjectsModelsDiagramsService, ProjectsModelsGitService, ProjectsModelsModelComplexityBadgeService, ProjectsModelsProvisioningService, ProjectsModelsREADMEService, ProjectsModelsRestrictionsService, ProjectsModelsT4CService, ProjectsPermissionsService, ProjectsToolsService, SessionsService, SettingsModelsourcesGitService, SettingsModelsourcesT4CInstancesService, SettingsModelsourcesT4CLicenseServersService, ToolsService, UsersService, UsersPermissionsService, UsersSessionsService, UsersTokenService, UsersWorkspacesService];
+export const APIS = [AnnouncementsService, AuthenticationService, ConfigurationService, EventsService, FeedbackService, HealthService, IntegrationsPureVariantsService, PermissionsService, ProjectsService, ProjectsEventsService, ProjectsModelsService, ProjectsModelsBackupsService, ProjectsModelsDiagramsService, ProjectsModelsGitService, ProjectsModelsModelComplexityBadgeService, ProjectsModelsProvisioningService, ProjectsModelsREADMEService, ProjectsModelsRestrictionsService, ProjectsModelsT4CService, ProjectsPermissionsService, ProjectsToolsService, SessionsService, SettingsModelsourcesGitService, SettingsModelsourcesT4CInstancesService, SettingsModelsourcesT4CLicenseServersService, ToolsService, UsersService, UsersPermissionsService, UsersSessionsService, UsersTokenService, UsersWorkspacesService];
diff --git a/frontend/src/app/openapi/model/admin-scopes-input.ts b/frontend/src/app/openapi/model/admin-scopes-input.ts
index 15e9e05e86..e8199f8105 100644
--- a/frontend/src/app/openapi/model/admin-scopes-input.ts
+++ b/frontend/src/app/openapi/model/admin-scopes-input.ts
@@ -87,9 +87,10 @@ export namespace AdminScopesInput {
Update: 'UPDATE' as ToolsEnum,
Delete: 'DELETE' as ToolsEnum
};
- export type AnnouncementsEnum = 'CREATE' | 'DELETE';
+ export type AnnouncementsEnum = 'CREATE' | 'UPDATE' | 'DELETE';
export const AnnouncementsEnum = {
Create: 'CREATE' as AnnouncementsEnum,
+ Update: 'UPDATE' as AnnouncementsEnum,
Delete: 'DELETE' as AnnouncementsEnum
};
export type MonitoringEnum = 'GET';
diff --git a/frontend/src/app/openapi/model/admin-scopes-output.ts b/frontend/src/app/openapi/model/admin-scopes-output.ts
index 30cc99fade..faf6d23476 100644
--- a/frontend/src/app/openapi/model/admin-scopes-output.ts
+++ b/frontend/src/app/openapi/model/admin-scopes-output.ts
@@ -87,9 +87,10 @@ export namespace AdminScopesOutput {
Update: 'UPDATE' as ToolsEnum,
Delete: 'DELETE' as ToolsEnum
};
- export type AnnouncementsEnum = 'CREATE' | 'DELETE';
+ export type AnnouncementsEnum = 'CREATE' | 'UPDATE' | 'DELETE';
export const AnnouncementsEnum = {
Create: 'CREATE' as AnnouncementsEnum,
+ Update: 'UPDATE' as AnnouncementsEnum,
Delete: 'DELETE' as AnnouncementsEnum
};
export type MonitoringEnum = 'GET';
diff --git a/frontend/src/app/openapi/model/announcement-level.ts b/frontend/src/app/openapi/model/announcement-level.ts
new file mode 100644
index 0000000000..c136a98802
--- /dev/null
+++ b/frontend/src/app/openapi/model/announcement-level.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Capella Collaboration Manager API
+ *
+ * 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 AnnouncementLevel = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'alert';
+
+export const AnnouncementLevel = {
+ Primary: 'primary' as AnnouncementLevel,
+ Secondary: 'secondary' as AnnouncementLevel,
+ Success: 'success' as AnnouncementLevel,
+ Danger: 'danger' as AnnouncementLevel,
+ Warning: 'warning' as AnnouncementLevel,
+ Info: 'info' as AnnouncementLevel,
+ Alert: 'alert' as AnnouncementLevel
+};
+
diff --git a/frontend/src/app/openapi/model/notice-response.ts b/frontend/src/app/openapi/model/announcement-response.ts
similarity index 69%
rename from frontend/src/app/openapi/model/notice-response.ts
rename to frontend/src/app/openapi/model/announcement-response.ts
index 4581bbb5c3..5812dd670b 100644
--- a/frontend/src/app/openapi/model/notice-response.ts
+++ b/frontend/src/app/openapi/model/announcement-response.ts
@@ -9,16 +9,17 @@
+ To generate a new version, run `make openapi` in the root directory of this repository.
*/
-import { NoticeLevel } from './notice-level';
+import { AnnouncementLevel } from './announcement-level';
-export interface NoticeResponse {
- level: NoticeLevel;
+export interface AnnouncementResponse {
+ level: AnnouncementLevel;
title: string;
message: string;
+ dismissible: boolean;
id: number;
}
-export namespace NoticeResponse {
+export namespace AnnouncementResponse {
}
diff --git a/frontend/src/app/openapi/model/create-notice-request.ts b/frontend/src/app/openapi/model/create-announcement-request.ts
similarity index 67%
rename from frontend/src/app/openapi/model/create-notice-request.ts
rename to frontend/src/app/openapi/model/create-announcement-request.ts
index c664facc31..7eac869675 100644
--- a/frontend/src/app/openapi/model/create-notice-request.ts
+++ b/frontend/src/app/openapi/model/create-announcement-request.ts
@@ -9,15 +9,16 @@
+ To generate a new version, run `make openapi` in the root directory of this repository.
*/
-import { NoticeLevel } from './notice-level';
+import { AnnouncementLevel } from './announcement-level';
-export interface CreateNoticeRequest {
- level: NoticeLevel;
+export interface CreateAnnouncementRequest {
+ level: AnnouncementLevel;
title: string;
message: string;
+ dismissible: boolean;
}
-export namespace CreateNoticeRequest {
+export namespace CreateAnnouncementRequest {
}
diff --git a/frontend/src/app/openapi/model/models.ts b/frontend/src/app/openapi/model/models.ts
index 0975d4bbb2..4169527ee1 100644
--- a/frontend/src/app/openapi/model/models.ts
+++ b/frontend/src/app/openapi/model/models.ts
@@ -11,6 +11,8 @@
export * from './admin-scopes-input';
export * from './admin-scopes-output';
+export * from './announcement-level';
+export * from './announcement-response';
export * from './anonymized-session';
export * from './authorization-response';
export * from './backup';
@@ -25,8 +27,8 @@ export * from './built-in-link-item';
export * from './built-in-navbar-link';
export * from './cpu-resources-input';
export * from './cpu-resources-output';
+export * from './create-announcement-request';
export * from './create-backup';
-export * from './create-notice-request';
export * from './create-t4-c-instance';
export * from './create-t4-c-repository';
export * from './create-tool-input';
@@ -86,8 +88,6 @@ export * from './model-provisioning';
export * from './navbar-configuration-input';
export * from './navbar-configuration-input-external-links-inner';
export * from './navbar-configuration-output';
-export * from './notice-level';
-export * from './notice-response';
export * from './page-history-event';
export * from './page-pipeline-run';
export * from './patch-project';
diff --git a/frontend/src/app/openapi/model/notice-level.ts b/frontend/src/app/openapi/model/notice-level.ts
deleted file mode 100644
index 7f82486485..0000000000
--- a/frontend/src/app/openapi/model/notice-level.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
- * SPDX-License-Identifier: Apache-2.0
- *
- * Capella Collaboration Manager API
- *
- * 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 NoticeLevel = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'alert';
-
-export const NoticeLevel = {
- Primary: 'primary' as NoticeLevel,
- Secondary: 'secondary' as NoticeLevel,
- Success: 'success' as NoticeLevel,
- Danger: 'danger' as NoticeLevel,
- Warning: 'warning' as NoticeLevel,
- Info: 'info' as NoticeLevel,
- Alert: 'alert' as NoticeLevel
-};
-
diff --git a/frontend/src/app/page-layout/page-layout.service.ts b/frontend/src/app/page-layout/page-layout.service.ts
index 645366fb88..be05e6a3f7 100644
--- a/frontend/src/app/page-layout/page-layout.service.ts
+++ b/frontend/src/app/page-layout/page-layout.service.ts
@@ -10,15 +10,19 @@ import { Injectable } from '@angular/core';
export class PageLayoutService {
showNavbar = true;
showFooter = true;
- showNotice = true;
+ showAnnouncement = true;
hideNavbar() {
this.showNavbar = false;
}
+ setShowAnnouncement(show: boolean) {
+ this.showAnnouncement = show;
+ }
+
enableAll() {
this.showNavbar = true;
this.showFooter = true;
- this.showNotice = true;
+ this.showAnnouncement = true;
}
}
diff --git a/frontend/src/app/sessions/sessions.component.html b/frontend/src/app/sessions/sessions.component.html
index 7460f08f83..742acd7a35 100644
--- a/frontend/src/app/sessions/sessions.component.html
+++ b/frontend/src/app/sessions/sessions.component.html
@@ -3,8 +3,6 @@
~ SPDX-License-Identifier: Apache-2.0
-->
-
-
diff --git a/frontend/src/app/sessions/sessions.component.ts b/frontend/src/app/sessions/sessions.component.ts
index decbc56520..eb47086c6c 100644
--- a/frontend/src/app/sessions/sessions.component.ts
+++ b/frontend/src/app/sessions/sessions.component.ts
@@ -3,7 +3,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { Component } from '@angular/core';
-import { WelcomeComponent } from '../general/welcome/welcome.component';
import { ActiveSessionsComponent } from './user-sessions-wrapper/active-sessions/active-sessions.component';
import { CreatePersistentSessionComponent } from './user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component';
import { UserSessionsWrapperComponent } from './user-sessions-wrapper/user-sessions-wrapper.component';
@@ -15,7 +14,6 @@ import { UserSessionsWrapperComponent } from './user-sessions-wrapper/user-sessi
UserSessionsWrapperComponent,
CreatePersistentSessionComponent,
ActiveSessionsComponent,
- WelcomeComponent,
],
})
export class SessionsComponent {}
diff --git a/frontend/src/app/settings/core/alert-settings/alert-settings.component.html b/frontend/src/app/settings/core/alert-settings/alert-settings.component.html
deleted file mode 100644
index d5733041a2..0000000000
--- a/frontend/src/app/settings/core/alert-settings/alert-settings.component.html
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
-
Handle alerts
- @if ((noticeWrapperService.notices$ | async) === undefined) {
- @for (_ of [0, 1, 2]; track $index) {
-
- }
- } @else {
-
- @for (
- notice of noticeWrapperService.notices$ | async;
- track notice.id
- ) {
-
-
- {{ notice.title }}
-
- {{ notice.level }}
-
-
-
-
{{ notice.message }}
-
-
-
- } @empty {
- There are no existing alerts.
- }
-
- }
-
-
diff --git a/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts b/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts
deleted file mode 100644
index 7c2ec4ec81..0000000000
--- a/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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 {
- AbstractControl,
- FormControl,
- FormGroup,
- ValidationErrors,
- ValidatorFn,
- Validators,
- FormsModule,
- ReactiveFormsModule,
-} from '@angular/forms';
-import { MatButton } from '@angular/material/button';
-import { MatOption } from '@angular/material/core';
-import { MatExpansionModule } from '@angular/material/expansion';
-import { MatError, MatFormFieldModule } from '@angular/material/form-field';
-import { MatInput } from '@angular/material/input';
-import { MatSelect } from '@angular/material/select';
-import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
-import { NoticeWrapperService } from 'src/app/general/notice/notice.service';
-import {
- CreateNoticeRequest,
- NoticeLevel,
- NoticesService,
-} from 'src/app/openapi';
-
-@Component({
- selector: 'app-alert-settings',
- templateUrl: './alert-settings.component.html',
- imports: [
- FormsModule,
- ReactiveFormsModule,
- MatFormFieldModule,
- MatInput,
- MatSelect,
- MatOption,
- MatError,
- MatButton,
- MatExpansionModule,
- AsyncPipe,
- NgxSkeletonLoaderModule,
- ],
-})
-export class AlertSettingsComponent {
- createAlertForm = new FormGroup(
- {
- title: new FormControl(''),
- message: new FormControl(''),
- level: new FormControl('', Validators.required),
- },
- this.titleOrDescriptionRequired(),
- );
-
- get message(): FormControl {
- return this.createAlertForm.get('message') as FormControl;
- }
-
- get noticeLevels(): string[] {
- return Object.values(NoticeLevel);
- }
-
- constructor(
- public noticeWrapperService: NoticeWrapperService,
- private noticeService: NoticesService,
- ) {}
-
- titleOrDescriptionRequired(): ValidatorFn {
- return (control: AbstractControl): ValidationErrors | null => {
- if (control.get('title')?.value || control.get('message')?.value) {
- control.get('message')?.setErrors(null);
- return null;
- }
- control.get('message')?.setErrors({ titleOrDescriptionAvailable: true });
- return { titleOrDescriptionAvailable: true };
- };
- }
-
- createNotice(): void {
- if (this.createAlertForm.valid) {
- this.noticeService
- .createNotice(this.createAlertForm.value as CreateNoticeRequest)
- .subscribe({
- next: () => {
- this.noticeWrapperService.refreshNotices();
- },
- });
- }
- }
-
- deleteNotice(id: number): void {
- this.noticeService.deleteNotice(id).subscribe({
- next: () => {
- this.noticeWrapperService.refreshNotices();
- },
- });
- }
-}
diff --git a/frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts b/frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts
deleted file mode 100644
index c4032fcb65..0000000000
--- a/frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
-import { userEvent, within } from '@storybook/test';
-import {
- mockNotice,
- mockNoticeWrapperServiceProvider,
-} from 'src/storybook/notices';
-import { AlertSettingsComponent } from './alert-settings.component';
-
-const meta: Meta
= {
- title: 'Settings Components/Alert Settings',
- component: AlertSettingsComponent,
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Loading: Story = {
- args: {},
-};
-
-export const NoAlerts: Story = {
- args: {},
- decorators: [
- moduleMetadata({
- providers: [mockNoticeWrapperServiceProvider([])],
- }),
- ],
-};
-
-export const SomeAlerts: Story = {
- args: {},
- decorators: [
- moduleMetadata({
- providers: [
- mockNoticeWrapperServiceProvider([
- mockNotice,
- { ...mockNotice, id: 2 },
- ]),
- ],
- }),
- ],
-};
-
-export const AlertExpanded: Story = {
- args: {},
- decorators: [
- moduleMetadata({
- providers: [mockNoticeWrapperServiceProvider([mockNotice])],
- }),
- ],
- play: async ({ canvasElement }) => {
- const canvas = within(canvasElement);
- const alert = canvas.getByTestId('alert-1');
- await userEvent.click(alert);
- },
-};
diff --git a/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.html b/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.html
new file mode 100644
index 0000000000..2c93ce07ea
--- /dev/null
+++ b/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.html
@@ -0,0 +1,36 @@
+
+
+
+
Create new Announcement
+
+
+
Edit existing Announcements
+ @if (announcementWrapperService.announcements() === undefined) {
+ @for (_ of [0, 1, 2]; track $index) {
+
+ }
+ } @else {
+ @for (
+ announcement of announcementWrapperService.announcements();
+ track announcement.id
+ ) {
+
+ } @empty {
+
No announcements found
+ }
+ }
+
diff --git a/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.ts b/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.ts
new file mode 100644
index 0000000000..74456114d1
--- /dev/null
+++ b/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.ts
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { Component, inject, OnDestroy, OnInit } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatExpansionModule } from '@angular/material/expansion';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { AnnouncementWrapperService } from 'src/app/general/announcement/announcement.service';
+import { PageLayoutService } from '../../../page-layout/page-layout.service';
+import { EditAnnouncementComponent } from './edit-announcement/edit-announcement/edit-announcement.component';
+
+@Component({
+ selector: 'app-announcement-settings',
+ templateUrl: './announcement-settings.component.html',
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ MatFormFieldModule,
+ MatExpansionModule,
+ NgxSkeletonLoaderModule,
+ EditAnnouncementComponent,
+ ],
+})
+export class AnnouncementSettingsComponent implements OnInit, OnDestroy {
+ public announcementWrapperService: AnnouncementWrapperService = inject(
+ AnnouncementWrapperService,
+ );
+ public pageLayoutService: PageLayoutService = inject(PageLayoutService);
+
+ ngOnInit(): void {
+ this.pageLayoutService.setShowAnnouncement(false);
+ }
+
+ ngOnDestroy(): void {
+ this.pageLayoutService.setShowAnnouncement(true);
+ }
+}
diff --git a/frontend/src/app/settings/core/announcement-settings/announcement-settings.stories.ts b/frontend/src/app/settings/core/announcement-settings/announcement-settings.stories.ts
new file mode 100644
index 0000000000..9b427d4d32
--- /dev/null
+++ b/frontend/src/app/settings/core/announcement-settings/announcement-settings.stories.ts
@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
+import {
+ mockAnnouncement,
+ mockAnnouncementWrapperServiceProvider,
+} from 'src/storybook/announcements';
+import { AnnouncementSettingsComponent } from './announcement-settings.component';
+
+const meta: Meta = {
+ title: 'Settings Components/Announcement Settings',
+ component: AnnouncementSettingsComponent,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Loading: Story = {
+ args: {},
+};
+
+export const NoAnnouncements: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [mockAnnouncementWrapperServiceProvider([], [])],
+ }),
+ ],
+};
+
+export const SomeAnnouncements: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ mockAnnouncementWrapperServiceProvider(
+ [mockAnnouncement, { ...mockAnnouncement, id: 2 }],
+ [],
+ ),
+ ],
+ }),
+ ],
+};
diff --git a/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.html b/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.html
new file mode 100644
index 0000000000..76f727d86a
--- /dev/null
+++ b/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.html
@@ -0,0 +1,62 @@
+
+
+
diff --git a/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.ts b/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.ts
new file mode 100644
index 0000000000..1d93fc7e3e
--- /dev/null
+++ b/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.ts
@@ -0,0 +1,167 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { Component, computed, effect, inject, input } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import {
+ AbstractControl,
+ FormControl,
+ FormGroup,
+ ReactiveFormsModule,
+ ValidationErrors,
+ ValidatorFn,
+ Validators,
+} from '@angular/forms';
+import { MatOption } from '@angular/material/autocomplete';
+import { MatButton } from '@angular/material/button';
+import { MatCheckbox } from '@angular/material/checkbox';
+import { MatError, MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { MatSelect } from '@angular/material/select';
+import { AnnouncementWrapperService } from '../../../../../general/announcement/announcement.service';
+import { AnnouncementComponent } from '../../../../../general/announcement/announcement/announcement.component';
+import { ToastService } from '../../../../../helpers/toast/toast.service';
+import {
+ AnnouncementLevel,
+ AnnouncementResponse,
+ AnnouncementsService,
+ CreateAnnouncementRequest,
+} from '../../../../../openapi';
+
+@Component({
+ selector: 'app-edit-announcement',
+ imports: [
+ MatError,
+ MatFormField,
+ MatInput,
+ MatLabel,
+ MatOption,
+ MatSelect,
+ ReactiveFormsModule,
+ AnnouncementComponent,
+ MatButton,
+ MatCheckbox,
+ ],
+ templateUrl: './edit-announcement.component.html',
+})
+export class EditAnnouncementComponent {
+ public existingAnnouncement = input();
+ announcementService: AnnouncementsService = inject(AnnouncementsService);
+ announcementWrapperService: AnnouncementWrapperService = inject(
+ AnnouncementWrapperService,
+ );
+ private toastService: ToastService = inject(ToastService);
+
+ announcementForm = new FormGroup(
+ {
+ title: new FormControl(''),
+ message: new FormControl(''),
+ level: new FormControl('', Validators.required),
+ dismissible: new FormControl(true),
+ },
+ this.titleOrDescriptionRequired(),
+ );
+ title = toSignal(this.announcementForm.controls.title.valueChanges);
+ message = toSignal(this.announcementForm.controls.message.valueChanges);
+ level = toSignal(this.announcementForm.controls.level.valueChanges);
+ dismissible = toSignal(
+ this.announcementForm.controls.dismissible.valueChanges,
+ );
+
+ displayedAnnouncement = computed(() => {
+ return {
+ id: this.existingAnnouncement()?.id || 0,
+ title: this.title() ?? '',
+ message:
+ this.message() ??
+ (!this.message() && !this.title()
+ ? 'Write some text above to preview your announcement'
+ : ''),
+ level: (this.level() as AnnouncementLevel) ?? AnnouncementLevel.Info,
+ dismissible: this.dismissible() ?? true,
+ };
+ });
+
+ constructor() {
+ effect(() => {
+ if (this.existingAnnouncement()) {
+ this.announcementForm.patchValue(this.existingAnnouncement()!);
+ }
+ });
+ }
+
+ announcementLevels = computed(() => {
+ return Object.values(AnnouncementLevel);
+ });
+
+ titleOrDescriptionRequired(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (control.get('title')?.value || control.get('message')?.value) {
+ control.get('message')?.setErrors(null);
+ return null;
+ }
+ control.get('message')?.setErrors({ titleOrDescriptionAvailable: true });
+ return { titleOrDescriptionAvailable: true };
+ };
+ }
+
+ createAnnouncement(): void {
+ if (this.announcementForm.valid && !this.existingAnnouncement()) {
+ this.announcementService
+ .createAnnouncement(
+ this.announcementForm.value as CreateAnnouncementRequest,
+ )
+ .subscribe({
+ next: () => {
+ this.toastService.showSuccess(
+ 'Announcement created',
+ 'The announcement has been created successfully',
+ );
+ this.announcementWrapperService.refreshAnnouncements();
+ this.announcementForm.reset({
+ title: '',
+ message: '',
+ level: '',
+ dismissible: true,
+ });
+ },
+ });
+ }
+ }
+
+ updateAnnouncement(): void {
+ if (this.announcementForm.valid && this.existingAnnouncement()) {
+ this.announcementService
+ .updateAnnouncement(
+ this.existingAnnouncement()!.id,
+ this.announcementForm.value as CreateAnnouncementRequest,
+ )
+ .subscribe({
+ next: () => {
+ this.toastService.showSuccess(
+ 'Announcement updated',
+ 'The announcement has been updated successfully',
+ );
+ this.announcementWrapperService.refreshAnnouncements();
+ },
+ });
+ }
+ }
+
+ deleteAnnouncement(): void {
+ if (this.existingAnnouncement()) {
+ this.announcementService
+ .deleteAnnouncement(this.existingAnnouncement()!.id)
+ .subscribe({
+ next: () => {
+ this.toastService.showSuccess(
+ 'Announcement deleted',
+ 'The announcement has been deleted successfully',
+ );
+ this.announcementWrapperService.refreshAnnouncements();
+ },
+ });
+ }
+ }
+}
diff --git a/frontend/src/app/settings/settings.component.html b/frontend/src/app/settings/settings.component.html
index 3cf7a3fcce..9533e52b3e 100644
--- a/frontend/src/app/settings/settings.component.html
+++ b/frontend/src/app/settings/settings.component.html
@@ -11,7 +11,7 @@
supervised_user_circle
-
+
Announcements
add_alert
diff --git a/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.html b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.html
new file mode 100644
index 0000000000..5f59bea62c
--- /dev/null
+++ b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.html
@@ -0,0 +1,23 @@
+
+
+@if (hiddenAnnouncements().length > 0) {
+
+
Reset hidden announcements
+
+ You currently have {{ hiddenAnnouncements().length }} hidden
+ {{
+ hiddenAnnouncements().length === 1 ? "announcement" : "announcements"
+ }}. To reset the hidden announcements, click the button below.
+
+
+
+}
diff --git a/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.ts b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.ts
new file mode 100644
index 0000000000..a35bbd342b
--- /dev/null
+++ b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.ts
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { Component, computed, inject } from '@angular/core';
+import { MatButton } from '@angular/material/button';
+import { AnnouncementWrapperService } from '../../../general/announcement/announcement.service';
+
+@Component({
+ selector: 'app-reset-hidden-announcements',
+ imports: [MatButton],
+ templateUrl: './reset-hidden-announcements.component.html',
+})
+export class ResetHiddenAnnouncementsComponent {
+ announcementWrapperService: AnnouncementWrapperService = inject(
+ AnnouncementWrapperService,
+ );
+
+ hiddenAnnouncements = computed(() => {
+ return this.announcementWrapperService
+ .dismissedAnnouncements()
+ .filter((announcement) => {
+ return this.announcementWrapperService
+ .announcements()
+ ?.find((visibleAnnouncement) => {
+ return visibleAnnouncement.id === announcement.id;
+ });
+ });
+ });
+}
diff --git a/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.stories.ts b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.stories.ts
new file mode 100644
index 0000000000..3cc71a5c48
--- /dev/null
+++ b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.stories.ts
@@ -0,0 +1,74 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
+import {
+ mockAnnouncement,
+ mockAnnouncementWrapperServiceProvider,
+} from '../../../../storybook/announcements';
+import { ResetHiddenAnnouncementsComponent } from './reset-hidden-announcements.component';
+
+const meta: Meta
= {
+ title: 'Settings Components/Users Profile/Reset Hidden Announcements',
+ component: ResetHiddenAnnouncementsComponent,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const NoDismissedAnnouncements: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ mockAnnouncementWrapperServiceProvider([mockAnnouncement], []),
+ ],
+ }),
+ ],
+};
+
+export const DismissedAnnouncementThatIsNoLongerVisible: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ mockAnnouncementWrapperServiceProvider(
+ [mockAnnouncement],
+ [{ id: -1, date: new Date() }],
+ ),
+ ],
+ }),
+ ],
+};
+
+export const OneDismissedAnnouncement: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ mockAnnouncementWrapperServiceProvider(
+ [mockAnnouncement],
+ [{ id: 1, date: new Date() }],
+ ),
+ ],
+ }),
+ ],
+};
+
+export const TwoDismissedAnnouncements: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ mockAnnouncementWrapperServiceProvider(
+ [mockAnnouncement, { ...mockAnnouncement, id: 2 }],
+ [
+ { id: 1, date: new Date() },
+ { id: 2, date: new Date() },
+ ],
+ ),
+ ],
+ }),
+ ],
+};
diff --git a/frontend/src/app/users/users-profile/users-profile.component.html b/frontend/src/app/users/users-profile/users-profile.component.html
index 12b5737131..9e567e06b3 100644
--- a/frontend/src/app/users/users-profile/users-profile.component.html
+++ b/frontend/src/app/users/users-profile/users-profile.component.html
@@ -34,6 +34,10 @@ Profile of {{ user?.name }}
}
}
+ @if (ownUserService.user?.id === (userWrapperService.user$ | async)?.id) {
+
+ }
+
@if (ownUserService.user?.role === "administrator") {
diff --git a/frontend/src/app/users/users-profile/users-profile.component.ts b/frontend/src/app/users/users-profile/users-profile.component.ts
index 12ba1a6869..55c4b7a7c9 100644
--- a/frontend/src/app/users/users-profile/users-profile.component.ts
+++ b/frontend/src/app/users/users-profile/users-profile.component.ts
@@ -10,6 +10,7 @@ import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.serv
import { UnifiedConfigWrapperService } from '../../services/unified-config-wrapper/unified-config-wrapper.service';
import { BetaTestingComponent } from './beta-testing/beta-testing.component';
import { CommonProjectsComponent } from './common-projects/common-projects.component';
+import { ResetHiddenAnnouncementsComponent } from './reset-hidden-announcements/reset-hidden-announcements.component';
import { UserInformationComponent } from './user-information/user-information.component';
import { UserWorkspacesComponent } from './user-workspaces/user-workspaces.component';
@@ -24,6 +25,7 @@ import { UserWorkspacesComponent } from './user-workspaces/user-workspaces.compo
UserWorkspacesComponent,
AsyncPipe,
BetaTestingComponent,
+ ResetHiddenAnnouncementsComponent,
],
})
export class UsersProfileComponent {
diff --git a/frontend/src/storybook/announcements.ts b/frontend/src/storybook/announcements.ts
new file mode 100644
index 0000000000..595b9c9e56
--- /dev/null
+++ b/frontend/src/storybook/announcements.ts
@@ -0,0 +1,69 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { computed, signal } from '@angular/core';
+import {
+ AnnouncementWrapperService,
+ DismissedAnnouncements,
+} from 'src/app/general/announcement/announcement.service';
+import { AnnouncementLevel, AnnouncementResponse } from 'src/app/openapi';
+
+export const mockAnnouncement: AnnouncementResponse = {
+ id: 1,
+ title: 'Title of the announcement',
+ message:
+ 'This is the message / content of an announcement. It can also contain simple HTML like links: ' +
+ "example.com",
+ level: AnnouncementLevel.Info,
+ dismissible: true,
+};
+
+class MockAnnouncementWrapperService
+ implements Partial
+{
+ public announcements = signal([]);
+ public dismissedAnnouncements = signal([]);
+ public visibleAnnouncements = computed(() => {
+ return this.announcements().filter(
+ (announcement) =>
+ !this.dismissedAnnouncements().find((dismissedAnnouncement) => {
+ return (
+ dismissedAnnouncement.id === announcement.id &&
+ announcement.dismissible
+ );
+ }),
+ );
+ });
+ dismissAnnouncement(announcementId: number): void {
+ this.dismissedAnnouncements.set([
+ ...this.dismissedAnnouncements(),
+ { id: announcementId, date: new Date() },
+ ]);
+ }
+
+ resetDismissedAnnouncements(): void {
+ this.dismissedAnnouncements.set([]);
+ }
+
+ constructor(
+ notices: AnnouncementResponse[],
+ dismissedAnnouncements: DismissedAnnouncements,
+ ) {
+ this.announcements.set(notices);
+ this.dismissedAnnouncements.set(dismissedAnnouncements);
+ }
+}
+
+export const mockAnnouncementWrapperServiceProvider = (
+ announcements: AnnouncementResponse[],
+ dismissedAnnouncements: DismissedAnnouncements,
+) => {
+ return {
+ provide: AnnouncementWrapperService,
+ useValue: new MockAnnouncementWrapperService(
+ announcements,
+ dismissedAnnouncements,
+ ),
+ };
+};
diff --git a/frontend/src/storybook/notices.ts b/frontend/src/storybook/notices.ts
deleted file mode 100644
index 26cd6c3fe8..0000000000
--- a/frontend/src/storybook/notices.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-import { BehaviorSubject } from 'rxjs';
-import { NoticeWrapperService } from 'src/app/general/notice/notice.service';
-import { NoticeLevel, NoticeResponse } from 'src/app/openapi';
-
-export const mockNotice: NoticeResponse = {
- id: 1,
- title: 'Title of the notice',
- message:
- 'This is the message / content of a notice. It can also contain simple HTML like links: ' +
- "example.com",
- level: NoticeLevel.Info,
-};
-
-class MockNoticeWrapperService implements Partial {
- private _notices = new BehaviorSubject(
- undefined,
- );
- public readonly notices$ = this._notices.asObservable();
-
- constructor(notices: NoticeResponse[]) {
- this._notices.next(notices);
- }
-}
-
-export const mockNoticeWrapperServiceProvider = (notices: NoticeResponse[]) => {
- return {
- provide: NoticeWrapperService,
- useValue: new MockNoticeWrapperService(notices),
- };
-};