Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Redesign the active sessions view and rework idletime #2006

Merged
merged 1 commit into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
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
Loading