Skip to content

Commit

Permalink
Merge f912ba2 into 6a3ca78
Browse files Browse the repository at this point in the history
  • Loading branch information
zusorio authored Nov 19, 2024
2 parents 6a3ca78 + f912ba2 commit 40de9eb
Show file tree
Hide file tree
Showing 24 changed files with 463 additions and 114 deletions.
9 changes: 9 additions & 0 deletions backend/capellacollab/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ class PipelineConfig(BaseConfig):
)


class SessionsConfig(BaseConfig):
timeout: int = pydantic.Field(
default=90,
description="The timeout (in minutes) for unused and idle sessions.",
examples=[60, 90],
)


class DatabaseConfig(BaseConfig):
url: str = pydantic.Field(
default="postgresql://dev:dev@localhost:5432/dev",
Expand Down Expand Up @@ -396,4 +404,5 @@ class AppConfig(BaseConfig):
logging: LoggingConfig = LoggingConfig()
requests: RequestsConfig = RequestsConfig()
pipelines: PipelineConfig = PipelineConfig()
sessions: SessionsConfig = SessionsConfig()
smtp: SMTPConfig | None = SMTPConfig()
51 changes: 30 additions & 21 deletions backend/capellacollab/sessions/injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,47 @@

from capellacollab import core
from capellacollab.config import config
from capellacollab.sessions import models2 as sessions_models2

log = logging.getLogger(__name__)


def get_last_seen(sid: str) -> str:
"""Return project session last seen activity"""
def get_idle_state(sid: str) -> sessions_models2.IdleState:
if core.LOCAL_DEVELOPMENT_MODE:
return "Disabled in development mode"
return sessions_models2.IdleState(
available=False,
unavailable_reason="Unavailable in local development mode",
terminate_after_minutes=config.sessions.timeout,
)

url = f"{config.prometheus.url}/api/v1/query?query=idletime_minutes"
try:
response = requests.get(
url,
f'{config.prometheus.url}/api/v1/query?query=idletime_minutes{{session_id="{sid}"}}',
timeout=config.requests.timeout,
)
response.raise_for_status()

for session in response.json()["data"]["result"]:
if sid == session["metric"]["session_id"]:
return _get_last_seen(float(session["value"][1]))

log.debug("Couldn't find Prometheus metrics for session %s.", sid)
except Exception:
log.exception("Exception during fetching of last seen.")
return "UNKNOWN"


def _get_last_seen(idletime: int | float) -> str:
if idletime == -1:
return "Never connected"
log.exception("Exception during fetching of idle state.")
return sessions_models2.IdleState(
available=False,
unavailable_reason="Exception during fetching of idle state",
terminate_after_minutes=config.sessions.timeout,
)

if (idlehours := idletime / 60) > 1:
return f"{round(idlehours, 2)} hrs ago"
if len(response.json()["data"]["result"]) > 0:
idle_for_minutes = int(
response.json()["data"]["result"][0]["value"][1]
)
return sessions_models2.IdleState(
available=True,
idle_for_minutes=idle_for_minutes,
terminate_after_minutes=config.sessions.timeout,
)
else:
log.debug("Couldn't find Prometheus metrics for session %s.", sid)

return f"{idletime:.0f} mins ago"
return sessions_models2.IdleState(
available=False,
unavailable_reason="Unknown session",
terminate_after_minutes=config.sessions.timeout,
)
12 changes: 10 additions & 2 deletions backend/capellacollab/sessions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import sqlalchemy as sa
from sqlalchemy import orm

from capellacollab.config import config
from capellacollab.core import database
from capellacollab.core import models as core_models
from capellacollab.core import pydantic as core_pydantic
from capellacollab.sessions import models2 as sessions_models2
from capellacollab.sessions import operators
from capellacollab.tools import models as tools_models
from capellacollab.users import models as users_models
Expand Down Expand Up @@ -101,7 +103,13 @@ class Session(core_pydantic.BaseModel):
)
state: SessionState = pydantic.Field(default=SessionState.UNKNOWN)
warnings: list[core_models.Message] = pydantic.Field(default=[])
last_seen: str = pydantic.Field(default="UNKNOWN")
idle_state: sessions_models2.IdleState = pydantic.Field(
default=sessions_models2.IdleState(
available=False,
terminate_after_minutes=config.sessions.timeout,
unavailable_reason="Uninitialized",
)
)

connection_method_id: str
connection_method: tools_models.ToolSessionConnectionMethod | None = None
Expand All @@ -126,7 +134,7 @@ def resolve_connection_method(self) -> t.Any:

@pydantic.model_validator(mode="after")
def add_warnings_and_last_seen(self) -> t.Any:
self.last_seen = injection.get_last_seen(self.id)
self.idle_state = injection.get_idle_state(self.id)
self.preparation_state, self.state = (
operators.get_operator().get_session_state(self.id)
)
Expand Down
16 changes: 16 additions & 0 deletions backend/capellacollab/sessions/models2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import pydantic

from capellacollab.core import pydantic as core_pydantic


class IdleState(core_pydantic.BaseModel):
available: bool
idle_for_minutes: int | None = pydantic.Field(
default=None,
description="The number of minutes the session has been idle. Value is -1 if the session has never been connected to.",
)
terminate_after_minutes: int
unavailable_reason: str | None = pydantic.Field(default=None)
10 changes: 8 additions & 2 deletions backend/tests/sessions/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import pytest
from sqlalchemy import orm

from capellacollab import __main__
from capellacollab.sessions import crud as sessions_crud
from capellacollab.sessions import injection as sessions_injection
from capellacollab.sessions import models as sessions_models
from capellacollab.sessions import models2 as sessions_models2
from capellacollab.sessions.operators import k8s as k8s_operator
from capellacollab.tools import models as tools_models
from capellacollab.users import models as users_models
Expand Down Expand Up @@ -59,7 +59,13 @@ def fixture_test_session(
@pytest.fixture(name="mock_session_injection")
def fixture_mock_session_injection(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
sessions_injection, "get_last_seen", lambda _: "UNKNOWN"
sessions_injection,
"get_idle_state",
lambda _: sessions_models2.IdleState(
available=False,
unavailable_reason="Unavailable during testing",
terminate_after_minutes=90,
),
)
monkeypatch.setattr(
k8s_operator.KubernetesOperator,
Expand Down
63 changes: 61 additions & 2 deletions backend/tests/sessions/test_session_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,72 @@
# SPDX-License-Identifier: Apache-2.0

import pytest
import responses
from fastapi import status

from capellacollab import core
from capellacollab.config import config
from capellacollab.sessions import injection
from capellacollab.sessions import models2 as models2_sessions


def test_get_last_seen_disabled_in_development_mode(
def test_get_idle_status_disabled_in_development_mode(
monkeypatch: pytest.MonkeyPatch,
):
monkeypatch.setattr(core, "LOCAL_DEVELOPMENT_MODE", True)
assert injection.get_last_seen("test") == "Disabled in development mode"
assert injection.get_idle_state("test") == models2_sessions.IdleState(
available=False,
terminate_after_minutes=90,
unavailable_reason="Unavailable in local development mode",
)


def test_get_idle_status_exception():
assert injection.get_idle_state("test") == models2_sessions.IdleState(
available=False,
unavailable_reason="Exception during fetching of idle state",
terminate_after_minutes=90,
)


def test_get_idle_status_unknown_session():
with responses.RequestsMock() as rsps:
rsps.get(
f'{config.prometheus.url}/api/v1/query?query=idletime_minutes{{session_id="test"}}',
status=status.HTTP_200_OK,
json={
"status": "success",
"data": {"resultType": "vector", "result": []},
},
)

assert injection.get_idle_state("test") == models2_sessions.IdleState(
available=False,
unavailable_reason="Unknown session",
terminate_after_minutes=90,
)


def test_get_idle_status():
with responses.RequestsMock() as rsps:
rsps.get(
f'{config.prometheus.url}/api/v1/query?query=idletime_minutes{{session_id="test"}}',
status=status.HTTP_200_OK,
json={
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"value": [1731683497.386, "12"],
}
],
},
},
)

assert injection.get_idle_state("test") == models2_sessions.IdleState(
available=True,
idle_for_minutes=12,
terminate_after_minutes=90,
)
25 changes: 22 additions & 3 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@fontsource/roboto": "^5.1.0",
"@ngneat/until-destroy": "^10.0.0",
"@panzoom/panzoom": "^4.5.1",
"date-fns": "^4.1.0",
"file-saver": "^2.0.5",
"highlight.js": "^11.10.0",
"http-status-codes": "^2.3.0",
Expand Down Expand Up @@ -72,6 +73,7 @@
"eslint-plugin-storybook": "0.11.0",
"eslint-plugin-tailwindcss": "^3.17.5",
"eslint-plugin-unused-imports": "^4.1.4",
"mockdate": "^3.0.5",
"npm-check-updates": "^17.1.11",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!--
~ SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
~ SPDX-License-Identifier: Apache-2.0
-->

<span [matTooltip]="absoluteTime">
{{ relativeTime }}
</span>
29 changes: 29 additions & 0 deletions frontend/src/app/general/relative-time/relative-time.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
* SPDX-License-Identifier: Apache-2.0
*/
import { Component, Input } from '@angular/core';
import { MatTooltip } from '@angular/material/tooltip';
import { formatDistanceToNow, format } from 'date-fns';
import { DateArg } from 'date-fns/types';

@Component({
selector: 'app-relative-time',
standalone: true,
imports: [MatTooltip],
templateUrl: './relative-time.component.html',
})
export class RelativeTimeComponent {
@Input() date?: DateArg<Date>;
@Input() suffix = true;

get relativeTime(): string {
if (!this.date) return '';
return formatDistanceToNow(this.date, { addSuffix: this.suffix });
}

get absoluteTime(): string {
if (!this.date) return '';
return format(this.date, 'PPpp');
}
}
1 change: 1 addition & 0 deletions frontend/src/app/openapi/.openapi-generator/FILES

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions frontend/src/app/openapi/model/idle-state.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/src/app/openapi/model/models.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 40de9eb

Please sign in to comment.