diff --git a/Makefile b/Makefile
index b84bee3729..1f3c17715f 100644
--- a/Makefile
+++ b/Makefile
@@ -96,6 +96,7 @@ helm-deploy:
--set docker.registry.sessions=$(CAPELLACOLLAB_SESSIONS_REGISTRY) \
--set docker.tag=$(DOCKER_TAG) \
--set mocks.oauth=True \
+ --set mocks.smtp=True \
--set backend.authentication.claimMapping.username=sub \
--set backend.authentication.endpoints.authorization=https://localhost/default/authorize \
--set development=$(DEVELOPMENT_MODE) \
@@ -191,7 +192,7 @@ reach-registry:
fi
dev:
- $(MAKE) -j5 dev-frontend dev-backend dev-oauth-mock dev-docs dev-storybook
+ $(MAKE) -j6 dev-frontend dev-backend dev-oauth-mock dev-smtp-mock dev-docs dev-storybook
dev-frontend:
$(MAKE) -C frontend dev
@@ -202,6 +203,9 @@ dev-backend:
dev-oauth-mock:
$(MAKE) -C mocks/oauth start
+dev-smtp-mock:
+ $(MAKE) -C mocks/smtp start
+
dev-docs:
$(MAKE) -C docs serve
diff --git a/backend/capellacollab/config/models.py b/backend/capellacollab/config/models.py
index 9315d39178..b550e4fcd0 100644
--- a/backend/capellacollab/config/models.py
+++ b/backend/capellacollab/config/models.py
@@ -330,25 +330,30 @@ class PrometheusConfig(BaseConfig):
)
-class SmtpConfig(BaseConfig):
+class SMTPConfig(BaseConfig):
enabled: bool = pydantic.Field(
- default=False,
+ default=True,
description="Whether to enable SMTP. Necessary for feedback.",
examples=[True, False],
)
host: str = pydantic.Field(
description="The SMTP server host.",
+ default="localhost:587",
examples=["smtp.example.com:587"],
pattern=r"^(.*):(\d+)$",
)
user: str = pydantic.Field(
- description="The SMTP server user.", examples=["username"]
+ default="username",
+ description="The SMTP server user.",
+ examples=["username"],
)
password: str = pydantic.Field(
+ default="password",
description="The SMTP server password.",
examples=["password"],
)
sender: str = pydantic.Field(
+ default="capella@example.com",
description="The sender email address.",
examples=["capella@example.com"],
)
@@ -366,4 +371,4 @@ class AppConfig(BaseConfig):
logging: LoggingConfig = LoggingConfig()
requests: RequestsConfig = RequestsConfig()
pipelines: PipelineConfig = PipelineConfig()
- smtp: t.Optional[SmtpConfig] = None
+ smtp: SMTPConfig | None = SMTPConfig()
diff --git a/backend/capellacollab/core/__init__.py b/backend/capellacollab/core/__init__.py
index 643d920477..33a97c0118 100644
--- a/backend/capellacollab/core/__init__.py
+++ b/backend/capellacollab/core/__init__.py
@@ -3,7 +3,7 @@
import os
-DEVELOPMENT_MODE = os.getenv("DEVELOPMENT_MODE", "").lower() in (
+DEVELOPMENT_MODE: bool = os.getenv("DEVELOPMENT_MODE", "").lower() in (
"1",
"true",
"t",
diff --git a/backend/capellacollab/core/email/__init__.py b/backend/capellacollab/core/email/__init__.py
new file mode 100644
index 0000000000..04412280d8
--- /dev/null
+++ b/backend/capellacollab/core/email/__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/core/email/exceptions.py b/backend/capellacollab/core/email/exceptions.py
new file mode 100644
index 0000000000..a3003990df
--- /dev/null
+++ b/backend/capellacollab/core/email/exceptions.py
@@ -0,0 +1,16 @@
+# 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 SMTPNotConfiguredError(core_exceptions.BaseError):
+ def __init__(self):
+ super().__init__(
+ status_code=status.HTTP_403_FORBIDDEN,
+ title="SMTP is not configured",
+ reason="SMTP must be configured in the application configuration before sending emails and activating related features.",
+ err_code="SMTP_NOT_CONFIGURED",
+ )
diff --git a/backend/capellacollab/core/email/models.py b/backend/capellacollab/core/email/models.py
new file mode 100644
index 0000000000..588dc2268c
--- /dev/null
+++ b/backend/capellacollab/core/email/models.py
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+# SPDX-License-Identifier: Apache-2.0
+
+from capellacollab.core import pydantic as core_pydantic
+
+
+class EMailContent(core_pydantic.BaseModelStrict):
+ subject: str
+ message: str
diff --git a/backend/capellacollab/core/email/send.py b/backend/capellacollab/core/email/send.py
new file mode 100644
index 0000000000..304839fc87
--- /dev/null
+++ b/backend/capellacollab/core/email/send.py
@@ -0,0 +1,51 @@
+# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
+# SPDX-License-Identifier: Apache-2.0
+
+import logging
+import smtplib
+from email.mime import multipart, text
+
+import pydantic
+
+from capellacollab.config import config
+
+from . import exceptions, models
+
+
+def send_email(
+ recipients: list[pydantic.EmailStr],
+ email: models.EMailContent,
+ logger: logging.LoggerAdapter,
+):
+ if not (config.smtp and config.smtp.enabled):
+ raise exceptions.SMTPNotConfiguredError()
+
+ try:
+ with smtplib.SMTP(
+ config.smtp.host.split(":")[0], int(config.smtp.host.split(":")[1])
+ ) as smtp:
+ smtp.ehlo()
+ smtp.starttls()
+ smtp.ehlo()
+ smtp.login(config.smtp.user, config.smtp.password)
+
+ logger.info(
+ "Sending emails to recipients %s", ", ".join(recipients)
+ )
+
+ for recipient in recipients:
+ msg = multipart.MIMEMultipart()
+ msg["From"] = config.smtp.sender
+ msg["To"] = recipient
+ msg["Subject"] = email.subject
+ msg.attach(text.MIMEText(email.message, "plain"))
+
+ logger.info(
+ "Sending email to '%s' with subject '%s'",
+ recipient,
+ email.subject,
+ )
+
+ smtp.sendmail(config.smtp.sender, recipient, msg.as_string())
+ except Exception:
+ logger.exception("Error while sending email(s).")
diff --git a/backend/capellacollab/feedback/exceptions.py b/backend/capellacollab/feedback/exceptions.py
index 41d380c84b..5bcc47a4c0 100644
--- a/backend/capellacollab/feedback/exceptions.py
+++ b/backend/capellacollab/feedback/exceptions.py
@@ -6,21 +6,21 @@
from capellacollab.core import exceptions as core_exceptions
-class SmtpNotSetupError(core_exceptions.BaseError):
+class FeedbackNotEnabledError(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",
+ status_code=status.HTTP_403_FORBIDDEN,
+ title="Feedback is not enabled",
+ reason="Feedback must be set up to perform this action",
+ err_code="FEEDBACK_NOT_ENABLED",
)
-class FeedbackNotEnabledError(core_exceptions.BaseError):
+class NoFeedbackRecipientsError(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",
+ status_code=status.HTTP_400_BAD_REQUEST,
+ title="The list of recipients is empty",
+ reason="Feedback can only be activated when there are recipients.",
+ err_code="FEEDBACK_MISSING_RECIPIENTS",
)
diff --git a/backend/capellacollab/feedback/models.py b/backend/capellacollab/feedback/models.py
index c32e85a1ac..fc4b234f31 100644
--- a/backend/capellacollab/feedback/models.py
+++ b/backend/capellacollab/feedback/models.py
@@ -3,13 +3,12 @@
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.sessions import models as sessions_models
from capellacollab.tools import models as tools_models
@@ -21,24 +20,25 @@ class FeedbackRating(str, enum.Enum):
class AnonymizedSession(core_pydantic.BaseModel):
id: str
- type: SessionType
+ type: sessions_models.SessionType
created_at: datetime.datetime
- version: tools_models.ToolVersionWithTool
+ version: tools_models.MinimalToolVersionWithTool
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
+ connection_method: (
+ tools_models.MinimalToolSessionConnectionMethod | 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"
+ feedback_text: str | None = pydantic.Field(
+ description="The feedback text", max_length=255
)
share_contact: bool = pydantic.Field(
description="Whether the user wants to share their contact information"
@@ -46,6 +46,6 @@ class Feedback(core_pydantic.BaseModel):
sessions: list[AnonymizedSession] = pydantic.Field(
description="The sessions the feedback is for"
)
- trigger: str = pydantic.Field(
- description="What triggered the feedback form"
+ trigger: str | None = pydantic.Field(
+ description="What triggered the feedback form", max_length=255
)
diff --git a/backend/capellacollab/feedback/routes.py b/backend/capellacollab/feedback/routes.py
index 117af88cbd..69a3b54cff 100644
--- a/backend/capellacollab/feedback/routes.py
+++ b/backend/capellacollab/feedback/routes.py
@@ -2,46 +2,62 @@
# SPDX-License-Identifier: Apache-2.0
+import logging
import typing as t
import fastapi
from sqlalchemy import orm
+from capellacollab.config import config
from capellacollab.core import database
-from capellacollab.feedback.models import Feedback
-from capellacollab.feedback.util import is_feedback_allowed, send_email
+from capellacollab.core import logging as log
+from capellacollab.core.authentication import injectables as auth_injectables
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
+from . import models, util
+
router = fastapi.APIRouter()
@router.get(
- "/feedback",
+ "/configurations/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())
+def get_feedback_configuration(
+ db: orm.Session = fastapi.Depends(database.get_db),
+):
+ feedback = config_core.get_global_configuration(db).feedback
+ if not (config.smtp and config.smtp.enabled):
+ util.disable_feedback(feedback)
+ return feedback
-@router.post("/feedback")
+@router.post(
+ "/feedback",
+ status_code=204,
+ dependencies=[
+ fastapi.Depends(
+ auth_injectables.RoleVerification(
+ required_role=users_models.Role.USER
+ )
+ )
+ ],
+)
def submit_feedback(
- feedback: Feedback,
+ feedback: models.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),
+ logger: logging.LoggerAdapter = fastapi.Depends(log.get_request_logger),
):
- is_feedback_allowed(db)
- background_tasks.add_task(send_email, feedback, user, user_agent, db)
- return {"status": "sending"}
+ util.check_if_feedback_is_allowed(db)
+
+ background_tasks.add_task(
+ util.send_feedback_email, db, feedback, user, user_agent, logger
+ )
diff --git a/backend/capellacollab/feedback/util.py b/backend/capellacollab/feedback/util.py
index f07c373805..32a846fa65 100644
--- a/backend/capellacollab/feedback/util.py
+++ b/backend/capellacollab/feedback/util.py
@@ -1,117 +1,118 @@
# 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
+import logging
from sqlalchemy import orm
from capellacollab.config import config
-from capellacollab.feedback import exceptions
-from capellacollab.feedback.models import AnonymizedSession, Feedback
+from capellacollab.core.email import exceptions as email_exceptions
+from capellacollab.core.email import models as email_models
+from capellacollab.core.email import send as email_send
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
+from . import exceptions, models
+
+
+def validate_global_configuration(
+ feedback: settings_config_models.FeedbackConfiguration,
+):
+ if feedback.enabled and not (config.smtp and config.smtp.enabled):
+ raise email_exceptions.SMTPNotConfiguredError()
+
+ if feedback.enabled and not feedback.recipients:
+ raise exceptions.NoFeedbackRecipientsError()
+
-def format_session(session: AnonymizedSession):
+def disable_feedback(feedback: settings_config_models.FeedbackConfiguration):
+ feedback.enabled = False
+ feedback.after_session = False
+ feedback.on_footer = False
+ feedback.on_session_card = False
+ feedback.interval.enabled = False
+
+
+def format_session(session: models.AnonymizedSession):
return f"{session.version.tool.name} ({session.version.name})"
-def is_feedback_allowed(db: orm.Session):
+def check_if_feedback_is_allowed(db: orm.Session):
if not config.smtp or not config.smtp.enabled:
- raise exceptions.SmtpNotSetupError()
+ raise email_exceptions.SMTPNotConfiguredError()
- 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:
+ cfg = config_core.get_global_configuration(db)
+ if not cfg.feedback.enabled:
raise exceptions.FeedbackNotEnabledError()
def format_email(
- feedback: Feedback,
- user: t.Optional[users_models.DatabaseUser],
+ feedback: models.Feedback,
+ user: users_models.DatabaseUser | None,
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
- ],
- ]
+) -> email_models.EMailContent:
+ rating = feedback.rating.value
+ user_msg = user.name if user else "Anonymous"
+ if user and user.email:
+ user_msg += f" ({user.email})"
+
+ message_list = [
+ f"Rating: {rating.capitalize()}",
+ f"Text: {feedback.feedback_text or 'No feedback text provided'}",
+ f"User: {user_msg}",
+ f"User Agent: {user_agent or 'Unknown'}",
+ ]
+ if feedback.trigger:
+ message_list.append(f"Trigger: {feedback.trigger}")
+ message_list.append("Sessions:")
+ message_list += [
+ session.model_dump_json(indent=2) for session in feedback.sessions
+ ]
+
+ message_list.append("---")
+ message_list.append(
+ f"You receive this email because you're registered as feedback recipient in the "
+ f"Capella Collaboration Manager ({config.general.scheme}://{config.general.host}:{config.general.port})."
+ )
+ message_list.append(
+ "If you want to unsubscribe, contact your System Administrator."
)
+ message_list.append(
+ "Please note that only the user is validated. All other fields are provided via the API and should not be trusted."
+ )
+ message = "\n".join(message_list)
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,
- }
+ sessions = ", ".join(
+ [format_session(session) for session in feedback.sessions]
+ )
+ return email_models.EMailContent(
+ subject=f"New Feedback with rating {rating} for sessions: {sessions}",
+ message=message,
+ )
else:
- return {
- "subject": f"New General Feedback {feedback.rating.value.capitalize()}",
- "message": message,
- }
+ return email_models.EMailContent(
+ subject=f"New General Feedback with rating {rating}",
+ message=message,
+ )
-def send_email(
- feedback: Feedback,
+def send_feedback_email(
+ db: orm.Session,
+ feedback: models.Feedback,
user: users_models.DatabaseUser,
user_agent: str | None,
- db: orm.Session,
+ logger: logging.LoggerAdapter,
):
- 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
+ check_if_feedback_is_allowed(db)
+ assert config.smtp # Already checked in previous function
+ cfg = config_core.get_global_configuration(db)
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])
+ feedback, user if feedback.share_contact else None, user_agent
)
- 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()
+ email_send.send_email(cfg.feedback.recipients, email_text, logger)
diff --git a/backend/capellacollab/metadata/routes.py b/backend/capellacollab/metadata/routes.py
index 0d01ad3cd0..d079708618 100644
--- a/backend/capellacollab/metadata/routes.py
+++ b/backend/capellacollab/metadata/routes.py
@@ -9,9 +9,6 @@
from capellacollab.core import database
from capellacollab.core import pydantic as core_pydantic
from capellacollab.settings.configuration import core as config_core
-from capellacollab.settings.configuration import (
- models as settings_config_models,
-)
class Metadata(core_pydantic.BaseModel):
@@ -35,8 +32,7 @@ class Metadata(core_pydantic.BaseModel):
response_model=Metadata,
)
def get_metadata(db: orm.Session = fastapi.Depends(database.get_db)):
- cfg = config_core.get_config(db, "global")
- assert isinstance(cfg, settings_config_models.GlobalConfiguration)
+ cfg = config_core.get_global_configuration(db)
return Metadata.model_validate(
cfg.metadata.model_dump()
diff --git a/backend/capellacollab/navbar/routes.py b/backend/capellacollab/navbar/routes.py
index f6849d9ad1..051783e4ab 100644
--- a/backend/capellacollab/navbar/routes.py
+++ b/backend/capellacollab/navbar/routes.py
@@ -6,9 +6,6 @@
from capellacollab.core import database
from capellacollab.settings.configuration import core as config_core
-from capellacollab.settings.configuration import (
- models as settings_config_models,
-)
from capellacollab.settings.configuration.models import NavbarConfiguration
router = fastapi.APIRouter()
@@ -19,7 +16,6 @@
response_model=NavbarConfiguration,
)
def get_navbar(db: orm.Session = fastapi.Depends(database.get_db)):
- cfg = config_core.get_config(db, "global")
- assert isinstance(cfg, settings_config_models.GlobalConfiguration)
+ cfg = config_core.get_global_configuration(db)
return NavbarConfiguration.model_validate(cfg.navbar.model_dump())
diff --git a/backend/capellacollab/sessions/routes.py b/backend/capellacollab/sessions/routes.py
index bd40326475..ca60913833 100644
--- a/backend/capellacollab/sessions/routes.py
+++ b/backend/capellacollab/sessions/routes.py
@@ -37,7 +37,8 @@
required_role=users_models.Role.USER
)
)
- ]
+ ],
+ responses=responses.api_exceptions(include_authentication=True),
)
router_without_authentication = fastapi.APIRouter()
diff --git a/backend/capellacollab/settings/configuration/core.py b/backend/capellacollab/settings/configuration/core.py
index f84f310d98..2bd85eb86f 100644
--- a/backend/capellacollab/settings/configuration/core.py
+++ b/backend/capellacollab/settings/configuration/core.py
@@ -13,3 +13,9 @@ def get_config(db: orm.Session, name: str) -> models.ConfigurationBase:
if configuration:
return model_type().model_validate(configuration.configuration)
return model_type().model_validate({})
+
+
+def get_global_configuration(db: orm.Session) -> models.GlobalConfiguration:
+ cfg = get_config(db, "global")
+ assert isinstance(cfg, models.GlobalConfiguration)
+ return cfg
diff --git a/backend/capellacollab/settings/configuration/models.py b/backend/capellacollab/settings/configuration/models.py
index 6156231c85..a3b8d79f1e 100644
--- a/backend/capellacollab/settings/configuration/models.py
+++ b/backend/capellacollab/settings/configuration/models.py
@@ -8,6 +8,7 @@
import pydantic
from sqlalchemy import orm
+from capellacollab import core
from capellacollab.core import database
from capellacollab.core import pydantic as core_pydantic
from capellacollab.users import models as users_models
@@ -43,6 +44,7 @@ class BuiltInLinkItem(str, enum.Enum):
GRAFANA = "grafana"
PROMETHEUS = "prometheus"
DOCUMENTATION = "documentation"
+ SMTP_MOCK = "smtp_mock"
class NavbarLink(core_pydantic.BaseModelStrict):
@@ -67,37 +69,44 @@ class CustomNavbarLink(NavbarLink):
class NavbarConfiguration(core_pydantic.BaseModelStrict):
external_links: list[BuiltInNavbarLink | CustomNavbarLink] = (
pydantic.Field(
- default=[
- BuiltInNavbarLink(
- name="Grafana",
- service=BuiltInLinkItem.GRAFANA,
- role=users_models.Role.ADMIN,
- ),
- BuiltInNavbarLink(
- name="Prometheus",
- service=BuiltInLinkItem.PROMETHEUS,
- role=users_models.Role.ADMIN,
- ),
- BuiltInNavbarLink(
- name="Documentation",
- service=BuiltInLinkItem.DOCUMENTATION,
- role=users_models.Role.USER,
- ),
- ],
+ default=(
+ [
+ BuiltInNavbarLink(
+ name="Grafana",
+ service=BuiltInLinkItem.GRAFANA,
+ role=users_models.Role.ADMIN,
+ ),
+ BuiltInNavbarLink(
+ name="Prometheus",
+ service=BuiltInLinkItem.PROMETHEUS,
+ role=users_models.Role.ADMIN,
+ ),
+ BuiltInNavbarLink(
+ name="Documentation",
+ service=BuiltInLinkItem.DOCUMENTATION,
+ role=users_models.Role.USER,
+ ),
+ ]
+ + (
+ [
+ BuiltInNavbarLink(
+ name="SMTP Mock",
+ service=BuiltInLinkItem.SMTP_MOCK,
+ role=users_models.Role.USER,
+ )
+ ]
+ if core.DEVELOPMENT_MODE
+ else []
+ )
+ ),
description="Links to display in the navigation bar.",
)
)
-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,
+ default=core.DEVELOPMENT_MODE,
description="Whether the feedback interval is enabled.",
)
hours_between_prompt: int = pydantic.Field(
@@ -122,35 +131,30 @@ class FeedbackProbabilityConfiguration(core_pydantic.BaseModelStrict):
class FeedbackConfiguration(core_pydantic.BaseModelStrict):
enabled: bool = pydantic.Field(
- default=False,
+ default=core.DEVELOPMENT_MODE,
description="Enable or disable the feedback system. If enabled, SMTP configuration is required.",
)
- after_session: FeedbackProbabilityConfiguration = pydantic.Field(
- default_factory=FeedbackProbabilityConfiguration,
+ after_session: bool = pydantic.Field(
+ default=core.DEVELOPMENT_MODE,
description="If a feedback form is shown after terminating a session.",
)
on_footer: bool = pydantic.Field(
- default=True,
+ default=core.DEVELOPMENT_MODE,
description="Should a general feedback button be shown.",
)
on_session_card: bool = pydantic.Field(
- default=True,
+ default=core.DEVELOPMENT_MODE,
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=[],
+ recipients: list[pydantic.EmailStr] = pydantic.Field(
+ default=["test@example.com"] if core.DEVELOPMENT_MODE else [],
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):
diff --git a/backend/capellacollab/settings/configuration/routes.py b/backend/capellacollab/settings/configuration/routes.py
index d7dba47367..6f0bc593a7 100644
--- a/backend/capellacollab/settings/configuration/routes.py
+++ b/backend/capellacollab/settings/configuration/routes.py
@@ -8,6 +8,7 @@
from capellacollab.core import database
from capellacollab.core.authentication import injectables as auth_injectables
+from capellacollab.feedback import util as feedback_util
from capellacollab.users import models as users_models
from . import core, crud, models
@@ -33,7 +34,7 @@
async def get_configuration(
db: orm.Session = fastapi.Depends(database.get_db),
):
- return core.get_config(db, models.GlobalConfiguration._name)
+ return core.get_global_configuration(db)
@router.put(
@@ -48,6 +49,10 @@ async def update_configuration(
db, models.GlobalConfiguration._name
)
+ feedback_util.validate_global_configuration(body.feedback)
+ if body.feedback.enabled is False:
+ feedback_util.disable_feedback(body.feedback)
+
if configuration:
return crud.update_configuration(
db, configuration, body.model_dump()
diff --git a/backend/capellacollab/tools/models.py b/backend/capellacollab/tools/models.py
index 20f041d96c..2d6a10f0a2 100644
--- a/backend/capellacollab/tools/models.py
+++ b/backend/capellacollab/tools/models.py
@@ -84,6 +84,11 @@ class ToolSessionSharingConfiguration(core_pydantic.BaseModel):
)
+class MinimalToolSessionConnectionMethod(core_pydantic.BaseModel):
+ id: str
+ name: str
+
+
class ToolSessionConnectionMethod(core_pydantic.BaseModel):
id: str = pydantic.Field(default_factory=uuid_factory)
type: str
@@ -516,6 +521,18 @@ class ToolVersionWithTool(ToolVersion):
tool: Tool
+class MinimalTool(core_pydantic.BaseModel):
+ id: int = pydantic.Field(ge=1)
+ name: str = pydantic.Field(default="", min_length=2, max_length=30)
+
+
+class MinimalToolVersionWithTool(core_pydantic.BaseModel):
+ id: int = pydantic.Field(ge=1)
+ name: str = pydantic.Field(default="", min_length=2, max_length=30)
+
+ tool: MinimalTool
+
+
class ToolNature(core_pydantic.BaseModel):
id: int
name: str
diff --git a/backend/tests/projects/toolmodels/conftest.py b/backend/tests/projects/toolmodels/conftest.py
index 9fec519b1a..ea35572c96 100644
--- a/backend/tests/projects/toolmodels/conftest.py
+++ b/backend/tests/projects/toolmodels/conftest.py
@@ -143,7 +143,7 @@ def fixture_mock_git_rest_api_for_artifacts(
@pytest.fixture(name="git_query_params")
-def fixture_git_query_params(request: pytest.FixtureRequest) -> t.List[dict]:
+def fixture_git_query_params(request: pytest.FixtureRequest) -> list[dict]:
return request.param
@@ -174,7 +174,7 @@ def fixture_mock_git_get_commit_information_api(
request: pytest.FixtureRequest,
git_type: git_models.GitType,
git_response_status: int,
- git_query_params: t.List[dict],
+ git_query_params: list[dict],
):
match git_type:
case git_models.GitType.GITLAB:
diff --git a/backend/tests/sessions/test_session_feedback.py b/backend/tests/sessions/test_session_feedback.py
deleted file mode 100644
index eb07918ddc..0000000000
--- a/backend/tests/sessions/test_session_feedback.py
+++ /dev/null
@@ -1,155 +0,0 @@
-# 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 5868bc2680..ce3c1c7f1d 100644
--- a/backend/tests/settings/test_global_configuration.py
+++ b/backend/tests/settings/test_global_configuration.py
@@ -113,22 +113,10 @@ def test_update_general_configuration_additional_properties_fails(
assert response.json()["detail"][0]["type"] == "extra_forbidden"
+@pytest.mark.usefixtures("admin")
def test_metadata_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={
@@ -144,29 +132,15 @@ def get_mock_own_user():
assert response.status_code == 200
- del app.dependency_overrides[users_injectables.get_own_user]
-
response = client.get("/api/v1/metadata")
assert response.status_code == 200
assert response.json()["environment"] == "test"
+@pytest.mark.usefixtures("admin")
def test_navbar_is_updated(
client: testclient.TestClient,
- db: orm.Session,
- executor_name: str,
):
- admin = users_crud.create_user(
- db, executor_name, executor_name, None, users_models.Role.ADMIN
- )
-
- def get_mock_own_user():
- return admin
-
- app.dependency_overrides[users_injectables.get_own_user] = (
- get_mock_own_user
- )
-
response = client.put(
"/api/v1/settings/configurations/global",
json={
@@ -184,8 +158,6 @@ def get_mock_own_user():
assert response.status_code == 200
- del app.dependency_overrides[users_injectables.get_own_user]
-
response = client.get("/api/v1/navbar")
assert response.status_code == 200
assert response.json()["external_links"][0] == {
@@ -193,51 +165,3 @@ 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/backend/tests/test_feedback.py b/backend/tests/test_feedback.py
new file mode 100644
index 0000000000..46b4ee8262
--- /dev/null
+++ b/backend/tests/test_feedback.py
@@ -0,0 +1,259 @@
+# 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
+
+from capellacollab.config import config
+from capellacollab.config import models as config_models
+from capellacollab.settings.configuration import crud as configuration_crud
+
+
+@pytest.fixture(name="smtp_config_set")
+def fixture_smtp_config_set(monkeypatch: pytest.MonkeyPatch):
+ monkeypatch.setattr(
+ config,
+ "smtp",
+ config_models.SMTPConfig(
+ enabled=True,
+ host="smtp.example.com:587",
+ user="smtp_user",
+ password="smtp_password",
+ sender="capella@example.com",
+ ),
+ )
+
+
+@pytest.fixture(name="smtp_config_not_set")
+def fixture_smtp_config_not_set(monkeypatch: pytest.MonkeyPatch):
+ monkeypatch.setattr(config, "smtp", None)
+
+
+@pytest.fixture(name="feedback_enabled")
+def fixture_feedback_enabled(db: orm.Session):
+ configuration_crud.create_configuration(
+ db, "global", {"feedback": {"enabled": True}}
+ )
+
+
+@pytest.fixture(name="feedback_disabled")
+def fixture_feedback_disabled(db: orm.Session):
+ configuration_crud.create_configuration(
+ db, "global", {"feedback": {"enabled": False}}
+ )
+
+
+@pytest.mark.usefixtures("user", "smtp_config_set", "feedback_enabled")
+def test_send_feedback(
+ client: testclient.TestClient,
+):
+ with mock.patch(
+ "capellacollab.core.email.send.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 == 204
+ send_email_mock.assert_called_once()
+
+
+@pytest.mark.usefixtures("user", "smtp_config_set", "feedback_enabled")
+def test_send_feedback_with_session(
+ client: testclient.TestClient,
+):
+ with mock.patch(
+ "capellacollab.core.email.send.send_email"
+ ) as send_email_mock:
+ response = client.post(
+ "/api/v1/feedback",
+ json={
+ "rating": "good",
+ "share_contact": False,
+ "sessions": [
+ {
+ "id": "gqakbljquqenfmzvflacgqntx",
+ "type": "persistent",
+ "created_at": "2024-09-19T21:02:12Z",
+ "version": {
+ "id": 4,
+ "name": "6.1.0",
+ "tool": {"id": 1, "name": "Capella"},
+ },
+ "state": "Started",
+ "warnings": [],
+ "connection_method": {
+ "id": "xpra",
+ "name": "Experimental (Xpra)",
+ },
+ }
+ ],
+ "feedback_text": None,
+ "trigger": "test",
+ },
+ )
+
+ assert response.status_code == 204
+ send_email_mock.assert_called_once()
+
+
+@pytest.mark.usefixtures("user", "smtp_config_set", "feedback_enabled")
+def test_send_feedback_with_contact(
+ client: testclient.TestClient,
+):
+ with mock.patch(
+ "capellacollab.core.email.send.send_email"
+ ) as send_email_mock:
+ response = client.post(
+ "/api/v1/feedback",
+ json={
+ "rating": "good",
+ "share_contact": True,
+ "sessions": [],
+ "feedback_text": None,
+ "trigger": None,
+ },
+ )
+
+ assert response.status_code == 204
+ send_email_mock.assert_called_once()
+
+
+@pytest.mark.usefixtures("user", "smtp_config_set", "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 == 403
+ assert response.json()["detail"]["err_code"] == "FEEDBACK_NOT_ENABLED"
+
+
+@pytest.mark.usefixtures("user", "smtp_config_not_set", "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 == 403
+ assert response.json()["detail"]["err_code"] == "SMTP_NOT_CONFIGURED"
+
+
+@pytest.mark.usefixtures("user", "smtp_config_not_set", "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 == 403
+ assert response.json()["detail"]["err_code"] == "SMTP_NOT_CONFIGURED"
+
+
+@pytest.mark.usefixtures("admin")
+def test_feedback_is_updated(
+ client: testclient.TestClient,
+):
+ response = client.put(
+ "/api/v1/settings/configurations/global",
+ json={
+ "feedback": {
+ "enabled": True,
+ "after_session": True,
+ "on_footer": True,
+ "on_session_card": True,
+ "interval": {"enabled": True, "hours_between_prompt": 24},
+ "recipients": ["test@example.com"],
+ }
+ },
+ )
+
+ assert response.status_code == 200
+
+ response = client.get("/api/v1/configurations/feedback")
+ assert response.status_code == 200
+ assert response.json() == {
+ "enabled": True,
+ "after_session": True,
+ "on_footer": True,
+ "on_session_card": True,
+ "interval": {"enabled": True, "hours_between_prompt": 24},
+ "recipients": ["test@example.com"],
+ }
+
+
+@pytest.mark.usefixtures("admin", "smtp_config_not_set")
+def test_activate_feedback_without_smtp(
+ client: testclient.TestClient,
+):
+ response = client.put(
+ "/api/v1/settings/configurations/global",
+ json={
+ "feedback": {
+ "enabled": True,
+ }
+ },
+ )
+
+ assert response.status_code == 403
+ assert response.json()["detail"]["err_code"] == "SMTP_NOT_CONFIGURED"
+
+
+@pytest.mark.usefixtures("admin", "smtp_config_set")
+def test_activate_feedback_without_recipients(
+ client: testclient.TestClient,
+):
+ response = client.put(
+ "/api/v1/settings/configurations/global",
+ json={
+ "feedback": {
+ "enabled": True,
+ "recipients": [],
+ }
+ },
+ )
+
+ assert response.status_code == 400
+ assert (
+ response.json()["detail"]["err_code"] == "FEEDBACK_MISSING_RECIPIENTS"
+ )
+
+
+@pytest.mark.usefixtures("user", "smtp_config_not_set", "feedback_enabled")
+def test_feedback_is_disabled_without_smtp(client: testclient.TestClient):
+ response = client.get("/api/v1/configurations/feedback")
+ assert response.status_code == 200
+ assert response.json()["enabled"] is False
diff --git a/docs/docs/admin/configure-for-your-org.md b/docs/docs/admin/configure-for-your-org.md
index 1b90ceeeb9..099759671a 100644
--- a/docs/docs/admin/configure-for-your-org.md
+++ b/docs/docs/admin/configure-for-your-org.md
@@ -77,8 +77,7 @@ 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.
+ terminated a session.
- 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
@@ -87,27 +86,18 @@ There are several different types of feedback prompt:
```yaml
feedback:
enabled: true
- after_session:
- enabled: true
- percentage: 25
+ after_session: true
on_footer: true
on_session_card: true
interval:
enabled: true
hours_between_prompt: 168
- receivers:
+ recipients: # (1)!
- 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.
+1. Feedback will be sent by email to all addresses specified here.
diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts
index 05d9d33201..8479de0692 100644
--- a/frontend/src/app/app.component.ts
+++ b/frontend/src/app/app.component.ts
@@ -18,7 +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 { FeedbackWrapperService } from './sessions/feedback/feedback.service';
import { FullscreenService } from './sessions/service/fullscreen.service';
@Component({
@@ -45,7 +45,7 @@ export class AppComponent implements OnInit, AfterViewInit {
public pageLayoutService: PageLayoutService,
public fullscreenService: FullscreenService,
private navBarService: NavBarService,
- private feedbackService: FeedbackService,
+ private feedbackService: FeedbackWrapperService,
) {
slugify.extend({ '.': '-' });
}
diff --git a/frontend/src/app/general/footer/footer.component.html b/frontend/src/app/general/footer/footer.component.html
index 16d9f8bcc1..20345c7aa0 100644
--- a/frontend/src/app/general/footer/footer.component.html
+++ b/frontend/src/app/general/footer/footer.component.html
@@ -19,14 +19,13 @@
}}.
-
+
Imprint open_in_new
-
Privacy statement
open_in_new
-
Contribute on GitHub
open_in_new
-
- @if (feedbackService.showOnFooter$ | async) {
+ @if (
+ (feedbackService.feedbackConfig$ | async)?.on_footer &&
+ authService.isLoggedIn()
+ ) {
}
diff --git a/frontend/src/app/general/footer/footer.component.ts b/frontend/src/app/general/footer/footer.component.ts
index c3af01c382..9d0cc4e6f2 100644
--- a/frontend/src/app/general/footer/footer.component.ts
+++ b/frontend/src/app/general/footer/footer.component.ts
@@ -8,7 +8,8 @@ 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';
+import { AuthenticationWrapperService } from 'src/app/services/auth/auth.service';
+import { FeedbackWrapperService } from '../../sessions/feedback/feedback.service';
@Component({
selector: 'app-footer',
@@ -20,6 +21,7 @@ export class FooterComponent {
constructor(
public dialog: MatDialog,
public metadataService: MetadataService,
- public feedbackService: FeedbackService,
+ public feedbackService: FeedbackWrapperService,
+ public authService: AuthenticationWrapperService,
) {}
}
diff --git a/frontend/src/app/general/footer/footer.stories.ts b/frontend/src/app/general/footer/footer.stories.ts
index 44a26fce43..b3158392c2 100644
--- a/frontend/src/app/general/footer/footer.stories.ts
+++ b/frontend/src/app/general/footer/footer.stories.ts
@@ -2,7 +2,14 @@
* SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
* SPDX-License-Identifier: Apache-2.0
*/
-import { Meta, StoryObj } from '@storybook/angular';
+import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
+import { AuthenticationWrapperService } from 'src/app/services/auth/auth.service';
+import { FeedbackWrapperService } from 'src/app/sessions/feedback/feedback.service';
+import { MockAuthenticationWrapperService } from 'src/storybook/auth';
+import {
+ mockFeedbackConfig,
+ MockFeedbackWrapperService,
+} from 'src/storybook/feedback';
import { FooterComponent } from './footer.component';
const meta: Meta
= {
@@ -26,3 +33,21 @@ type Story = StoryObj;
export const General: Story = {
args: {},
};
+
+export const WithFeedbackEnabled: Story = {
+ args: {},
+ decorators: [
+ moduleMetadata({
+ providers: [
+ {
+ provide: FeedbackWrapperService,
+ useFactory: () => new MockFeedbackWrapperService(mockFeedbackConfig),
+ },
+ {
+ provide: AuthenticationWrapperService,
+ useFactory: () => new MockAuthenticationWrapperService(),
+ },
+ ],
+ }),
+ ],
+};
diff --git a/frontend/src/app/general/metadata/version/version.component.html b/frontend/src/app/general/metadata/version/version.component.html
index becf7e83e7..4cdbba14fa 100644
--- a/frontend/src/app/general/metadata/version/version.component.html
+++ b/frontend/src/app/general/metadata/version/version.component.html
@@ -5,7 +5,7 @@
diff --git a/frontend/src/app/general/metadata/version/version.component.ts b/frontend/src/app/general/metadata/version/version.component.ts
index ede9fd8ead..9d385ca42c 100644
--- a/frontend/src/app/general/metadata/version/version.component.ts
+++ b/frontend/src/app/general/metadata/version/version.component.ts
@@ -6,6 +6,7 @@ import { NgIf, AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
+import { environment } from 'src/environments/environment';
import { MetadataService } from '../metadata.service';
@Component({
@@ -19,4 +20,8 @@ export class VersionComponent {
public metadataService: MetadataService,
public dialog: MatDialog,
) {}
+
+ get docsURL() {
+ return environment.docs_url;
+ }
}
diff --git a/frontend/src/app/general/nav-bar/nav-bar.service.ts b/frontend/src/app/general/nav-bar/nav-bar.service.ts
index 7d37118ca6..2cf9156c46 100644
--- a/frontend/src/app/general/nav-bar/nav-bar.service.ts
+++ b/frontend/src/app/general/nav-bar/nav-bar.service.ts
@@ -76,6 +76,7 @@ export class NavBarService {
private _linkMap: Record = {
prometheus: environment.prometheus_url,
grafana: environment.grafana_url,
+ smtp_mock: environment.smtp_mock_url,
documentation: environment.docs_url + '/',
};
}
diff --git a/frontend/src/app/openapi/.openapi-generator/FILES b/frontend/src/app/openapi/.openapi-generator/FILES
index a6d2b463d5..929e1a2d80 100644
--- a/frontend/src/app/openapi/.openapi-generator/FILES
+++ b/frontend/src/app/openapi/.openapi-generator/FILES
@@ -54,13 +54,10 @@ 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
@@ -89,6 +86,9 @@ model/message.ts
model/metadata-configuration-input.ts
model/metadata-configuration-output.ts
model/metadata.ts
+model/minimal-tool-session-connection-method.ts
+model/minimal-tool-version-with-tool.ts
+model/minimal-tool.ts
model/model-artifact-status.ts
model/models.ts
model/navbar-configuration-input-external-links-inner.ts
@@ -141,8 +141,7 @@ model/role.ts
model/session-connection-information.ts
model/session-monitoring-input.ts
model/session-monitoring-output.ts
-model/session-ports-input.ts
-model/session-ports-output.ts
+model/session-ports.ts
model/session-provisioning-request.ts
model/session-sharing.ts
model/session-tool-configuration-input.ts
@@ -160,7 +159,6 @@ 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
@@ -168,13 +166,11 @@ 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-input.ts
-model/tool-session-connection-method-output.ts
+model/tool-session-connection-method.ts
model/tool-session-connection-output-methods-inner.ts
model/tool-session-connection-output.ts
model/tool-session-environment-input.ts
@@ -184,9 +180,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-input.ts
-model/tool-version-with-tool-output.ts
+model/tool-version-with-tool.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/feedback.service.ts b/frontend/src/app/openapi/api/feedback.service.ts
index 74c1952cc7..b86b031e3b 100644
--- a/frontend/src/app/openapi/api/feedback.service.ts
+++ b/frontend/src/app/openapi/api/feedback.service.ts
@@ -97,14 +97,14 @@ export class FeedbackService {
}
/**
- * Get Feedback
+ * Get Feedback Configuration
* @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 {
+ public getFeedbackConfiguration(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable;
+ public getFeedbackConfiguration(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public getFeedbackConfiguration(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>;
+ public getFeedbackConfiguration(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable {
let localVarHeaders = this.defaultHeaders;
@@ -142,7 +142,7 @@ export class FeedbackService {
}
}
- let localVarPath = `/api/v1/feedback`;
+ let localVarPath = `/api/v1/configurations/feedback`;
return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
diff --git a/frontend/src/app/openapi/api/tools.service.ts b/frontend/src/app/openapi/api/tools.service.ts
index b1be2acad9..40711b7bd3 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 { ToolNature } from '../model/tool-nature';
+import { Tool } from '../model/tool';
// @ts-ignore
-import { ToolOutput } from '../model/tool-output';
+import { ToolNature } from '../model/tool-nature';
// @ts-ignore
import { ToolVersion } from '../model/tool-version';
// @ts-ignore
-import { ToolVersionWithToolOutput } from '../model/tool-version-with-tool-output';
+import { ToolVersionWithTool } from '../model/tool-version-with-tool';
// @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
index f3b5c04e54..17255c26bc 100644
--- a/frontend/src/app/openapi/model/anonymized-session.ts
+++ b/frontend/src/app/openapi/model/anonymized-session.ts
@@ -9,21 +9,20 @@
+ To generate a new version, run `make openapi` in the root directory of this repository.
*/
+import { MinimalToolSessionConnectionMethod } from './minimal-tool-session-connection-method';
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';
+import { MinimalToolVersionWithTool } from './minimal-tool-version-with-tool';
export interface AnonymizedSession {
id: string;
type: SessionType;
created_at: string;
- version: ToolVersionWithToolInput;
+ version: MinimalToolVersionWithTool;
state?: string;
warnings?: Array;
- connection_method_id: string;
- connection_method?: ToolSessionConnectionMethodInput | null;
+ connection_method?: MinimalToolSessionConnectionMethod | null;
}
export namespace AnonymizedSession {
}
diff --git a/frontend/src/app/openapi/model/built-in-link-item.ts b/frontend/src/app/openapi/model/built-in-link-item.ts
index d122038a7d..2ffd6b9f3a 100644
--- a/frontend/src/app/openapi/model/built-in-link-item.ts
+++ b/frontend/src/app/openapi/model/built-in-link-item.ts
@@ -11,11 +11,12 @@
-export type BuiltInLinkItem = 'grafana' | 'prometheus' | 'documentation';
+export type BuiltInLinkItem = 'grafana' | 'prometheus' | 'documentation' | 'smtp_mock';
export const BuiltInLinkItem = {
Grafana: 'grafana' as BuiltInLinkItem,
Prometheus: 'prometheus' as BuiltInLinkItem,
- Documentation: 'documentation' as BuiltInLinkItem
+ Documentation: 'documentation' as BuiltInLinkItem,
+ SmtpMock: 'smtp_mock' as BuiltInLinkItem
};
diff --git a/frontend/src/app/openapi/model/feedback-anonymity-policy.ts b/frontend/src/app/openapi/model/feedback-anonymity-policy.ts
deleted file mode 100644
index 0a50781fae..0000000000
--- a/frontend/src/app/openapi/model/feedback-anonymity-policy.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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
index c89a2ec4a7..921877fd53 100644
--- a/frontend/src/app/openapi/model/feedback-configuration-input.ts
+++ b/frontend/src/app/openapi/model/feedback-configuration-input.ts
@@ -9,8 +9,6 @@
+ 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';
@@ -22,7 +20,7 @@ export interface FeedbackConfigurationInput {
/**
* If a feedback form is shown after terminating a session.
*/
- after_session?: FeedbackProbabilityConfigurationInput;
+ after_session?: boolean;
/**
* Should a general feedback button be shown.
*/
@@ -38,13 +36,6 @@ export interface FeedbackConfigurationInput {
/**
* Email addresses to send feedback to.
*/
- receivers?: Array;
- /**
- * If feedback should be anonymous or identified.
- */
- anonymity_policy?: FeedbackAnonymityPolicy;
+ recipients?: Array;
}
-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
index ceff380a10..2f69bf3d7c 100644
--- a/frontend/src/app/openapi/model/feedback-configuration-output.ts
+++ b/frontend/src/app/openapi/model/feedback-configuration-output.ts
@@ -9,9 +9,7 @@
+ 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 {
@@ -22,7 +20,7 @@ export interface FeedbackConfigurationOutput {
/**
* If a feedback form is shown after terminating a session.
*/
- after_session: FeedbackProbabilityConfigurationOutput;
+ after_session: boolean;
/**
* Should a general feedback button be shown.
*/
@@ -38,13 +36,6 @@ export interface FeedbackConfigurationOutput {
/**
* Email addresses to send feedback to.
*/
- receivers: Array;
- /**
- * If feedback should be anonymous or identified.
- */
- anonymity_policy: FeedbackAnonymityPolicy;
+ recipients: Array;
}
-export namespace FeedbackConfigurationOutput {
-}
-
diff --git a/frontend/src/app/openapi/model/feedback.ts b/frontend/src/app/openapi/model/feedback.ts
index 236bdde481..6494ffec6b 100644
--- a/frontend/src/app/openapi/model/feedback.ts
+++ b/frontend/src/app/openapi/model/feedback.ts
@@ -27,10 +27,7 @@ export interface Feedback {
* The sessions the feedback is for
*/
sessions: Array;
- /**
- * What triggered the feedback form
- */
- trigger: string;
+ trigger: string | null;
}
export namespace Feedback {
}
diff --git a/frontend/src/app/openapi/model/session-ports-input.ts b/frontend/src/app/openapi/model/minimal-tool-session-connection-method.ts
similarity index 74%
rename from frontend/src/app/openapi/model/session-ports-input.ts
rename to frontend/src/app/openapi/model/minimal-tool-session-connection-method.ts
index d81fbbb5d2..61d2d78761 100644
--- a/frontend/src/app/openapi/model/session-ports-input.ts
+++ b/frontend/src/app/openapi/model/minimal-tool-session-connection-method.ts
@@ -11,10 +11,8 @@
-export interface SessionPortsInput {
- /**
- * Port of the metrics endpoint in the container.
- */
- metrics?: number;
+export interface MinimalToolSessionConnectionMethod {
+ id: string;
+ name: string;
}
diff --git a/frontend/src/app/openapi/model/feedback-probability-configuration-output.ts b/frontend/src/app/openapi/model/minimal-tool-version-with-tool.ts
similarity index 59%
rename from frontend/src/app/openapi/model/feedback-probability-configuration-output.ts
rename to frontend/src/app/openapi/model/minimal-tool-version-with-tool.ts
index 9a79c3bdb7..9e2d7e031a 100644
--- a/frontend/src/app/openapi/model/feedback-probability-configuration-output.ts
+++ b/frontend/src/app/openapi/model/minimal-tool-version-with-tool.ts
@@ -9,16 +9,12 @@
+ To generate a new version, run `make openapi` in the root directory of this repository.
*/
+import { MinimalTool } from './minimal-tool';
-export interface FeedbackProbabilityConfigurationOutput {
- /**
- * Whether the feedback probability is enabled.
- */
- enabled: boolean;
- /**
- * The percentage of users that will be asked for feedback.
- */
- percentage: number;
+export interface MinimalToolVersionWithTool {
+ id: number;
+ name?: string;
+ tool: MinimalTool;
}
diff --git a/frontend/src/app/openapi/model/feedback-probability-configuration-input.ts b/frontend/src/app/openapi/model/minimal-tool.ts
similarity index 59%
rename from frontend/src/app/openapi/model/feedback-probability-configuration-input.ts
rename to frontend/src/app/openapi/model/minimal-tool.ts
index 0f6c695202..57535f290a 100644
--- a/frontend/src/app/openapi/model/feedback-probability-configuration-input.ts
+++ b/frontend/src/app/openapi/model/minimal-tool.ts
@@ -11,14 +11,8 @@
-export interface FeedbackProbabilityConfigurationInput {
- /**
- * Whether the feedback probability is enabled.
- */
- enabled?: boolean;
- /**
- * The percentage of users that will be asked for feedback.
- */
- percentage?: number;
+export interface MinimalTool {
+ id: number;
+ name?: string;
}
diff --git a/frontend/src/app/openapi/model/models.ts b/frontend/src/app/openapi/model/models.ts
index b248ff37fd..a0c996257d 100644
--- a/frontend/src/app/openapi/model/models.ts
+++ b/frontend/src/app/openapi/model/models.ts
@@ -35,13 +35,10 @@ 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';
@@ -69,6 +66,9 @@ export * from './message';
export * from './metadata';
export * from './metadata-configuration-input';
export * from './metadata-configuration-output';
+export * from './minimal-tool';
+export * from './minimal-tool-session-connection-method';
+export * from './minimal-tool-version-with-tool';
export * from './model-artifact-status';
export * from './navbar-configuration-input';
export * from './navbar-configuration-input-external-links-inner';
@@ -121,8 +121,7 @@ export * from './session';
export * from './session-connection-information';
export * from './session-monitoring-input';
export * from './session-monitoring-output';
-export * from './session-ports-input';
-export * from './session-ports-output';
+export * from './session-ports';
export * from './session-provisioning-request';
export * from './session-sharing';
export * from './session-tool-configuration-input';
@@ -137,9 +136,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';
@@ -147,13 +146,11 @@ 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-input';
-export * from './tool-session-connection-method-output';
+export * from './tool-session-connection-method';
export * from './tool-session-connection-output';
export * from './tool-session-connection-output-methods-inner';
export * from './tool-session-environment-input';
@@ -164,8 +161,7 @@ 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-input';
-export * from './tool-version-with-tool-output';
+export * from './tool-version-with-tool';
export * from './toolmodel-status';
export * from './user';
export * from './user-metadata';
diff --git a/frontend/src/app/openapi/model/session-ports-output.ts b/frontend/src/app/openapi/model/session-ports.ts
similarity index 92%
rename from frontend/src/app/openapi/model/session-ports-output.ts
rename to frontend/src/app/openapi/model/session-ports.ts
index 073bbe1275..4d0a5ffac5 100644
--- a/frontend/src/app/openapi/model/session-ports-output.ts
+++ b/frontend/src/app/openapi/model/session-ports.ts
@@ -11,7 +11,7 @@
-export interface SessionPortsOutput {
+export interface SessionPorts {
/**
* 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 661e2296e9..4a2a348356 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 { ToolSessionConnectionMethodOutput } from './tool-session-connection-method-output';
+import { ToolVersionWithTool } from './tool-version-with-tool';
+import { ToolSessionConnectionMethod } from './tool-session-connection-method';
import { SessionSharing } from './session-sharing';
@@ -22,12 +22,12 @@ export interface Session {
type: SessionType;
created_at: string;
owner: BaseUser;
- version: ToolVersionWithToolOutput;
+ version: ToolVersionWithTool;
state: string;
warnings: Array;
last_seen: string;
connection_method_id: string;
- connection_method: ToolSessionConnectionMethodOutput | null;
+ connection_method: ToolSessionConnectionMethod | 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
deleted file mode 100644
index 978d11c308..0000000000
--- a/frontend/src/app/openapi/model/tool-input.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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 d61619a15b..d20807d6ad 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: ToolOutput;
+ tool: Tool;
version: ToolVersion | null;
nature: ToolNature | null;
git_models: Array | null;
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
deleted file mode 100644
index 976b1748c1..0000000000
--- a/frontend/src/app/openapi/model/tool-session-connection-method-input.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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-output.ts b/frontend/src/app/openapi/model/tool-session-connection-method.ts
similarity index 85%
rename from frontend/src/app/openapi/model/tool-session-connection-method-output.ts
rename to frontend/src/app/openapi/model/tool-session-connection-method.ts
index 88d92f90fc..41ba6a5e57 100644
--- a/frontend/src/app/openapi/model/tool-session-connection-method-output.ts
+++ b/frontend/src/app/openapi/model/tool-session-connection-method.ts
@@ -9,17 +9,17 @@
+ To generate a new version, run `make openapi` in the root directory of this repository.
*/
-import { SessionPortsOutput } from './session-ports-output';
+import { SessionPorts } from './session-ports';
import { ToolSessionSharingConfigurationOutput } from './tool-session-sharing-configuration-output';
import { EnvironmentValue1 } from './environment-value1';
-export interface ToolSessionConnectionMethodOutput {
+export interface ToolSessionConnectionMethod {
id: string;
type: string;
name: string;
description: string;
- ports: SessionPortsOutput;
+ ports: SessionPorts;
/**
* 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
deleted file mode 100644
index d03035a849..0000000000
--- a/frontend/src/app/openapi/model/tool-version-with-tool-input.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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-output.ts b/frontend/src/app/openapi/model/tool-version-with-tool.ts
similarity index 84%
rename from frontend/src/app/openapi/model/tool-version-with-tool-output.ts
rename to frontend/src/app/openapi/model/tool-version-with-tool.ts
index 1dec33004c..399bef2bf3 100644
--- a/frontend/src/app/openapi/model/tool-version-with-tool-output.ts
+++ b/frontend/src/app/openapi/model/tool-version-with-tool.ts
@@ -10,16 +10,16 @@
*/
import { ToolVersionConfigurationOutput } from './tool-version-configuration-output';
-import { ToolOutput } from './tool-output';
+import { Tool } from './tool';
-export interface ToolVersionWithToolOutput {
+export interface ToolVersionWithTool {
/**
* Unique identifier of the resource.
*/
id: number;
name: string;
config: ToolVersionConfigurationOutput;
- tool: ToolOutput;
+ tool: Tool;
}
diff --git a/frontend/src/app/openapi/model/tool-output.ts b/frontend/src/app/openapi/model/tool.ts
similarity index 95%
rename from frontend/src/app/openapi/model/tool-output.ts
rename to frontend/src/app/openapi/model/tool.ts
index 6588b4a470..0d1c3ee13b 100644
--- a/frontend/src/app/openapi/model/tool-output.ts
+++ b/frontend/src/app/openapi/model/tool.ts
@@ -13,7 +13,7 @@ import { ToolSessionConfigurationOutput } from './tool-session-configuration-out
import { ToolIntegrationsOutput } from './tool-integrations-output';
-export interface ToolOutput {
+export interface Tool {
/**
* Unique identifier of the resource.
*/
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 eb60c57ff4..9668d8d686 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,11 +20,11 @@ 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 {
- ToolOutput,
ToolModel,
ToolNature,
ToolVersion,
ToolsService,
+ Tool,
} from 'src/app/openapi';
import { ModelWrapperService } from 'src/app/projects/models/service/model.service';
import { GitModelService } from '../../project-detail/model-overview/model-detail/git-model.service';
@@ -92,7 +92,7 @@ export class InitModelComponent implements OnInit {
}
}),
map((model: ToolModel) => model.tool),
- switchMap((tool: ToolOutput) =>
+ switchMap((tool: Tool) =>
combineLatest([
this.toolsService.getToolVersions(tool.id, undefined, undefined, {
context: SKIP_ERROR_HANDLING_CONTEXT,
diff --git a/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.html b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.html
index d01855ea2a..58502cd9bc 100644
--- a/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.html
+++ b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.html
@@ -13,67 +13,64 @@
How were your sessions?
}
-
-
-
+
+
+ @for (rating of ratings; track rating) {
-
-
-
+ }
+
- @if (
- feedbackForm.get("rating")?.value === "bad" ||
- feedbackForm.get("rating")?.value === "okay"
- ) {
-
+ @if (feedbackForm.get("rating")?.value) {
+
+ @if (feedbackForm.get("rating")?.value === "good") {
+ What was good?
+ } @else {
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
-
}
- }
+
+ Try to be as specific as possible.
+
+ }
+
+
+ Share user information
+
-
- @if (
- (feedbackService.anonymityPolicy$ | async) === "force_identified" ||
- feedbackForm.get("shareContact")?.value
- ) {
+
+
+ @if (feedbackForm.get("shareContact")?.value) {
privacy_tip
Your contact information will be shared with
@@ -85,20 +82,23 @@
} @else {
security
- Your feedback will be anonymous.
+ Your feedback will be anonymous.
}
-
- @if (data.sessions.length > 0) {
-
+
+ @if (data.sessions.length > 0) {
storage
- Your feedback includes anonymized session details.
-
- }
+ @if (feedbackForm.get("shareContact")?.value) {
+
Your feedback includes session details.
+ } @else {
+
Your feedback includes anonymized session details.
+ }
+ }
+
-
-
+
+