Skip to content

Commit

Permalink
feat: Redesign the sessions view and rework idletime
Browse files Browse the repository at this point in the history
Rework idletime:
- Renamed idletime to idle_status object
- Also return termination time from config
- Show card on active session if session will be terminated soon

Overhaul parts of the session view:
- Show times relative to now
- Show session info as one text block instead of split
- Redesign last requested session and active session card
- Show additional info on active session cards as mini cards

Closes #1783
Closes #1486
  • Loading branch information
zusorio committed Nov 18, 2024
1 parent 5c1976f commit 6ca7ea3
Show file tree
Hide file tree
Showing 23 changed files with 431 additions and 107 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()
54 changes: 33 additions & 21 deletions backend/capellacollab/sessions/injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,50 @@

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"""
# Has to be in here because of circular imports


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,
)
11 changes: 11 additions & 0 deletions frontend/package-lock.json

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

1 change: 1 addition & 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
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.

3 changes: 2 additions & 1 deletion frontend/src/app/openapi/model/session.ts

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

Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,21 @@

<ng-container matColumnDef="last_seen">
<th mat-header-cell *matHeaderCellDef>Last seen</th>
<td mat-cell *matCellDef="let element">{{ element.last_seen }}</td>
<td mat-cell *matCellDef="let element">
@if (element.idle_state.available) {
@if (element.idle_state.idle_for_minutes! === -1) {
Never connected
} @else {
<app-relative-time
[date]="
subMinutes(Date.now(), element.idle_state.idle_for_minutes!)
"
/>
}
} @else {
{{ element.idle_state.unavailable_reason }}
}
</td>
</ng-container>

<ng-container matColumnDef="tool">
Expand Down
Loading

0 comments on commit 6ca7ea3

Please sign in to comment.