diff --git a/backend/capellacollab/config/models.py b/backend/capellacollab/config/models.py index cc89b851e..9315d3917 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 000000000..04412280d --- /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 000000000..41d380c84 --- /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 000000000..c32e85a1a --- /dev/null +++ b/backend/capellacollab/feedback/models.py @@ -0,0 +1,51 @@ +# 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" + ) + trigger: str = pydantic.Field( + description="What triggered the feedback form" + ) diff --git a/backend/capellacollab/feedback/routes.py b/backend/capellacollab/feedback/routes.py new file mode 100644 index 000000000..117af88cb --- /dev/null +++ b/backend/capellacollab/feedback/routes.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + + +import typing as t + +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_agent: t.Annotated[str | None, fastapi.Header()] = None, + 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, user_agent, db) + return {"status": "sending"} diff --git a/backend/capellacollab/feedback/util.py b/backend/capellacollab/feedback/util.py new file mode 100644 index 000000000..f07c37380 --- /dev/null +++ b/backend/capellacollab/feedback/util.py @@ -0,0 +1,117 @@ +# 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], + user_agent: str | None, +): + message = "\n".join( + [ + f"Rating: {feedback.rating.value}", + f"Text: {feedback.feedback_text or 'No feedback text provided'}", + f"User: {f'{user.name} ({user.email})' if user else 'Anonymous User'}", + f"User Agent: {user_agent or 'Unknown'}", + f"Feedback Trigger: {feedback.trigger}", + *[ + session.model_dump_json(indent=2) + for session in feedback.sessions + ], + ] + ) + + 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": message, + } + else: + return { + "subject": f"New General Feedback {feedback.rating.value.capitalize()}", + "message": message, + } + + +def send_email( + feedback: Feedback, + user: users_models.DatabaseUser, + user_agent: str | None, + 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 + + email_text = format_email( + feedback, None if is_anonymous else user, user_agent + ) + + 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) + + for receiver in feedback_config.receivers: + msg = MIMEMultipart() + msg["From"] = config.smtp.sender + msg["To"] = receiver + msg["Subject"] = email_text["subject"] + msg.attach(MIMEText(email_text["message"], "plain")) + + mailserver.sendmail(config.smtp.sender, receiver, msg.as_string()) + + mailserver.quit() diff --git a/backend/capellacollab/routes.py b/backend/capellacollab/routes.py index 656e7894f..5c3d6fdce 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 36fc6038c..6156231c8 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.", + ) + receivers: list[pydantic.EmailStr] = pydantic.Field( + default=[], + description="Email addresses 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 1db0a3145..c5b0f5845 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "alembic==1.13.2", "appdirs", "cachetools", + "email-validator", "fastapi>=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 000000000..eb07918dd --- /dev/null +++ b/backend/tests/sessions/test_session_feedback.py @@ -0,0 +1,155 @@ +# 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, + "trigger": "test", + }, + ) + + 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, + "trigger": "test", + }, + ) + + 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, + "trigger": "test", + }, + ) + 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, + "trigger": "test", + }, + ) + 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, + "trigger": "test", + }, + ) + assert response.status_code == 500 diff --git a/backend/tests/settings/test_global_configuration.py b/backend/tests/settings/test_global_configuration.py index 03437295d..5868bc268 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}, + "receivers": ["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}, + "receivers": ["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 165898bc8..1b90ceeeb 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,52 @@ 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 + receivers: + - test1@example.com + - test2@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 2888a4f2d..05d9d3320 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([], 'On interval'); + 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 346efbc00..16d9f8bcc 100644 --- a/frontend/src/app/general/footer/footer.component.html +++ b/frontend/src/app/general/footer/footer.component.html @@ -42,6 +42,12 @@ >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 0bebcb8f0..c3af01c38 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 bf02952da..a6d2b463d 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 f498c004f..0b15f2169 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 000000000..74c1952cc --- /dev/null +++ b/frontend/src/app/openapi/api/feedback.service.ts @@ -0,0 +1,244 @@ +/* + * 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 userAgent + * @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, userAgent?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public submitFeedback(feedback: Feedback, userAgent?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public submitFeedback(feedback: Feedback, userAgent?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public submitFeedback(feedback: Feedback, userAgent?: string, 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; + if (userAgent !== undefined && userAgent !== null) { + localVarHeaders = localVarHeaders.set('user-agent', String(userAgent)); + } + + 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 40711b7bd..b1be2acad 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 000000000..f3b5c04e5 --- /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 000000000..0a50781fa --- /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 000000000..c89a2ec4a --- /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 addresses to send feedback to. + */ + receivers?: Array; + /** + * 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 000000000..ceff380a1 --- /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 addresses to send feedback to. + */ + receivers: Array; + /** + * 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 000000000..9e543140e --- /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 000000000..f3a53f26c --- /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 000000000..0f6c69520 --- /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 000000000..9a79c3bdb --- /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 000000000..16b48081d --- /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 000000000..236bdde48 --- /dev/null +++ b/frontend/src/app/openapi/model/feedback.ts @@ -0,0 +1,38 @@ +/* + * 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; + /** + * What triggered the feedback form + */ + trigger: string; +} +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 70c4954a1..6c568c595 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 cab85de70..c52b7fa67 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 7585cc893..b248ff37f 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 000000000..d81fbbb5d --- /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 4d0a5ffac..073bbe127 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 4a2a34835..661e2296e 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 000000000..978d11c30 --- /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 d20807d6a..d61619a15 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 0d1c3ee13..6588b4a47 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 000000000..976b1748c --- /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 41ba6a5e5..88d92f90f 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 000000000..d03035a84 --- /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 399bef2bf..1dec33004 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 2552e9804..eb60c57ff 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 e7e5e38ae..6d6768a08 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 (data.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 000000000..0e5fda2cc --- /dev/null +++ b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.ts @@ -0,0 +1,127 @@ +/* + * 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'; + +interface DialogData { + sessions: Session[]; + trigger: string; +} + +@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 data: DialogData, + 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.data.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, + trigger: this.data.trigger, + }; + + 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 000000000..51c87984e --- /dev/null +++ b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.stories.ts @@ -0,0 +1,82 @@ +/* + * 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: { sessions: [], trigger: 'storybook' }, + }, + { + provide: FeedbackService, + useFactory: () => new mockFeedbackService(), + }, + ], + }), + ], +}; + +export const OneSession: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { + sessions: [createPersistentSessionWithState('running')], + trigger: 'storybook', + }, + }, + ], + }), + ], +}; + +export const TwoSessions: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { + sessions: [ + createPersistentSessionWithState('running'), + createPersistentSessionWithState('running'), + ], + trigger: 'storybook', + }, + }, + ], + }), + ], +}; 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 000000000..e6a94440a --- /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[], trigger: string) { + this.dialog.open(FeedbackDialogComponent, { + data: { sessions, trigger }, + 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 f1fce5cde..467b8b80f 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)) {