From 09e1541698fcd70d52ea66d6cda0988150598e09 Mon Sep 17 00:00:00 2001 From: Tobias Messner Date: Wed, 4 Sep 2024 17:00:39 +0200 Subject: [PATCH] feat: Add a feedback form Add a feedback form that can be triggered after terminating a session, on a session card, in regular intervals, and in the footer. Feedback can optionally contain freeform text and include the users contact information. Feedback includes an anonymized version of any associated sessions. Closes #1742 --- backend/capellacollab/config/models.py | 25 ++ backend/capellacollab/feedback/__init__.py | 2 + backend/capellacollab/feedback/exceptions.py | 26 ++ backend/capellacollab/feedback/models.py | 48 ++++ backend/capellacollab/feedback/routes.py | 44 ++++ backend/capellacollab/feedback/util.py | 103 ++++++++ backend/capellacollab/routes.py | 2 + .../settings/configuration/models.py | 68 +++++ backend/pyproject.toml | 1 + .../tests/sessions/test_session_feedback.py | 150 +++++++++++ .../settings/test_global_configuration.py | 48 ++++ docs/docs/admin/configure-for-your-org.md | 66 ++++- frontend/src/app/app.component.ts | 15 +- .../app/general/footer/footer.component.html | 4 + .../app/general/footer/footer.component.ts | 2 + .../src/app/openapi/.openapi-generator/FILES | 23 +- frontend/src/app/openapi/api/api.ts | 4 +- .../src/app/openapi/api/feedback.service.ts | 240 ++++++++++++++++++ frontend/src/app/openapi/api/tools.service.ts | 46 ++-- .../app/openapi/model/anonymized-session.ts | 31 +++ .../model/feedback-anonymity-policy.ts | 21 ++ .../model/feedback-configuration-input.ts | 50 ++++ .../model/feedback-configuration-output.ts | 50 ++++ .../feedback-interval-configuration-input.ts | 24 ++ .../feedback-interval-configuration-output.ts | 24 ++ ...eedback-probability-configuration-input.ts | 24 ++ ...edback-probability-configuration-output.ts | 24 ++ .../src/app/openapi/model/feedback-rating.ts | 21 ++ frontend/src/app/openapi/model/feedback.ts | 34 +++ .../model/global-configuration-input.ts | 2 + .../model/global-configuration-output.ts | 2 + frontend/src/app/openapi/model/models.ts | 22 +- .../app/openapi/model/session-ports-input.ts | 20 ++ ...ssion-ports.ts => session-ports-output.ts} | 2 +- frontend/src/app/openapi/model/session.ts | 8 +- frontend/src/app/openapi/model/tool-input.ts | 25 ++ frontend/src/app/openapi/model/tool-model.ts | 4 +- .../openapi/model/{tool.ts => tool-output.ts} | 2 +- .../tool-session-connection-method-input.ts | 29 +++ ... tool-session-connection-method-output.ts} | 6 +- .../model/tool-version-with-tool-input.ts | 25 ++ ...ol.ts => tool-version-with-tool-output.ts} | 6 +- .../models/init-model/init-model.component.ts | 4 +- .../delete-session-dialog.component.html | 2 +- .../delete-session-dialog.component.ts | 2 +- .../feedback-dialog.component.html | 113 +++++++++ .../feedback-dialog.component.ts | 119 +++++++++ .../feedback-dialog.stories.ts | 76 ++++++ .../app/sessions/feedback/feedback.service.ts | 108 ++++++++ .../active-sessions.component.html | 27 +- .../active-sessions.component.ts | 17 +- .../create-readonly-session.component.ts | 10 +- .../create-persistent-session.component.ts | 6 +- ...create-readonly-model-options.component.ts | 4 +- ...reate-readonly-session-dialog.component.ts | 4 +- .../create-readonly-session-dialog.stories.ts | 4 +- .../create-session-history.component.ts | 6 +- .../configuration-settings.component.ts | 3 + .../tool-deletion-dialog.component.ts | 4 +- .../tool-details/tool-details.component.ts | 6 +- .../tool-nature/tool-nature.component.ts | 6 +- .../tool-version/tool-version.component.ts | 6 +- .../core/tools-settings/tool.service.ts | 18 +- frontend/src/storybook/tool.ts | 4 +- helm/config/backend.yaml | 9 + 65 files changed, 1827 insertions(+), 104 deletions(-) create mode 100644 backend/capellacollab/feedback/__init__.py create mode 100644 backend/capellacollab/feedback/exceptions.py create mode 100644 backend/capellacollab/feedback/models.py create mode 100644 backend/capellacollab/feedback/routes.py create mode 100644 backend/capellacollab/feedback/util.py create mode 100644 backend/tests/sessions/test_session_feedback.py create mode 100644 frontend/src/app/openapi/api/feedback.service.ts create mode 100644 frontend/src/app/openapi/model/anonymized-session.ts create mode 100644 frontend/src/app/openapi/model/feedback-anonymity-policy.ts create mode 100644 frontend/src/app/openapi/model/feedback-configuration-input.ts create mode 100644 frontend/src/app/openapi/model/feedback-configuration-output.ts create mode 100644 frontend/src/app/openapi/model/feedback-interval-configuration-input.ts create mode 100644 frontend/src/app/openapi/model/feedback-interval-configuration-output.ts create mode 100644 frontend/src/app/openapi/model/feedback-probability-configuration-input.ts create mode 100644 frontend/src/app/openapi/model/feedback-probability-configuration-output.ts create mode 100644 frontend/src/app/openapi/model/feedback-rating.ts create mode 100644 frontend/src/app/openapi/model/feedback.ts create mode 100644 frontend/src/app/openapi/model/session-ports-input.ts rename frontend/src/app/openapi/model/{session-ports.ts => session-ports-output.ts} (92%) create mode 100644 frontend/src/app/openapi/model/tool-input.ts rename frontend/src/app/openapi/model/{tool.ts => tool-output.ts} (95%) create mode 100644 frontend/src/app/openapi/model/tool-session-connection-method-input.ts rename frontend/src/app/openapi/model/{tool-session-connection-method.ts => tool-session-connection-method-output.ts} (85%) create mode 100644 frontend/src/app/openapi/model/tool-version-with-tool-input.ts rename frontend/src/app/openapi/model/{tool-version-with-tool.ts => tool-version-with-tool-output.ts} (84%) create mode 100644 frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.html create mode 100644 frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.ts create mode 100644 frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.stories.ts create mode 100644 frontend/src/app/sessions/feedback/feedback.service.ts diff --git a/backend/capellacollab/config/models.py b/backend/capellacollab/config/models.py index cc89b851ec..9315d39178 100644 --- a/backend/capellacollab/config/models.py +++ b/backend/capellacollab/config/models.py @@ -330,6 +330,30 @@ class PrometheusConfig(BaseConfig): ) +class SmtpConfig(BaseConfig): + enabled: bool = pydantic.Field( + default=False, + description="Whether to enable SMTP. Necessary for feedback.", + examples=[True, False], + ) + host: str = pydantic.Field( + description="The SMTP server host.", + examples=["smtp.example.com:587"], + pattern=r"^(.*):(\d+)$", + ) + user: str = pydantic.Field( + description="The SMTP server user.", examples=["username"] + ) + password: str = pydantic.Field( + description="The SMTP server password.", + examples=["password"], + ) + sender: str = pydantic.Field( + description="The sender email address.", + examples=["capella@example.com"], + ) + + class AppConfig(BaseConfig): docker: DockerConfig = DockerConfig() k8s: K8sConfig = K8sConfig(context="k3d-collab-cluster") @@ -342,3 +366,4 @@ class AppConfig(BaseConfig): logging: LoggingConfig = LoggingConfig() requests: RequestsConfig = RequestsConfig() pipelines: PipelineConfig = PipelineConfig() + smtp: t.Optional[SmtpConfig] = None diff --git a/backend/capellacollab/feedback/__init__.py b/backend/capellacollab/feedback/__init__.py new file mode 100644 index 0000000000..04412280d8 --- /dev/null +++ b/backend/capellacollab/feedback/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 diff --git a/backend/capellacollab/feedback/exceptions.py b/backend/capellacollab/feedback/exceptions.py new file mode 100644 index 0000000000..41d380c84b --- /dev/null +++ b/backend/capellacollab/feedback/exceptions.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from fastapi import status + +from capellacollab.core import exceptions as core_exceptions + + +class SmtpNotSetupError(core_exceptions.BaseError): + def __init__(self): + super().__init__( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + title="SMTP is not set up", + reason="SMTP must be set up to perform this action", + err_code="SMTP_NOT_SETUP", + ) + + +class FeedbackNotEnabledError(core_exceptions.BaseError): + def __init__(self): + super().__init__( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + title="Feedback is not set enabled", + reason="Feedback must be set up to perform this action", + err_code="FEEDBACK_NOT_SETUP", + ) diff --git a/backend/capellacollab/feedback/models.py b/backend/capellacollab/feedback/models.py new file mode 100644 index 0000000000..4029851346 --- /dev/null +++ b/backend/capellacollab/feedback/models.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import datetime +import enum +import typing as t + +import pydantic + +from capellacollab.core import models as core_models +from capellacollab.core import pydantic as core_pydantic +from capellacollab.sessions.models import SessionType +from capellacollab.tools import models as tools_models + + +class FeedbackRating(str, enum.Enum): + GOOD = "good" + OKAY = "okay" + BAD = "bad" + + +class AnonymizedSession(core_pydantic.BaseModel): + id: str + type: SessionType + created_at: datetime.datetime + + version: tools_models.ToolVersionWithTool + + state: str = pydantic.Field(default="UNKNOWN") + warnings: list[core_models.Message] = pydantic.Field(default=[]) + + connection_method_id: str + connection_method: tools_models.ToolSessionConnectionMethod | None = None + + +class Feedback(core_pydantic.BaseModel): + rating: FeedbackRating = pydantic.Field( + description="The rating of the feedback" + ) + feedback_text: t.Optional[str] = pydantic.Field( + description="The feedback text" + ) + share_contact: bool = pydantic.Field( + description="Whether the user wants to share their contact information" + ) + sessions: list[AnonymizedSession] = pydantic.Field( + description="The sessions the feedback is for" + ) diff --git a/backend/capellacollab/feedback/routes.py b/backend/capellacollab/feedback/routes.py new file mode 100644 index 0000000000..5c72bc4f6a --- /dev/null +++ b/backend/capellacollab/feedback/routes.py @@ -0,0 +1,44 @@ +# 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.feedback.models import Feedback +from capellacollab.feedback.util import is_feedback_allowed, send_email +from capellacollab.settings.configuration import core as config_core +from capellacollab.settings.configuration import ( + models as settings_config_models, +) +from capellacollab.settings.configuration.models import FeedbackConfiguration +from capellacollab.users import injectables as user_injectables +from capellacollab.users import models as users_models + +router = fastapi.APIRouter() + + +@router.get( + "/feedback", + response_model=FeedbackConfiguration, +) +def get_feedback(db: orm.Session = fastapi.Depends(database.get_db)): + cfg = config_core.get_config(db, "global") + assert isinstance(cfg, settings_config_models.GlobalConfiguration) + + return FeedbackConfiguration.model_validate(cfg.feedback.model_dump()) + + +@router.post("/feedback") +def submit_feedback( + feedback: Feedback, + background_tasks: fastapi.BackgroundTasks, + user: users_models.DatabaseUser = fastapi.Depends( + user_injectables.get_own_user + ), + db: orm.Session = fastapi.Depends(database.get_db), +): + is_feedback_allowed(db) + background_tasks.add_task(send_email, feedback, user, db) + return {"status": "sending"} diff --git a/backend/capellacollab/feedback/util.py b/backend/capellacollab/feedback/util.py new file mode 100644 index 0000000000..b6c8ae34b4 --- /dev/null +++ b/backend/capellacollab/feedback/util.py @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import smtplib +import typing as t +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +from sqlalchemy import orm + +from capellacollab.config import config +from capellacollab.feedback import exceptions +from capellacollab.feedback.models import AnonymizedSession, Feedback +from capellacollab.settings.configuration import core as config_core +from capellacollab.settings.configuration import ( + models as settings_config_models, +) +from capellacollab.settings.configuration.models import ( + FeedbackAnonymityPolicy, + FeedbackConfiguration, +) +from capellacollab.users import models as users_models + + +def format_session(session: AnonymizedSession): + return f"{session.version.tool.name} ({session.version.name})" + + +def is_feedback_allowed(db: orm.Session): + if not config.smtp or not config.smtp.enabled: + raise exceptions.SmtpNotSetupError() + + cfg = config_core.get_config(db, "global") + assert isinstance(cfg, settings_config_models.GlobalConfiguration) + feedback_config = FeedbackConfiguration.model_validate( + cfg.feedback.model_dump() + ) + if not feedback_config.enabled: + raise exceptions.FeedbackNotEnabledError() + + +def format_email( + feedback: Feedback, user: t.Optional[users_models.DatabaseUser] +): + if len(feedback.sessions) > 0: + return { + "subject": f"New Feedback {feedback.rating.value.capitalize()} for {', '.join([format_session(session) for session in feedback.sessions])}", + "message": f"""Rating: {feedback.rating.value} +Text: {feedback.feedback_text or "No feedback text provided"} +User: {f'{user.name} ({user.email})' if user else "Anonymous User"} + +{'\n'.join([session.model_dump_json(indent=2) for session in feedback.sessions])}""", + } + else: + return { + "subject": f"New General Feedback {feedback.rating.value.capitalize()}", + "message": f"""Rating: {feedback.rating.value} +Text: {feedback.feedback_text or "No feedback text provided"} +User: {f'{user.name} ({user.email})' if user else "Anonymous User"}""", + } + + +def send_email( + feedback: Feedback, user: users_models.DatabaseUser, db: orm.Session +): + is_feedback_allowed(db) + assert config.smtp, "SMTP configuration is not set up" + + cfg = config_core.get_config(db, "global") + assert isinstance(cfg, settings_config_models.GlobalConfiguration) + feedback_config = FeedbackConfiguration.model_validate( + cfg.feedback.model_dump() + ) + + match feedback_config.anonymity_policy: + case FeedbackAnonymityPolicy.FORCE_ANONYMOUS: + is_anonymous = True + case FeedbackAnonymityPolicy.FORCE_IDENTIFIED: + is_anonymous = False + case _: + is_anonymous = not feedback + + msg = MIMEMultipart() + msg["From"] = config.smtp.sender + msg["To"] = feedback_config.receiver + email_text = format_email(feedback, None if is_anonymous else user) + + msg["Subject"] = email_text["subject"] + msg.attach(MIMEText(email_text["message"], "plain")) + + mailserver = smtplib.SMTP( + config.smtp.host.split(":")[0], int(config.smtp.host.split(":")[1]) + ) + mailserver.ehlo() + mailserver.starttls() + mailserver.ehlo() + mailserver.login(config.smtp.user, config.smtp.password) + + mailserver.sendmail( + config.smtp.sender, feedback_config.receiver, msg.as_string() + ) + + mailserver.quit() diff --git a/backend/capellacollab/routes.py b/backend/capellacollab/routes.py index 656e7894f2..5c3d6fdce3 100644 --- a/backend/capellacollab/routes.py +++ b/backend/capellacollab/routes.py @@ -9,6 +9,7 @@ from capellacollab.core import responses as auth_responses 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.metadata import routes as core_metadata from capellacollab.navbar import routes as navbar_routes @@ -31,6 +32,7 @@ ) router.include_router(core_metadata.router, tags=["Metadata"]) router.include_router(navbar_routes.router, tags=["Navbar"]) +router.include_router(feedback_routes.router, tags=["Feedback"]) router.include_router( sessions_routes.router, prefix="/sessions", diff --git a/backend/capellacollab/settings/configuration/models.py b/backend/capellacollab/settings/configuration/models.py index 36fc6038cb..ff1aee91f0 100644 --- a/backend/capellacollab/settings/configuration/models.py +++ b/backend/capellacollab/settings/configuration/models.py @@ -89,6 +89,70 @@ class NavbarConfiguration(core_pydantic.BaseModelStrict): ) +class FeedbackAnonymityPolicy(str, enum.Enum): + FORCE_ANONYMOUS = "force_anonymous" + FORCE_IDENTIFIED = "force_identified" + ASK_USER = "ask_user" + + +class FeedbackIntervalConfiguration(core_pydantic.BaseModelStrict): + enabled: bool = pydantic.Field( + default=True, + description="Whether the feedback interval is enabled.", + ) + hours_between_prompt: int = pydantic.Field( + default=168, + description="The interval in hours between feedback requests.", + ge=0, + ) + + +class FeedbackProbabilityConfiguration(core_pydantic.BaseModelStrict): + enabled: bool = pydantic.Field( + default=True, + description="Whether the feedback probability is enabled.", + ) + percentage: int = pydantic.Field( + default=100, + description="The percentage of users that will be asked for feedback.", + ge=0, + le=100, + ) + + +class FeedbackConfiguration(core_pydantic.BaseModelStrict): + enabled: bool = pydantic.Field( + default=False, + description="Enable or disable the feedback system. If enabled, SMTP configuration is required.", + ) + after_session: FeedbackProbabilityConfiguration = pydantic.Field( + default_factory=FeedbackProbabilityConfiguration, + description="If a feedback form is shown after terminating a session.", + ) + on_footer: bool = pydantic.Field( + default=True, + description="Should a general feedback button be shown.", + ) + on_session_card: bool = pydantic.Field( + default=True, + description="Should a feedback button be shown on the session cards.", + ) + interval: FeedbackIntervalConfiguration = pydantic.Field( + default_factory=FeedbackIntervalConfiguration, + description="Request feedback at regular intervals.", + ) + receiver: pydantic.EmailStr = pydantic.Field( + default="test@example.com", + description="Email address to send feedback to.", + examples=["test@example.com"], + ) + anonymity_policy: FeedbackAnonymityPolicy = pydantic.Field( + default=FeedbackAnonymityPolicy.ASK_USER, + description="If feedback should be anonymous or identified.", + examples=["force_anonymous", "force_identified", "ask_user"], + ) + + class ConfigurationBase(core_pydantic.BaseModelStrict, abc.ABC): """ Base class for configuration models. Can be used to define new configurations @@ -111,6 +175,10 @@ class GlobalConfiguration(ConfigurationBase): default_factory=NavbarConfiguration ) + feedback: FeedbackConfiguration = pydantic.Field( + default_factory=FeedbackConfiguration + ) + # All subclasses of ConfigurationBase are automatically registered using this dict. NAME_TO_MODEL_TYPE_MAPPING: dict[str, t.Type[ConfigurationBase]] = { diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 730a34f81f..5626e5c049 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "alembic==1.13.2", "appdirs", "cachetools", + "email-validator", "fastapi>=0.101.0,<0.112.4", "kubernetes", "psycopg2-binary>2.9.7", diff --git a/backend/tests/sessions/test_session_feedback.py b/backend/tests/sessions/test_session_feedback.py new file mode 100644 index 0000000000..acc83a3dc0 --- /dev/null +++ b/backend/tests/sessions/test_session_feedback.py @@ -0,0 +1,150 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from unittest import mock + +import pytest +from fastapi import testclient +from sqlalchemy import orm + +import capellacollab.feedback.util +from capellacollab.config import models as config_models +from capellacollab.settings.configuration import crud as configuration_crud + + +@pytest.fixture(name="smtp_config_set") +def mock_smtp_config_set(monkeypatch: pytest.MonkeyPatch): + mocked_config = config_models.AppConfig( + smtp=config_models.SmtpConfig( + enabled=True, + host="smtp.example.com:587", + user="smtp_user", + password="smtp_password", + sender="capella@example.com", + ) + ) + monkeypatch.setattr(capellacollab.feedback.util, "config", mocked_config) + + +@pytest.fixture(name="smtp_config_not_set") +def mock_smtp_config_not_set(monkeypatch: pytest.MonkeyPatch): + mocked_config = config_models.AppConfig(smtp=None) + monkeypatch.setattr(capellacollab.feedback.util, "config", mocked_config) + + +@pytest.fixture(name="feedback_enabled") +def mock_feedback_enabled(db: orm.Session): + configuration_crud.create_configuration( + db, "global", {"feedback": {"enabled": True}} + ) + + +@pytest.fixture(name="feedback_disabled") +def mock_feedback_disabled(db: orm.Session): + configuration_crud.create_configuration( + db, "global", {"feedback": {"enabled": False}} + ) + + +@pytest.mark.usefixtures("user") +@pytest.mark.usefixtures("smtp_config_set") +@pytest.mark.usefixtures("feedback_enabled") +def test_send_feedback( + client: testclient.TestClient, +): + with mock.patch( + "capellacollab.feedback.routes.send_email" + ) as send_email_mock: + response = client.post( + "/api/v1/feedback", + json={ + "rating": "good", + "share_contact": False, + "sessions": [], + "feedback_text": None, + }, + ) + + assert response.status_code == 200 + assert response.json() == {"status": "sending"} + + send_email_mock.assert_called_once() + + +@pytest.mark.usefixtures("user") +@pytest.mark.usefixtures("smtp_config_set") +@pytest.mark.usefixtures("feedback_enabled") +def test_send_feedback_with_contact( + client: testclient.TestClient, +): + with mock.patch( + "capellacollab.feedback.routes.send_email" + ) as send_email_mock: + response = client.post( + "/api/v1/feedback", + json={ + "rating": "good", + "share_contact": True, + "sessions": [], + "feedback_text": None, + }, + ) + + assert response.status_code == 200 + assert response.json() == {"status": "sending"} + + send_email_mock.assert_called_once() + + +@pytest.mark.usefixtures("user") +@pytest.mark.usefixtures("smtp_config_set") +@pytest.mark.usefixtures("feedback_disabled") +def test_send_feedback_fail_disabled( + client: testclient.TestClient, +): + response = client.post( + "/api/v1/feedback", + json={ + "rating": "good", + "share_contact": False, + "sessions": [], + "feedback_text": None, + }, + ) + assert response.status_code == 500 + + +@pytest.mark.usefixtures("user") +@pytest.mark.usefixtures("smtp_config_not_set") +@pytest.mark.usefixtures("feedback_enabled") +def test_send_feedback_fail_missing_smtp( + client: testclient.TestClient, +): + response = client.post( + "/api/v1/feedback", + json={ + "rating": "good", + "share_contact": False, + "sessions": [], + "feedback_text": None, + }, + ) + assert response.status_code == 500 + + +@pytest.mark.usefixtures("user") +@pytest.mark.usefixtures("smtp_config_not_set") +@pytest.mark.usefixtures("feedback_disabled") +def test_send_feedback_fail_disabled_and_missing_smtp( + client: testclient.TestClient, +): + response = client.post( + "/api/v1/feedback", + json={ + "rating": "good", + "share_contact": False, + "sessions": [], + "feedback_text": None, + }, + ) + assert response.status_code == 500 diff --git a/backend/tests/settings/test_global_configuration.py b/backend/tests/settings/test_global_configuration.py index 03437295d0..bf3b1ffc0c 100644 --- a/backend/tests/settings/test_global_configuration.py +++ b/backend/tests/settings/test_global_configuration.py @@ -193,3 +193,51 @@ def get_mock_own_user(): "href": "https://example.com", "role": "user", } + + +def test_feedback_is_updated( + client: testclient.TestClient, + db: orm.Session, + executor_name: str, +): + admin = users_crud.create_user( + db, executor_name, executor_name, None, users_models.Role.ADMIN + ) + + def get_mock_own_user(): + return admin + + app.dependency_overrides[users_injectables.get_own_user] = ( + get_mock_own_user + ) + + response = client.put( + "/api/v1/settings/configurations/global", + json={ + "feedback": { + "enabled": True, + "after_session": {"enabled": True, "percentage": 100}, + "on_footer": True, + "on_session_card": True, + "interval": {"enabled": True, "hours_between_prompt": 24}, + "receiver": "test@example.com", + "anonymity_policy": "ask_user", + } + }, + ) + + assert response.status_code == 200 + + del app.dependency_overrides[users_injectables.get_own_user] + + response = client.get("/api/v1/feedback") + assert response.status_code == 200 + assert response.json() == { + "enabled": True, + "after_session": {"enabled": True, "percentage": 100}, + "on_footer": True, + "on_session_card": True, + "interval": {"enabled": True, "hours_between_prompt": 24}, + "receiver": "test@example.com", + "anonymity_policy": "ask_user", + } diff --git a/docs/docs/admin/configure-for-your-org.md b/docs/docs/admin/configure-for-your-org.md index 165898bc87..45e674abba 100644 --- a/docs/docs/admin/configure-for-your-org.md +++ b/docs/docs/admin/configure-for-your-org.md @@ -6,15 +6,17 @@ # Configure for your Organization When running the Collaboration Manager in production, you may want to provide -information about the team responsible for it, as well as an imprint and -privacy policy. +information about the team responsible for it. You can set this information from the configuration page in the admin interface. Navigate to _Settings_, then _Configuration_, then edit the file to your liking. -Here, you can also edit the links in the navigation bar if you are not using -the default monitoring services. +## About your Organization + +You can set URLs to your organization's privacy policy and imprint. These are +shown in the footer. The provider field should be used for the name of the team +responsible for the Collaboration Manager. ```yaml metadata: @@ -23,6 +25,15 @@ metadata: provider: Systems Engineering Toolchain team authentication_provider: OAuth2 environment: '-' +``` + +## Navigation Bar + +You can edit the links in the navigation bar. This can be useful if you want to +link to external resources or if you are not using the default monitoring +setup. + +```yaml navbar: external_links: - name: Grafana @@ -51,3 +62,50 @@ The `role` field and can be one of `user` or `administrator`. While this will hide the link from users without the appropriate role, it is not a security feature, and you should make sure that the linked service enforces the necessary access controls. + +## Feedback + +!!! info "Configure SMTP server for feedback" + + For feedback to be sent, you need to configure an SMTP server in the + `values.yaml` of the Helm chart. Have a look at the `alerting.email` + configuration. + +Capella Collaboration Manager can prompt users for feedback. This can be useful +for learning about any potential issues users may be facing. + +There are several different types of feedback prompt: + +- After a session: Prompt the user for feedback after they have manually + terminated a session. You can reduce the percentage of users that are + prompted by changing the `percentage` field. +- On the session card: Show a feedback button on the session card. +- In the footer: Show a feedback button in the footer. +- Interval: Prompt the user for feedback after a certain number of hours have + passed since the last prompt. + +```yaml +feedback: + enabled: true + after_session: + enabled: true + percentage: 25 + on_footer: true + on_session_card: true + interval: + enabled: true + hours_between_prompt: 168 + receiver: feedback@example.com + anonymity_policy: ask_user +``` + +Prompts that are associated with a session automatically include anonymized +metadata about the session. + +By default, users can choose to share their contact information when providing +feedback. This can be disabled or made mandatory by changing the +`anonymity_policy` field from `ask_user` to `force_anonymous` or +`force_identified`, respectively. + +Feedback will be sent by email to the address specified in the `receiver` +field. diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 2888a4f2d6..cd917a34a4 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import { NgIf, NgClass, AsyncPipe } from '@angular/common'; -import { AfterViewInit, Component, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; import { MatSidenav, MatDrawerContainer, @@ -18,6 +18,7 @@ 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 { FeedbackService } from './sessions/feedback/feedback.service'; import { FullscreenService } from './sessions/service/fullscreen.service'; @Component({ @@ -39,17 +40,27 @@ import { FullscreenService } from './sessions/service/fullscreen.service'; AsyncPipe, ], }) -export class AppComponent implements AfterViewInit { +export class AppComponent implements OnInit, AfterViewInit { constructor( public pageLayoutService: PageLayoutService, public fullscreenService: FullscreenService, private navBarService: NavBarService, + private feedbackService: FeedbackService, ) { slugify.extend({ '.': '-' }); } @ViewChild('sidenav') private sidenav?: MatSidenav; + async ngOnInit() { + this.feedbackService.loadFeedbackConfig().subscribe(() => { + if (this.feedbackService.shouldShowIntervalPrompt()) { + this.feedbackService.showDialog([]); + this.feedbackService.saveFeedbackPromptDate(); + } + }); + } + ngAfterViewInit(): void { this.navBarService.sidenav = this.sidenav; } diff --git a/frontend/src/app/general/footer/footer.component.html b/frontend/src/app/general/footer/footer.component.html index 346efbc00c..e2fbd90ada 100644 --- a/frontend/src/app/general/footer/footer.component.html +++ b/frontend/src/app/general/footer/footer.component.html @@ -42,6 +42,10 @@ >Contribute on GitHub open_in_new +   + @if (feedbackService.showOnFooter$ | async) { + + } diff --git a/frontend/src/app/general/footer/footer.component.ts b/frontend/src/app/general/footer/footer.component.ts index 0bebcb8f01..c3af01c382 100644 --- a/frontend/src/app/general/footer/footer.component.ts +++ b/frontend/src/app/general/footer/footer.component.ts @@ -8,6 +8,7 @@ import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MetadataService } from 'src/app/general/metadata/metadata.service'; import { VersionComponent } from 'src/app/general/metadata/version/version.component'; +import { FeedbackService } from '../../sessions/feedback/feedback.service'; @Component({ selector: 'app-footer', @@ -19,5 +20,6 @@ export class FooterComponent { constructor( public dialog: MatDialog, public metadataService: MetadataService, + public feedbackService: FeedbackService, ) {} } diff --git a/frontend/src/app/openapi/.openapi-generator/FILES b/frontend/src/app/openapi/.openapi-generator/FILES index bf02952da7..a6d2b463d5 100644 --- a/frontend/src/app/openapi/.openapi-generator/FILES +++ b/frontend/src/app/openapi/.openapi-generator/FILES @@ -3,6 +3,7 @@ api/api.ts api/authentication.service.ts api/configuration.service.ts api/events.service.ts +api/feedback.service.ts api/health.service.ts api/integrations-pure-variants.service.ts api/metadata.service.ts @@ -28,6 +29,7 @@ api/users.service.ts configuration.ts encoder.ts index.ts +model/anonymized-session.ts model/authorization-response.ts model/backup-pipeline-run.ts model/backup.ts @@ -52,6 +54,15 @@ model/diagram-metadata.ts model/environment-value.ts model/environment-value1.ts model/event-type.ts +model/feedback-anonymity-policy.ts +model/feedback-configuration-input.ts +model/feedback-configuration-output.ts +model/feedback-interval-configuration-input.ts +model/feedback-interval-configuration-output.ts +model/feedback-probability-configuration-input.ts +model/feedback-probability-configuration-output.ts +model/feedback-rating.ts +model/feedback.ts model/file-tree.ts model/file-type.ts model/get-revision-model.ts @@ -130,7 +141,8 @@ model/role.ts model/session-connection-information.ts model/session-monitoring-input.ts model/session-monitoring-output.ts -model/session-ports.ts +model/session-ports-input.ts +model/session-ports-output.ts model/session-provisioning-request.ts model/session-sharing.ts model/session-tool-configuration-input.ts @@ -148,6 +160,7 @@ model/t4-c-repository.ts model/token-request.ts model/tool-backup-configuration-input.ts model/tool-backup-configuration-output.ts +model/tool-input.ts model/tool-integrations-input.ts model/tool-integrations-output.ts model/tool-model-provisioning-input.ts @@ -155,11 +168,13 @@ model/tool-model-provisioning-output.ts model/tool-model-restrictions.ts model/tool-model.ts model/tool-nature.ts +model/tool-output.ts model/tool-session-configuration-input.ts model/tool-session-configuration-output.ts model/tool-session-connection-input-methods-inner.ts model/tool-session-connection-input.ts -model/tool-session-connection-method.ts +model/tool-session-connection-method-input.ts +model/tool-session-connection-method-output.ts model/tool-session-connection-output-methods-inner.ts model/tool-session-connection-output.ts model/tool-session-environment-input.ts @@ -169,9 +184,9 @@ model/tool-session-sharing-configuration-input.ts model/tool-session-sharing-configuration-output.ts model/tool-version-configuration-input.ts model/tool-version-configuration-output.ts -model/tool-version-with-tool.ts +model/tool-version-with-tool-input.ts +model/tool-version-with-tool-output.ts model/tool-version.ts -model/tool.ts model/toolmodel-status.ts model/user-metadata.ts model/user-token-with-password.ts diff --git a/frontend/src/app/openapi/api/api.ts b/frontend/src/app/openapi/api/api.ts index f498c004f8..0b15f2169d 100644 --- a/frontend/src/app/openapi/api/api.ts +++ b/frontend/src/app/openapi/api/api.ts @@ -15,6 +15,8 @@ export * from './configuration.service'; import { ConfigurationService } from './configuration.service'; export * from './events.service'; import { EventsService } from './events.service'; +export * from './feedback.service'; +import { FeedbackService } from './feedback.service'; export * from './health.service'; import { HealthService } from './health.service'; export * from './integrations-pure-variants.service'; @@ -59,4 +61,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, HealthService, IntegrationsPureVariantsService, MetadataService, NavbarService, NoticesService, ProjectsService, ProjectsEventsService, ProjectsModelsService, ProjectsModelsBackupsService, ProjectsModelsDiagramsService, ProjectsModelsGitService, ProjectsModelsModelComplexityBadgeService, ProjectsModelsRestrictionsService, ProjectsModelsT4CService, SessionsService, SettingsModelsourcesGitService, SettingsModelsourcesT4CService, ToolsService, UsersService, UsersSessionsService, UsersTokenService, UsersWorkspacesService]; +export const APIS = [AuthenticationService, ConfigurationService, EventsService, FeedbackService, HealthService, IntegrationsPureVariantsService, MetadataService, NavbarService, NoticesService, ProjectsService, ProjectsEventsService, ProjectsModelsService, ProjectsModelsBackupsService, ProjectsModelsDiagramsService, ProjectsModelsGitService, ProjectsModelsModelComplexityBadgeService, ProjectsModelsRestrictionsService, ProjectsModelsT4CService, SessionsService, SettingsModelsourcesGitService, SettingsModelsourcesT4CService, ToolsService, UsersService, UsersSessionsService, UsersTokenService, UsersWorkspacesService]; diff --git a/frontend/src/app/openapi/api/feedback.service.ts b/frontend/src/app/openapi/api/feedback.service.ts new file mode 100644 index 0000000000..18f002da82 --- /dev/null +++ b/frontend/src/app/openapi/api/feedback.service.ts @@ -0,0 +1,240 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { Feedback } from '../model/feedback'; +// @ts-ignore +import { FeedbackConfigurationOutput } from '../model/feedback-configuration-output'; +// @ts-ignore +import { HTTPValidationError } from '../model/http-validation-error'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; + + + +@Injectable({ + providedIn: 'root' +}) +export class FeedbackService { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * Get Feedback + * @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 getFeedback(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getFeedback(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getFeedback(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getFeedback(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/feedback`; + 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 + } + ); + } + + /** + * Submit Feedback + * @param feedback + * @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 submitFeedback(feedback: Feedback, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public submitFeedback(feedback: Feedback, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public submitFeedback(feedback: Feedback, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public submitFeedback(feedback: Feedback, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (feedback === null || feedback === undefined) { + throw new Error('Required parameter feedback was null or undefined when calling submitFeedback.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // 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 + 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; + } + + + // 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')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/v1/feedback`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: feedback, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/frontend/src/app/openapi/api/tools.service.ts b/frontend/src/app/openapi/api/tools.service.ts index 40711b7bd3..b1be2acad9 100644 --- a/frontend/src/app/openapi/api/tools.service.ts +++ b/frontend/src/app/openapi/api/tools.service.ts @@ -33,13 +33,13 @@ import { CreateToolVersionOutput } from '../model/create-tool-version-output'; // @ts-ignore import { HTTPValidationError } from '../model/http-validation-error'; // @ts-ignore -import { Tool } from '../model/tool'; -// @ts-ignore import { ToolNature } from '../model/tool-nature'; // @ts-ignore +import { ToolOutput } from '../model/tool-output'; +// @ts-ignore import { ToolVersion } from '../model/tool-version'; // @ts-ignore -import { ToolVersionWithTool } from '../model/tool-version-with-tool'; +import { ToolVersionWithToolOutput } from '../model/tool-version-with-tool-output'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; @@ -119,9 +119,9 @@ export class ToolsService { * @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 createTool(createToolInput: CreateToolInput, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public createTool(createToolInput: CreateToolInput, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public createTool(createToolInput: CreateToolInput, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createTool(createToolInput: CreateToolInput, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createTool(createToolInput: CreateToolInput, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createTool(createToolInput: CreateToolInput, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; public createTool(createToolInput: CreateToolInput, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (createToolInput === null || createToolInput === undefined) { throw new Error('Required parameter createToolInput was null or undefined when calling createTool.'); @@ -180,7 +180,7 @@ export class ToolsService { } let localVarPath = `/api/v1/tools`; - return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, body: createToolInput, @@ -800,9 +800,9 @@ export class ToolsService { * @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 getToolById(toolId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public getToolById(toolId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getToolById(toolId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getToolById(toolId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getToolById(toolId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getToolById(toolId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; public getToolById(toolId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (toolId === null || toolId === undefined) { throw new Error('Required parameter toolId was null or undefined when calling getToolById.'); @@ -852,7 +852,7 @@ export class ToolsService { } let localVarPath = `/api/v1/tools/${this.configuration.encodeParam({name: "toolId", value: toolId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; - return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, @@ -1162,9 +1162,9 @@ export class ToolsService { * @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 getTools(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getTools(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getTools(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getTools(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getTools(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getTools(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; public getTools(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { let localVarHeaders = this.defaultHeaders; @@ -1211,7 +1211,7 @@ export class ToolsService { } let localVarPath = `/api/v1/tools`; - return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, @@ -1229,9 +1229,9 @@ export class ToolsService { * @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 getVersionsForAllTools(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getVersionsForAllTools(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getVersionsForAllTools(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getVersionsForAllTools(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getVersionsForAllTools(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getVersionsForAllTools(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; public getVersionsForAllTools(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { let localVarHeaders = this.defaultHeaders; @@ -1278,7 +1278,7 @@ export class ToolsService { } let localVarPath = `/api/v1/tools/*/versions`; - return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, @@ -1298,9 +1298,9 @@ export class ToolsService { * @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 updateTool(toolId: number, createToolInput: CreateToolInput, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public updateTool(toolId: number, createToolInput: CreateToolInput, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public updateTool(toolId: number, createToolInput: CreateToolInput, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateTool(toolId: number, createToolInput: CreateToolInput, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateTool(toolId: number, createToolInput: CreateToolInput, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateTool(toolId: number, createToolInput: CreateToolInput, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; public updateTool(toolId: number, createToolInput: CreateToolInput, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (toolId === null || toolId === undefined) { throw new Error('Required parameter toolId was null or undefined when calling updateTool.'); @@ -1362,7 +1362,7 @@ export class ToolsService { } let localVarPath = `/api/v1/tools/${this.configuration.encodeParam({name: "toolId", value: toolId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; - return this.httpClient.request('put', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request('put', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, body: createToolInput, diff --git a/frontend/src/app/openapi/model/anonymized-session.ts b/frontend/src/app/openapi/model/anonymized-session.ts new file mode 100644 index 0000000000..f3b5c04e54 --- /dev/null +++ b/frontend/src/app/openapi/model/anonymized-session.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { SessionType } from './session-type'; +import { Message } from './message'; +import { ToolSessionConnectionMethodInput } from './tool-session-connection-method-input'; +import { ToolVersionWithToolInput } from './tool-version-with-tool-input'; + + +export interface AnonymizedSession { + id: string; + type: SessionType; + created_at: string; + version: ToolVersionWithToolInput; + state?: string; + warnings?: Array; + connection_method_id: string; + connection_method?: ToolSessionConnectionMethodInput | null; +} +export namespace AnonymizedSession { +} + + diff --git a/frontend/src/app/openapi/model/feedback-anonymity-policy.ts b/frontend/src/app/openapi/model/feedback-anonymity-policy.ts new file mode 100644 index 0000000000..0a50781fae --- /dev/null +++ b/frontend/src/app/openapi/model/feedback-anonymity-policy.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export type FeedbackAnonymityPolicy = 'force_anonymous' | 'force_identified' | 'ask_user'; + +export const FeedbackAnonymityPolicy = { + ForceAnonymous: 'force_anonymous' as FeedbackAnonymityPolicy, + ForceIdentified: 'force_identified' as FeedbackAnonymityPolicy, + AskUser: 'ask_user' as FeedbackAnonymityPolicy +}; + diff --git a/frontend/src/app/openapi/model/feedback-configuration-input.ts b/frontend/src/app/openapi/model/feedback-configuration-input.ts new file mode 100644 index 0000000000..a314129032 --- /dev/null +++ b/frontend/src/app/openapi/model/feedback-configuration-input.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { FeedbackAnonymityPolicy } from './feedback-anonymity-policy'; +import { FeedbackProbabilityConfigurationInput } from './feedback-probability-configuration-input'; +import { FeedbackIntervalConfigurationInput } from './feedback-interval-configuration-input'; + + +export interface FeedbackConfigurationInput { + /** + * Enable or disable the feedback system. If enabled, SMTP configuration is required. + */ + enabled?: boolean; + /** + * If a feedback form is shown after terminating a session. + */ + after_session?: FeedbackProbabilityConfigurationInput; + /** + * Should a general feedback button be shown. + */ + on_footer?: boolean; + /** + * Should a feedback button be shown on the session cards. + */ + on_session_card?: boolean; + /** + * Request feedback at regular intervals. + */ + interval?: FeedbackIntervalConfigurationInput; + /** + * Email address to send feedback to. + */ + receiver?: string; + /** + * If feedback should be anonymous or identified. + */ + anonymity_policy?: FeedbackAnonymityPolicy; +} +export namespace FeedbackConfigurationInput { +} + + diff --git a/frontend/src/app/openapi/model/feedback-configuration-output.ts b/frontend/src/app/openapi/model/feedback-configuration-output.ts new file mode 100644 index 0000000000..4c7f8af769 --- /dev/null +++ b/frontend/src/app/openapi/model/feedback-configuration-output.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { FeedbackAnonymityPolicy } from './feedback-anonymity-policy'; +import { FeedbackIntervalConfigurationOutput } from './feedback-interval-configuration-output'; +import { FeedbackProbabilityConfigurationOutput } from './feedback-probability-configuration-output'; + + +export interface FeedbackConfigurationOutput { + /** + * Enable or disable the feedback system. If enabled, SMTP configuration is required. + */ + enabled: boolean; + /** + * If a feedback form is shown after terminating a session. + */ + after_session: FeedbackProbabilityConfigurationOutput; + /** + * Should a general feedback button be shown. + */ + on_footer: boolean; + /** + * Should a feedback button be shown on the session cards. + */ + on_session_card: boolean; + /** + * Request feedback at regular intervals. + */ + interval: FeedbackIntervalConfigurationOutput; + /** + * Email address to send feedback to. + */ + receiver: string; + /** + * If feedback should be anonymous or identified. + */ + anonymity_policy: FeedbackAnonymityPolicy; +} +export namespace FeedbackConfigurationOutput { +} + + diff --git a/frontend/src/app/openapi/model/feedback-interval-configuration-input.ts b/frontend/src/app/openapi/model/feedback-interval-configuration-input.ts new file mode 100644 index 0000000000..9e543140e7 --- /dev/null +++ b/frontend/src/app/openapi/model/feedback-interval-configuration-input.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export interface FeedbackIntervalConfigurationInput { + /** + * Whether the feedback interval is enabled. + */ + enabled?: boolean; + /** + * The interval in hours between feedback requests. + */ + hours_between_prompt?: number; +} + diff --git a/frontend/src/app/openapi/model/feedback-interval-configuration-output.ts b/frontend/src/app/openapi/model/feedback-interval-configuration-output.ts new file mode 100644 index 0000000000..f3a53f26cb --- /dev/null +++ b/frontend/src/app/openapi/model/feedback-interval-configuration-output.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export interface FeedbackIntervalConfigurationOutput { + /** + * Whether the feedback interval is enabled. + */ + enabled: boolean; + /** + * The interval in hours between feedback requests. + */ + hours_between_prompt: number; +} + diff --git a/frontend/src/app/openapi/model/feedback-probability-configuration-input.ts b/frontend/src/app/openapi/model/feedback-probability-configuration-input.ts new file mode 100644 index 0000000000..0f6c695202 --- /dev/null +++ b/frontend/src/app/openapi/model/feedback-probability-configuration-input.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export interface FeedbackProbabilityConfigurationInput { + /** + * Whether the feedback probability is enabled. + */ + enabled?: boolean; + /** + * The percentage of users that will be asked for feedback. + */ + percentage?: number; +} + diff --git a/frontend/src/app/openapi/model/feedback-probability-configuration-output.ts b/frontend/src/app/openapi/model/feedback-probability-configuration-output.ts new file mode 100644 index 0000000000..9a79c3bdb7 --- /dev/null +++ b/frontend/src/app/openapi/model/feedback-probability-configuration-output.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export interface FeedbackProbabilityConfigurationOutput { + /** + * Whether the feedback probability is enabled. + */ + enabled: boolean; + /** + * The percentage of users that will be asked for feedback. + */ + percentage: number; +} + diff --git a/frontend/src/app/openapi/model/feedback-rating.ts b/frontend/src/app/openapi/model/feedback-rating.ts new file mode 100644 index 0000000000..16b48081df --- /dev/null +++ b/frontend/src/app/openapi/model/feedback-rating.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export type FeedbackRating = 'good' | 'okay' | 'bad'; + +export const FeedbackRating = { + Good: 'good' as FeedbackRating, + Okay: 'okay' as FeedbackRating, + Bad: 'bad' as FeedbackRating +}; + diff --git a/frontend/src/app/openapi/model/feedback.ts b/frontend/src/app/openapi/model/feedback.ts new file mode 100644 index 0000000000..8d15689d6f --- /dev/null +++ b/frontend/src/app/openapi/model/feedback.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { FeedbackRating } from './feedback-rating'; +import { AnonymizedSession } from './anonymized-session'; + + +export interface Feedback { + /** + * The rating of the feedback + */ + rating: FeedbackRating; + feedback_text: string | null; + /** + * Whether the user wants to share their contact information + */ + share_contact: boolean; + /** + * The sessions the feedback is for + */ + sessions: Array; +} +export namespace Feedback { +} + + diff --git a/frontend/src/app/openapi/model/global-configuration-input.ts b/frontend/src/app/openapi/model/global-configuration-input.ts index 70c4954a1f..6c568c5958 100644 --- a/frontend/src/app/openapi/model/global-configuration-input.ts +++ b/frontend/src/app/openapi/model/global-configuration-input.ts @@ -11,6 +11,7 @@ import { NavbarConfigurationInput } from './navbar-configuration-input'; import { MetadataConfigurationInput } from './metadata-configuration-input'; +import { FeedbackConfigurationInput } from './feedback-configuration-input'; /** @@ -19,5 +20,6 @@ import { MetadataConfigurationInput } from './metadata-configuration-input'; export interface GlobalConfigurationInput { metadata?: MetadataConfigurationInput; navbar?: NavbarConfigurationInput; + feedback?: FeedbackConfigurationInput; } diff --git a/frontend/src/app/openapi/model/global-configuration-output.ts b/frontend/src/app/openapi/model/global-configuration-output.ts index cab85de70b..c52b7fa677 100644 --- a/frontend/src/app/openapi/model/global-configuration-output.ts +++ b/frontend/src/app/openapi/model/global-configuration-output.ts @@ -11,6 +11,7 @@ import { NavbarConfigurationOutput } from './navbar-configuration-output'; import { MetadataConfigurationOutput } from './metadata-configuration-output'; +import { FeedbackConfigurationOutput } from './feedback-configuration-output'; /** @@ -19,5 +20,6 @@ import { MetadataConfigurationOutput } from './metadata-configuration-output'; export interface GlobalConfigurationOutput { metadata: MetadataConfigurationOutput; navbar: NavbarConfigurationOutput; + feedback: FeedbackConfigurationOutput; } diff --git a/frontend/src/app/openapi/model/models.ts b/frontend/src/app/openapi/model/models.ts index 7585cc8939..b248ff37fd 100644 --- a/frontend/src/app/openapi/model/models.ts +++ b/frontend/src/app/openapi/model/models.ts @@ -9,6 +9,7 @@ + To generate a new version, run `make openapi` in the root directory of this repository. */ +export * from './anonymized-session'; export * from './authorization-response'; export * from './backup'; export * from './backup-pipeline-run'; @@ -33,6 +34,15 @@ export * from './diagram-metadata'; export * from './environment-value'; export * from './environment-value1'; export * from './event-type'; +export * from './feedback'; +export * from './feedback-anonymity-policy'; +export * from './feedback-configuration-input'; +export * from './feedback-configuration-output'; +export * from './feedback-interval-configuration-input'; +export * from './feedback-interval-configuration-output'; +export * from './feedback-probability-configuration-input'; +export * from './feedback-probability-configuration-output'; +export * from './feedback-rating'; export * from './file-tree'; export * from './file-type'; export * from './get-revision-model'; @@ -111,7 +121,8 @@ export * from './session'; export * from './session-connection-information'; export * from './session-monitoring-input'; export * from './session-monitoring-output'; -export * from './session-ports'; +export * from './session-ports-input'; +export * from './session-ports-output'; export * from './session-provisioning-request'; export * from './session-sharing'; export * from './session-tool-configuration-input'; @@ -126,9 +137,9 @@ export * from './t4-c-model'; export * from './t4-c-repository'; export * from './t4-c-repository-status'; export * from './token-request'; -export * from './tool'; export * from './tool-backup-configuration-input'; export * from './tool-backup-configuration-output'; +export * from './tool-input'; export * from './tool-integrations-input'; export * from './tool-integrations-output'; export * from './tool-model'; @@ -136,11 +147,13 @@ export * from './tool-model-provisioning-input'; export * from './tool-model-provisioning-output'; export * from './tool-model-restrictions'; export * from './tool-nature'; +export * from './tool-output'; export * from './tool-session-configuration-input'; export * from './tool-session-configuration-output'; export * from './tool-session-connection-input'; export * from './tool-session-connection-input-methods-inner'; -export * from './tool-session-connection-method'; +export * from './tool-session-connection-method-input'; +export * from './tool-session-connection-method-output'; export * from './tool-session-connection-output'; export * from './tool-session-connection-output-methods-inner'; export * from './tool-session-environment-input'; @@ -151,7 +164,8 @@ export * from './tool-session-sharing-configuration-output'; export * from './tool-version'; export * from './tool-version-configuration-input'; export * from './tool-version-configuration-output'; -export * from './tool-version-with-tool'; +export * from './tool-version-with-tool-input'; +export * from './tool-version-with-tool-output'; export * from './toolmodel-status'; export * from './user'; export * from './user-metadata'; diff --git a/frontend/src/app/openapi/model/session-ports-input.ts b/frontend/src/app/openapi/model/session-ports-input.ts new file mode 100644 index 0000000000..d81fbbb5d2 --- /dev/null +++ b/frontend/src/app/openapi/model/session-ports-input.ts @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export interface SessionPortsInput { + /** + * Port of the metrics endpoint in the container. + */ + metrics?: number; +} + diff --git a/frontend/src/app/openapi/model/session-ports.ts b/frontend/src/app/openapi/model/session-ports-output.ts similarity index 92% rename from frontend/src/app/openapi/model/session-ports.ts rename to frontend/src/app/openapi/model/session-ports-output.ts index 4d0a5ffac5..073bbe1275 100644 --- a/frontend/src/app/openapi/model/session-ports.ts +++ b/frontend/src/app/openapi/model/session-ports-output.ts @@ -11,7 +11,7 @@ -export interface SessionPorts { +export interface SessionPortsOutput { /** * Port of the metrics endpoint in the container. */ diff --git a/frontend/src/app/openapi/model/session.ts b/frontend/src/app/openapi/model/session.ts index 4a2a348356..661e2296e9 100644 --- a/frontend/src/app/openapi/model/session.ts +++ b/frontend/src/app/openapi/model/session.ts @@ -11,9 +11,9 @@ import { BaseUser } from './base-user'; import { SessionType } from './session-type'; +import { ToolVersionWithToolOutput } from './tool-version-with-tool-output'; import { Message } from './message'; -import { ToolVersionWithTool } from './tool-version-with-tool'; -import { ToolSessionConnectionMethod } from './tool-session-connection-method'; +import { ToolSessionConnectionMethodOutput } from './tool-session-connection-method-output'; import { SessionSharing } from './session-sharing'; @@ -22,12 +22,12 @@ export interface Session { type: SessionType; created_at: string; owner: BaseUser; - version: ToolVersionWithTool; + version: ToolVersionWithToolOutput; state: string; warnings: Array; last_seen: string; connection_method_id: string; - connection_method: ToolSessionConnectionMethod | null; + connection_method: ToolSessionConnectionMethodOutput | null; shared_with: Array; } export namespace Session { diff --git a/frontend/src/app/openapi/model/tool-input.ts b/frontend/src/app/openapi/model/tool-input.ts new file mode 100644 index 0000000000..978d11c308 --- /dev/null +++ b/frontend/src/app/openapi/model/tool-input.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { ToolSessionConfigurationInput } from './tool-session-configuration-input'; +import { ToolIntegrationsInput } from './tool-integrations-input'; + + +export interface ToolInput { + /** + * Unique identifier of the resource. + */ + id: number; + name?: string; + integrations?: ToolIntegrationsInput; + config?: ToolSessionConfigurationInput; +} + diff --git a/frontend/src/app/openapi/model/tool-model.ts b/frontend/src/app/openapi/model/tool-model.ts index d20807d6ad..d61619a15b 100644 --- a/frontend/src/app/openapi/model/tool-model.ts +++ b/frontend/src/app/openapi/model/tool-model.ts @@ -10,10 +10,10 @@ */ import { GitModel } from './git-model'; +import { ToolOutput } from './tool-output'; import { T4CModel } from './t4-c-model'; import { ToolVersion } from './tool-version'; import { ToolModelRestrictions } from './tool-model-restrictions'; -import { Tool } from './tool'; import { ToolNature } from './tool-nature'; @@ -23,7 +23,7 @@ export interface ToolModel { name: string; description: string; display_order: number | null; - tool: Tool; + tool: ToolOutput; version: ToolVersion | null; nature: ToolNature | null; git_models: Array | null; diff --git a/frontend/src/app/openapi/model/tool.ts b/frontend/src/app/openapi/model/tool-output.ts similarity index 95% rename from frontend/src/app/openapi/model/tool.ts rename to frontend/src/app/openapi/model/tool-output.ts index 0d1c3ee13b..6588b4a470 100644 --- a/frontend/src/app/openapi/model/tool.ts +++ b/frontend/src/app/openapi/model/tool-output.ts @@ -13,7 +13,7 @@ import { ToolSessionConfigurationOutput } from './tool-session-configuration-out import { ToolIntegrationsOutput } from './tool-integrations-output'; -export interface Tool { +export interface ToolOutput { /** * Unique identifier of the resource. */ diff --git a/frontend/src/app/openapi/model/tool-session-connection-method-input.ts b/frontend/src/app/openapi/model/tool-session-connection-method-input.ts new file mode 100644 index 0000000000..976b1748c1 --- /dev/null +++ b/frontend/src/app/openapi/model/tool-session-connection-method-input.ts @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { EnvironmentValue } from './environment-value'; +import { SessionPortsInput } from './session-ports-input'; +import { ToolSessionSharingConfigurationInput } from './tool-session-sharing-configuration-input'; + + +export interface ToolSessionConnectionMethodInput { + id?: string; + type: string; + name?: string; + description?: string; + ports: SessionPortsInput; + /** + * Connection method specific environment variables. Check the global environment field for more information. + */ + environment?: { [key: string]: EnvironmentValue; }; + sharing?: ToolSessionSharingConfigurationInput; +} + diff --git a/frontend/src/app/openapi/model/tool-session-connection-method.ts b/frontend/src/app/openapi/model/tool-session-connection-method-output.ts similarity index 85% rename from frontend/src/app/openapi/model/tool-session-connection-method.ts rename to frontend/src/app/openapi/model/tool-session-connection-method-output.ts index 41ba6a5e57..88d92f90fc 100644 --- a/frontend/src/app/openapi/model/tool-session-connection-method.ts +++ b/frontend/src/app/openapi/model/tool-session-connection-method-output.ts @@ -9,17 +9,17 @@ + To generate a new version, run `make openapi` in the root directory of this repository. */ -import { SessionPorts } from './session-ports'; +import { SessionPortsOutput } from './session-ports-output'; import { ToolSessionSharingConfigurationOutput } from './tool-session-sharing-configuration-output'; import { EnvironmentValue1 } from './environment-value1'; -export interface ToolSessionConnectionMethod { +export interface ToolSessionConnectionMethodOutput { id: string; type: string; name: string; description: string; - ports: SessionPorts; + ports: SessionPortsOutput; /** * Connection method specific environment variables. Check the global environment field for more information. */ diff --git a/frontend/src/app/openapi/model/tool-version-with-tool-input.ts b/frontend/src/app/openapi/model/tool-version-with-tool-input.ts new file mode 100644 index 0000000000..d03035a849 --- /dev/null +++ b/frontend/src/app/openapi/model/tool-version-with-tool-input.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { ToolVersionConfigurationInput } from './tool-version-configuration-input'; +import { ToolInput } from './tool-input'; + + +export interface ToolVersionWithToolInput { + /** + * Unique identifier of the resource. + */ + id: number; + name?: string; + config?: ToolVersionConfigurationInput; + tool: ToolInput; +} + diff --git a/frontend/src/app/openapi/model/tool-version-with-tool.ts b/frontend/src/app/openapi/model/tool-version-with-tool-output.ts similarity index 84% rename from frontend/src/app/openapi/model/tool-version-with-tool.ts rename to frontend/src/app/openapi/model/tool-version-with-tool-output.ts index 399bef2bf3..1dec33004c 100644 --- a/frontend/src/app/openapi/model/tool-version-with-tool.ts +++ b/frontend/src/app/openapi/model/tool-version-with-tool-output.ts @@ -10,16 +10,16 @@ */ import { ToolVersionConfigurationOutput } from './tool-version-configuration-output'; -import { Tool } from './tool'; +import { ToolOutput } from './tool-output'; -export interface ToolVersionWithTool { +export interface ToolVersionWithToolOutput { /** * Unique identifier of the resource. */ id: number; name: string; config: ToolVersionConfigurationOutput; - tool: Tool; + tool: ToolOutput; } diff --git a/frontend/src/app/projects/models/init-model/init-model.component.ts b/frontend/src/app/projects/models/init-model/init-model.component.ts index 2552e98043..eb60c57ff4 100644 --- a/frontend/src/app/projects/models/init-model/init-model.component.ts +++ b/frontend/src/app/projects/models/init-model/init-model.component.ts @@ -20,7 +20,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { combineLatest, filter, map, switchMap, tap } from 'rxjs'; import { SKIP_ERROR_HANDLING_CONTEXT } from 'src/app/general/error-handling/error-handling.interceptor'; import { - Tool, + ToolOutput, ToolModel, ToolNature, ToolVersion, @@ -92,7 +92,7 @@ export class InitModelComponent implements OnInit { } }), map((model: ToolModel) => model.tool), - switchMap((tool: Tool) => + switchMap((tool: ToolOutput) => combineLatest([ this.toolsService.getToolVersions(tool.id, undefined, undefined, { context: SKIP_ERROR_HANDLING_CONTEXT, diff --git a/frontend/src/app/sessions/delete-session-dialog/delete-session-dialog.component.html b/frontend/src/app/sessions/delete-session-dialog/delete-session-dialog.component.html index e7e5e38aed..6d6768a082 100644 --- a/frontend/src/app/sessions/delete-session-dialog/delete-session-dialog.component.html +++ b/frontend/src/app/sessions/delete-session-dialog/delete-session-dialog.component.html @@ -21,7 +21,7 @@

Terminate Session

- + + + +
+ + @if ( + feedbackForm.get("rating")?.value === "bad" || + feedbackForm.get("rating")?.value === "okay" + ) { + + What can we do better? + + Try and be as specific as possible + + + @if ((feedbackService.anonymityPolicy$ | async) === "ask_user") { +
+ I want to be contacted about this +
+ } + } + +
+ @if ( + (feedbackService.anonymityPolicy$ | async) === "force_identified" || + feedbackForm.get("shareContact")?.value + ) { + privacy_tip + + Your contact information will be shared with + {{ + (metadataService.backendMetadata | async)?.provider || + "Systems Engineering Toolchain team" + }}. You may be contacted for further information. + + } @else { + security + Your feedback will be anonymous. + } +
+ + @if (sessions.length > 0) { +
+ storage + Your feedback includes anonymized session details. +
+ } + + +
+ + +
+ diff --git a/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.ts b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.ts new file mode 100644 index 0000000000..03d8c73f13 --- /dev/null +++ b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.ts @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { AsyncPipe } from '@angular/common'; +import { Component, Inject } from '@angular/core'; +import { + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { + MAT_DIALOG_DATA, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogRef, + MatDialogTitle, +} from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIcon } from '@angular/material/icon'; +import { MatInput } from '@angular/material/input'; +import { MetadataService } from '../../../general/metadata/metadata.service'; +import { + AnonymizedSession, + Feedback, + FeedbackService as OpenAPIFeedbackService, + Session, +} from '../../../openapi'; +import { FeedbackService } from '../feedback.service'; + +@Component({ + selector: 'app-feedback', + standalone: true, + imports: [ + MatButtonModule, + MatIcon, + MatFormFieldModule, + MatCheckbox, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatInput, + MatDialogClose, + ReactiveFormsModule, + FormsModule, + AsyncPipe, + ], + templateUrl: './feedback-dialog.component.html', +}) +export class FeedbackDialogComponent { + feedbackForm = new FormGroup({ + rating: new FormControl<'good' | 'okay' | 'bad' | undefined>( + undefined, + Validators.required, + ), + feedbackText: new FormControl(''), + shareContact: new FormControl(false), + }); + + constructor( + @Inject(MAT_DIALOG_DATA) public sessions: Session[], + public dialogRef: MatDialogRef, + public metadataService: MetadataService, + public openApiFeedbackService: OpenAPIFeedbackService, + public feedbackService: FeedbackService, + ) {} + + setRating(rating: 'good' | 'okay' | 'bad') { + this.feedbackForm.get('rating')?.setValue(rating); + this.feedbackForm.get('rating')?.markAsTouched(); + this.feedbackForm.get('rating')?.markAsDirty(); + if (rating === 'good') { + this.feedbackForm.get('feedbackText')?.reset(); + this.feedbackForm.get('shareContact')?.reset(); + } + } + + submitButton = { + disabled: false, + text: 'Submit', + }; + + submit() { + if (this.feedbackForm.invalid) { + return; + } + + this.submitButton.text = 'Submitting...'; + this.submitButton.disabled = true; + + const _sessionData: AnonymizedSession[] = this.sessions.map((session) => ({ + ...session, + owner: null, + shared_with: null, + })); + + const feedback: Feedback = { + rating: this.feedbackForm.get('rating')!.value!, + feedback_text: this.feedbackForm.get('feedbackText')?.value || null, + share_contact: this.feedbackForm.get('shareContact')?.value || false, + sessions: _sessionData, + }; + + this.openApiFeedbackService.submitFeedback(feedback).subscribe({ + next: () => { + this.dialogRef.close(); + }, + error: () => { + this.submitButton.text = 'Try again.'; + this.submitButton.disabled = false; + }, + }); + } +} diff --git a/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.stories.ts b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.stories.ts new file mode 100644 index 0000000000..5b323d9929 --- /dev/null +++ b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.stories.ts @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { BehaviorSubject } from 'rxjs'; +import { dialogWrapper } from 'src/storybook/decorators'; +import { createPersistentSessionWithState } from '../../../../storybook/session'; +import { FeedbackAnonymityPolicy } from '../../../openapi'; +import { FeedbackService } from '../feedback.service'; +import { FeedbackDialogComponent } from './feedback-dialog.component'; + +const meta: Meta = { + title: 'Session Components / Feedback', + component: FeedbackDialogComponent, + decorators: [dialogWrapper], +}; + +export default meta; +type Story = StoryObj; + +class mockFeedbackService implements Partial { + readonly anonymityPolicy$ = new BehaviorSubject( + 'ask_user', + ); +} + +export const NoSessions: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: [], + }, + { + provide: FeedbackService, + useFactory: () => new mockFeedbackService(), + }, + ], + }), + ], +}; + +export const OneSession: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: [createPersistentSessionWithState('running')], + }, + ], + }), + ], +}; + +export const TwoSessions: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: [ + createPersistentSessionWithState('running'), + createPersistentSessionWithState('running'), + ], + }, + ], + }), + ], +}; diff --git a/frontend/src/app/sessions/feedback/feedback.service.ts b/frontend/src/app/sessions/feedback/feedback.service.ts new file mode 100644 index 0000000000..32ce91ced4 --- /dev/null +++ b/frontend/src/app/sessions/feedback/feedback.service.ts @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, combineLatest, map, Observable, tap } from 'rxjs'; +import { + FeedbackAnonymityPolicy, + FeedbackConfigurationOutput, + FeedbackService as OpenAPIFeedbackService, + Session, +} from '../../openapi'; +import { FeedbackDialogComponent } from './feedback-dialog/feedback-dialog.component'; + +@Injectable({ + providedIn: 'root', +}) +export class FeedbackService { + constructor( + private feedbackService: OpenAPIFeedbackService, + public dialog: MatDialog, + ) { + this.loadFeedbackConfig().subscribe(); + } + + private _feedbackConfig = new BehaviorSubject< + FeedbackConfigurationOutput | undefined + >(undefined); + + loadFeedbackConfig(): Observable { + return this.feedbackService + .getFeedback() + .pipe(tap((feedbackConf) => this._feedbackConfig.next(feedbackConf))); + } + + public showDialog(sessions: Session[]) { + this.dialog.open(FeedbackDialogComponent, { + data: sessions, + autoFocus: 'dialog', + }); + } + + // Observable for the feedback configuration + get feedbackConfig$(): Observable { + return this._feedbackConfig.asObservable(); + } + + get enabled$(): Observable { + return this.feedbackConfig$.pipe(map((config) => config?.enabled)); + } + + get showOnFooter$(): Observable { + return combineLatest([this.enabled$, this.feedbackConfig$]).pipe( + map(([enabled, config]) => !!enabled && !!config?.on_footer), + ); + } + + get showOnSessionCard$(): Observable { + return combineLatest([this.enabled$, this.feedbackConfig$]).pipe( + map(([enabled, config]) => !!enabled && !!config?.on_session_card), + ); + } + + get anonymityPolicy$(): Observable { + return this.feedbackConfig$.pipe(map((config) => config?.anonymity_policy)); + } + + public shouldShowIntervalPrompt() { + if (!this.enabled$) return false; + if (!this._feedbackConfig.value?.interval?.enabled) return false; + const lastPrompt = localStorage.getItem('feedbackPrompt'); + if (!lastPrompt) { + return true; + } + const lastPromptDate = new Date(parseInt(lastPrompt)); + + const hoursInterval = + this._feedbackConfig.value.interval.hours_between_prompt; + const now = new Date(); + const diff = now.getTime() - lastPromptDate.getTime(); + const hours = diff / (1000 * 60 * 60); + if (hours >= hoursInterval) { + return true; + } else { + return false; + } + } + + public saveFeedbackPromptDate() { + localStorage.setItem('feedbackPrompt', Date.now().toString()); + } + + public shouldShowPostSessionPrompt() { + if (!this.enabled$) return false; + if ( + !this._feedbackConfig.value?.after_session?.enabled || + this._feedbackConfig.value?.after_session?.percentage === 0 + ) { + return false; + } + + return ( + Math.random() * 100 < + this._feedbackConfig.value?.after_session?.percentage + ); + } +} diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html index f1fce5cded..1d23195a75 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html @@ -38,13 +38,27 @@

No active sessions

@for (session of userSessionService.sessions$ | async; track session.id) {
- - @if (isPersistentSession(session)) { -

Persistent workspace session

- } @else if (isReadonlySession(session)) { -

Read-only session

+
+ + @if (isPersistentSession(session)) { +

Persistent workspace session

+ } @else if (isReadonlySession(session)) { +

Read-only session

+ } +
+ @if (feedbackService.showOnSessionCard$ | async) { + } -
+
+

Read-only session

> Connect open_in_browser + @if (!isSessionShared(session)) {