From 6ed087c6ff682b6b9a721b07aa8eb0f6f3e64fa4 Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Mon, 2 Dec 2024 16:54:54 +0100 Subject: [PATCH] feat: Group sessions by projects Show the project for read-only and provisioned sessions in the Active Sessions overview. --- ...f986_add_project_scope_to_session_table.py | 25 ++ backend/capellacollab/sessions/models.py | 15 + backend/capellacollab/sessions/routes.py | 1 + .../guacamole-connection-method-input.ts | 9 +- .../guacamole-connection-method-output.ts | 9 +- .../model/http-connection-method-input.ts | 9 +- .../model/http-connection-method-output.ts | 9 +- frontend/src/app/openapi/model/session.ts | 2 + ...-session-connection-input-methods-inner.ts | 10 +- ...session-connection-output-methods-inner.ts | 10 +- .../app/sessions/service/session.service.ts | 42 +- .../session-overview.stories.ts | 8 +- .../active-sessions.component.css | 20 - .../active-sessions.component.html | 186 ++------- .../active-sessions.component.ts | 112 +---- .../active-sessions.stories.ts | 391 ++---------------- .../session-card/session-card.component.html | 145 +++++++ .../session-card/session-card.component.ts | 143 +++++++ .../session-card/session-card.stories.ts | 279 +++++++++++++ .../create-persistent-session.component.ts | 16 +- ...reate-readonly-session-dialog.component.ts | 17 +- .../create-session-history.component.ts | 15 +- frontend/src/storybook/session.ts | 52 +-- 23 files changed, 825 insertions(+), 700 deletions(-) create mode 100644 backend/capellacollab/alembic/versions/4cf566b4f986_add_project_scope_to_session_table.py delete mode 100644 frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.css create mode 100644 frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.component.html create mode 100644 frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.component.ts create mode 100644 frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.stories.ts diff --git a/backend/capellacollab/alembic/versions/4cf566b4f986_add_project_scope_to_session_table.py b/backend/capellacollab/alembic/versions/4cf566b4f986_add_project_scope_to_session_table.py new file mode 100644 index 0000000000..e6fa7c64b6 --- /dev/null +++ b/backend/capellacollab/alembic/versions/4cf566b4f986_add_project_scope_to_session_table.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Add project scope to session table + +Revision ID: 4cf566b4f986 +Revises: 2f8449c217fa +Create Date: 2024-12-02 14:40:15.815359 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "4cf566b4f986" +down_revision = "2f8449c217fa" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "sessions", sa.Column("project_id", sa.Integer(), nullable=True) + ) + op.create_foreign_key(None, "sessions", "projects", ["project_id"], ["id"]) diff --git a/backend/capellacollab/sessions/models.py b/backend/capellacollab/sessions/models.py index 86813392fe..a468298d33 100644 --- a/backend/capellacollab/sessions/models.py +++ b/backend/capellacollab/sessions/models.py @@ -16,6 +16,7 @@ from capellacollab.core import database from capellacollab.core import models as core_models from capellacollab.core import pydantic as core_pydantic +from capellacollab.projects import models as projects_models from capellacollab.sessions import models2 as sessions_models2 from capellacollab.sessions import operators from capellacollab.tools import models as tools_models @@ -24,6 +25,7 @@ from . import injection if t.TYPE_CHECKING: + from capellacollab.projects.models import DatabaseProject from capellacollab.projects.toolmodels.provisioning.models import ( DatabaseModelProvisioning, ) @@ -131,6 +133,10 @@ class Session(core_pydantic.BaseModel): shared_with: list[SessionSharing] = pydantic.Field(default=[]) + project: projects_models.SimpleProject | None = pydantic.Field( + default=None + ) + _validate_created_at = pydantic.field_serializer("created_at")( core_pydantic.datetime_serializer ) @@ -217,6 +223,15 @@ class DatabaseSession(database.Base): ) ) + project_id: orm.Mapped[int | None] = orm.mapped_column( + sa.ForeignKey("projects.id"), + init=False, + ) + project: orm.Mapped[DatabaseProject | None] = orm.relationship( + foreign_keys=[project_id], + default=None, + ) + environment: orm.Mapped[dict[str, str]] = orm.mapped_column( nullable=False, default_factory=dict ) diff --git a/backend/capellacollab/sessions/routes.py b/backend/capellacollab/sessions/routes.py index 6ed27b24de..5873b7cf10 100644 --- a/backend/capellacollab/sessions/routes.py +++ b/backend/capellacollab/sessions/routes.py @@ -230,6 +230,7 @@ async def request_session( }, created_at=session["created_at"], connection_method_id=connection_method.id, + project=project_scope, ), ) diff --git a/frontend/src/app/openapi/model/guacamole-connection-method-input.ts b/frontend/src/app/openapi/model/guacamole-connection-method-input.ts index aa784f6a10..2149f07d7c 100644 --- a/frontend/src/app/openapi/model/guacamole-connection-method-input.ts +++ b/frontend/src/app/openapi/model/guacamole-connection-method-input.ts @@ -16,7 +16,7 @@ import { RDPPortsInput } from './rdp-ports-input'; export interface GuacamoleConnectionMethodInput { id?: string; - type?: string; + type?: GuacamoleConnectionMethodInput.TypeEnum; name?: string; description?: string; ports?: RDPPortsInput; @@ -26,4 +26,11 @@ export interface GuacamoleConnectionMethodInput { environment?: { [key: string]: EnvironmentValue; }; sharing?: ToolSessionSharingConfigurationInput; } +export namespace GuacamoleConnectionMethodInput { + export type TypeEnum = 'guacamole'; + export const TypeEnum = { + Guacamole: 'guacamole' as TypeEnum + }; +} + diff --git a/frontend/src/app/openapi/model/guacamole-connection-method-output.ts b/frontend/src/app/openapi/model/guacamole-connection-method-output.ts index 893b1a3bf5..77faae7408 100644 --- a/frontend/src/app/openapi/model/guacamole-connection-method-output.ts +++ b/frontend/src/app/openapi/model/guacamole-connection-method-output.ts @@ -16,7 +16,7 @@ import { EnvironmentValue1 } from './environment-value1'; export interface GuacamoleConnectionMethodOutput { id: string; - type: string; + type: GuacamoleConnectionMethodOutput.TypeEnum; name: string; description: string; ports: RDPPortsOutput; @@ -26,4 +26,11 @@ export interface GuacamoleConnectionMethodOutput { environment: { [key: string]: EnvironmentValue1; }; sharing: ToolSessionSharingConfigurationOutput; } +export namespace GuacamoleConnectionMethodOutput { + export type TypeEnum = 'guacamole'; + export const TypeEnum = { + Guacamole: 'guacamole' as TypeEnum + }; +} + diff --git a/frontend/src/app/openapi/model/http-connection-method-input.ts b/frontend/src/app/openapi/model/http-connection-method-input.ts index d2f8ea9f79..3288c623b7 100644 --- a/frontend/src/app/openapi/model/http-connection-method-input.ts +++ b/frontend/src/app/openapi/model/http-connection-method-input.ts @@ -16,7 +16,7 @@ import { HTTPPortsInput } from './http-ports-input'; export interface HTTPConnectionMethodInput { id?: string; - type?: string; + type?: HTTPConnectionMethodInput.TypeEnum; name?: string; description?: string; ports?: HTTPPortsInput; @@ -31,4 +31,11 @@ export interface HTTPConnectionMethodInput { */ cookies?: { [key: string]: string; }; } +export namespace HTTPConnectionMethodInput { + export type TypeEnum = 'http'; + export const TypeEnum = { + Http: 'http' as TypeEnum + }; +} + diff --git a/frontend/src/app/openapi/model/http-connection-method-output.ts b/frontend/src/app/openapi/model/http-connection-method-output.ts index 075bdd218d..2264c081a0 100644 --- a/frontend/src/app/openapi/model/http-connection-method-output.ts +++ b/frontend/src/app/openapi/model/http-connection-method-output.ts @@ -16,7 +16,7 @@ import { EnvironmentValue1 } from './environment-value1'; export interface HTTPConnectionMethodOutput { id: string; - type: string; + type: HTTPConnectionMethodOutput.TypeEnum; name: string; description: string; ports: HTTPPortsOutput; @@ -31,4 +31,11 @@ export interface HTTPConnectionMethodOutput { */ cookies: { [key: string]: string; }; } +export namespace HTTPConnectionMethodOutput { + export type TypeEnum = 'http'; + export const TypeEnum = { + Http: 'http' as TypeEnum + }; +} + diff --git a/frontend/src/app/openapi/model/session.ts b/frontend/src/app/openapi/model/session.ts index cbf0c2769f..d30de9c509 100644 --- a/frontend/src/app/openapi/model/session.ts +++ b/frontend/src/app/openapi/model/session.ts @@ -9,6 +9,7 @@ + To generate a new version, run `make openapi` in the root directory of this repository. */ +import { SimpleProject } from './simple-project'; import { SessionState } from './session-state'; import { BaseUser } from './base-user'; import { SessionType } from './session-type'; @@ -33,6 +34,7 @@ export interface Session { connection_method_id: string; connection_method: ToolSessionConnectionMethod | null; shared_with: Array; + project: SimpleProject | null; } export namespace Session { } diff --git a/frontend/src/app/openapi/model/tool-session-connection-input-methods-inner.ts b/frontend/src/app/openapi/model/tool-session-connection-input-methods-inner.ts index 24f6f199cb..2cd98e1f51 100644 --- a/frontend/src/app/openapi/model/tool-session-connection-input-methods-inner.ts +++ b/frontend/src/app/openapi/model/tool-session-connection-input-methods-inner.ts @@ -18,7 +18,7 @@ import { HTTPPortsInput } from './http-ports-input'; export interface ToolSessionConnectionInputMethodsInner { id?: string; - type?: string; + type?: ToolSessionConnectionInputMethodsInner.TypeEnum; name?: string; description?: string; ports?: HTTPPortsInput; @@ -33,4 +33,12 @@ export interface ToolSessionConnectionInputMethodsInner { */ cookies?: { [key: string]: string; }; } +export namespace ToolSessionConnectionInputMethodsInner { + export type TypeEnum = 'guacamole' | 'http'; + export const TypeEnum = { + Guacamole: 'guacamole' as TypeEnum, + Http: 'http' as TypeEnum + }; +} + diff --git a/frontend/src/app/openapi/model/tool-session-connection-output-methods-inner.ts b/frontend/src/app/openapi/model/tool-session-connection-output-methods-inner.ts index e31d5889b7..aee12f9fa7 100644 --- a/frontend/src/app/openapi/model/tool-session-connection-output-methods-inner.ts +++ b/frontend/src/app/openapi/model/tool-session-connection-output-methods-inner.ts @@ -18,7 +18,7 @@ import { EnvironmentValue1 } from './environment-value1'; export interface ToolSessionConnectionOutputMethodsInner { id: string; - type: string; + type: ToolSessionConnectionOutputMethodsInner.TypeEnum; name: string; description: string; ports: HTTPPortsOutput; @@ -33,4 +33,12 @@ export interface ToolSessionConnectionOutputMethodsInner { */ cookies: { [key: string]: string; }; } +export namespace ToolSessionConnectionOutputMethodsInner { + export type TypeEnum = 'guacamole' | 'http'; + export const TypeEnum = { + Guacamole: 'guacamole' as TypeEnum, + Http: 'http' as TypeEnum + }; +} + diff --git a/frontend/src/app/sessions/service/session.service.ts b/frontend/src/app/sessions/service/session.service.ts index 5fb140a186..b0d0e51469 100644 --- a/frontend/src/app/sessions/service/session.service.ts +++ b/frontend/src/app/sessions/service/session.service.ts @@ -6,12 +6,12 @@ import { Injectable } from '@angular/core'; import { Observable, tap } from 'rxjs'; import { Session, - SessionProvisioningRequest, SessionsService, SessionConnectionInformation, FileTree, SessionPreparationState, SessionState, + PostSessionRequest, } from 'src/app/openapi'; import { SessionHistoryService } from 'src/app/sessions/user-sessions-wrapper/create-sessions/create-session-history/session-history.service'; @@ -32,33 +32,19 @@ export class SessionService { private sessionsService: SessionsService, ) {} - createSession( - toolId: number, - versionId: number, - connectionMethodId: string, - session_type: 'persistent' | 'readonly', - models: SessionProvisioningRequest[], - ): Observable { - return this.sessionsService - .requestSession({ - tool_id: toolId, - version_id: versionId, - connection_method_id: connectionMethodId, - session_type: session_type, - provisioning: models, - }) - .pipe( - tap((session) => { - if (isPersistentSession(session)) { - this.sessionHistoryService.addSessionRequestToHistory({ - toolId, - versionId, - connectionMethodId, - lastRequested: new Date(), - }); - } - }), - ); + createSession(request: PostSessionRequest): Observable { + return this.sessionsService.requestSession(request).pipe( + tap((session) => { + if (isPersistentSession(session)) { + this.sessionHistoryService.addSessionRequestToHistory({ + toolId: request.tool_id, + versionId: request.version_id, + connectionMethodId: session.connection_method_id, + lastRequested: new Date(), + }); + } + }), + ); } setConnectionInformation(connectionInfo: SessionConnectionInformation): void { diff --git a/frontend/src/app/sessions/session-overview/session-overview.stories.ts b/frontend/src/app/sessions/session-overview/session-overview.stories.ts index 2a6c56b93c..ed8b86bf7f 100644 --- a/frontend/src/app/sessions/session-overview/session-overview.stories.ts +++ b/frontend/src/app/sessions/session-overview/session-overview.stories.ts @@ -13,7 +13,6 @@ import { SessionState, } from 'src/app/openapi'; import { - createPersistentSessionWithState, mockPersistentSession, mockReadonlySession, } from 'src/storybook/session'; @@ -57,10 +56,9 @@ const sessions = [ mockPersistentSession, { ...mockReadonlySession, id: 'vjmczglcgeltbfcronujtelwx' }, { - ...createPersistentSessionWithState( - SessionPreparationState.Failed, - SessionState.Pending, - ), + ...mockPersistentSession, + preparation_state: SessionPreparationState.Failed, + state: SessionState.Pending, owner: { ...mockUser, name: 'anotherUser', diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.css b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.css deleted file mode 100644 index abd9a56cbd..0000000000 --- a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.css +++ /dev/null @@ -1,20 +0,0 @@ -/* -* SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -* SPDX-License-Identifier: Apache-2.0 -*/ - -.error { - @apply bg-error; -} - -.warning { - @apply bg-warning; -} - -.success { - @apply bg-success; -} - -.primary { - @apply bg-primary; -} diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html index e1f2184e77..0253cf352d 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html @@ -44,162 +44,50 @@

No active sessions

} @else if ((sessions | async)?.length !== 0) { - @for (session of sessions | async; track session.id) { - @let sessionState = - sessionService.beautifyState(session.preparation_state, session.state); + @for (item of sessionsGroupedByName | async | keyvalue; track item.key) { + @let project = item.value[0].project; -
-
- -
-

- {{ session.version.tool.name }} ({{ session.version.name }}) - - via - {{ session.connection_method?.name }} - -

-
- @if (isPersistentSession(session)) { - Persistent workspace session created - - } @else if (isReadonlySession(session)) { - Read-Only session created - - } + @if (project) { +
+ - -
- @if ((feedbackService.feedbackConfig$ | async)?.on_session_card) { - +
+
+
+ @for (session of item.value; track session.id) { + }
- -
-

- {{ sessionState.icon }} - {{ sessionState.text }} -

- @let remainingMinutes = minutesUntilSessionTermination(session); - @if (remainingMinutes !== null && remainingMinutes < 30) { -
- warning -
- This session will automatically terminate - - if you do not interact with it. -
-
- } - - @if (session.shared_with.length > 0 && !isSessionShared(session)) { -
- share -
- You're sharing this session with - @for ( - sharedSession of session.shared_with; - track sharedSession.user.id - ) { - - {{ sharedSession.user.name }} - - @if (!$last) { - + - } - } -
-
- } - - @if (isSessionShared(session)) { -
- share -
- This session is shared with you and was created by - {{ session.owner.name }} - -
-
- } -
-
- - - @if (!isSessionShared(session)) { - - - - - - } -
-
+ } @else { + @for (session of item.value; track session.id) { + + } + } } }
diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.ts b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.ts index 046ce3da59..a0667702ca 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.ts @@ -2,66 +2,55 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { NgClass, AsyncPipe } from '@angular/common'; +import { AsyncPipe, KeyValuePipe } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatAnchor, MatButton, MatIconButton } from '@angular/material/button'; +import { MatAnchor, MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDialog } from '@angular/material/dialog'; import { MatIcon } from '@angular/material/icon'; -import { MatTooltip } from '@angular/material/tooltip'; import { RouterLink } from '@angular/router'; -import { addMinutes, differenceInMinutes } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { BehaviorSubject, map } from 'rxjs'; import { Session } from 'src/app/openapi'; -import { OwnUserWrapperService } from 'src/app/services/user/user.service'; -import { ConnectionDialogComponent } from 'src/app/sessions/user-sessions-wrapper/active-sessions/connection-dialog/connection-dialog.component'; -import { SessionSharingDialogComponent } from 'src/app/sessions/user-sessions-wrapper/active-sessions/session-sharing-dialog/session-sharing-dialog.component'; -import { RelativeTimeComponent } from '../../../general/relative-time/relative-time.component'; -import { DeleteSessionDialogComponent } from '../../delete-session-dialog/delete-session-dialog.component'; -import { FeedbackWrapperService } from '../../feedback/feedback.service'; -import { - SessionService, - isPersistentSession, - isReadonlySession, -} from '../../service/session.service'; +import { SessionCardComponent } from 'src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.component'; +import { SessionService } from '../../service/session.service'; import { UserSessionService } from '../../service/user-session.service'; -import { FileBrowserDialogComponent } from './file-browser-dialog/file-browser-dialog.component'; @Component({ selector: 'app-active-sessions', templateUrl: './active-sessions.component.html', - styleUrls: ['./active-sessions.component.css'], imports: [ NgxSkeletonLoaderModule, MatAnchor, RouterLink, MatIcon, - NgClass, - MatButton, AsyncPipe, - MatIconButton, - MatTooltip, MatCheckboxModule, FormsModule, - RelativeTimeComponent, + KeyValuePipe, + SessionCardComponent, + MatButtonModule, ], }) export class ActiveSessionsComponent implements OnInit { - isReadonlySession = isReadonlySession; - isPersistentSession = isPersistentSession; - constructor( public sessionService: SessionService, public userSessionService: UserSessionService, - public feedbackService: FeedbackWrapperService, - private userWrapperService: OwnUserWrapperService, - private dialog: MatDialog, ) {} sessions = new BehaviorSubject(undefined); + // groupBy is not supported by our target browsers + sessionsGroupedByName = this.sessions.pipe( + map((sessions) => + sessions?.reduce((rv: GroupedSessions, x) => { + const projectID = x.project?.id ?? -1; + (rv[projectID] = rv[projectID] || []).push(x); + return rv; + }, {}), + ), + ); + ngOnInit(): void { this.userSessionService.sessions$.subscribe((sessions) => { const selectedSessionIDs = new Set( @@ -87,67 +76,10 @@ export class ActiveSessionsComponent implements OnInit { ); } - openDeletionDialog(sessions: Session[]): void { - const dialogRef = this.dialog.open(DeleteSessionDialogComponent, { - data: sessions, - }); - - dialogRef.afterClosed().subscribe((result: boolean) => { - if (result) { - if (this.feedbackService.shouldShowPostSessionPrompt()) { - this.feedbackService.showDialog( - sessions, - 'After session termination', - ); - } - - this.userSessionService.loadSessions(); - } - }); - } - - openConnectDialog(session: Session): void { - this.dialog.open(ConnectionDialogComponent, { - data: session, - }); + sessionIDsForSessions(sessions: SessionWithSelection[]) { + return sessions.map((session) => session.id); } - - openShareDialog(session: Session): void { - this.dialog.open(SessionSharingDialogComponent, { - data: session, - }); - } - - uploadFileDialog(session: Session): void { - this.dialog.open(FileBrowserDialogComponent, { data: session }); - } - - isSessionShared(session: Session): boolean { - return session.owner.id != this.userWrapperService.user?.id; - } - - minutesUntilSessionTermination(session: Session): number | null { - if (session.idle_state.available) { - if (session.idle_state.idle_for_minutes === -1) { - // session was never connected to, use creation time - return ( - session.idle_state.terminate_after_minutes - - differenceInMinutes(session.created_at, Date.now()) - ); - } else { - return ( - session.idle_state.terminate_after_minutes - - session.idle_state.idle_for_minutes! - ); - } - } else { - return null; - } - } - - protected readonly Date = Date; - protected readonly addMinutes = addMinutes; - protected readonly differenceInMinutes = differenceInMinutes; } -type SessionWithSelection = Session & { selected: boolean }; +export type SessionWithSelection = Session & { selected: boolean }; +type GroupedSessions = Record; diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.stories.ts b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.stories.ts index d53b54bb3b..663460f250 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.stories.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.stories.ts @@ -10,21 +10,12 @@ import { } from '@storybook/angular'; import MockDate from 'mockdate'; import { Observable, of } from 'rxjs'; +import { ProjectType, Session } from 'src/app/openapi'; +import { mockProject } from 'src/storybook/project'; import { - Session, - SessionPreparationState, - SessionState, -} from 'src/app/openapi'; -import { - mockFeedbackConfig, - mockFeedbackWrapperServiceProvider, -} from 'src/storybook/feedback'; -import { - createPersistentSessionWithState, mockPersistentSession, mockReadonlySession, } from 'src/storybook/session'; -import { mockHttpConnectionMethod } from 'src/storybook/tool'; import { mockOwnUserWrapperServiceProvider, mockUser, @@ -35,15 +26,18 @@ import { ActiveSessionsComponent } from './active-sessions.component'; class MockUserSessionService implements Partial { public readonly sessions$: Observable = of(undefined); - constructor(session?: Session, empty?: boolean) { - if (session !== undefined) { - this.sessions$ = of([session]); - } else if (empty === true) { - this.sessions$ = of([]); - } + constructor(sessions?: Session[]) { + this.sessions$ = of(sessions); } } +const mockUserSessionServiceProvider = (sessions?: Session[]) => { + return { + provide: UserSessionService, + useValue: new MockUserSessionService(sessions), + }; +}; + const meta: Meta = { title: 'Session Components/Active Sessions', component: ActiveSessionsComponent, @@ -67,12 +61,7 @@ export const Loading: Story = { args: {}, decorators: [ moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => new MockUserSessionService(), - }, - ], + providers: [mockUserSessionServiceProvider()], }), ], }; @@ -81,357 +70,63 @@ export const NoActiveStories: Story = { args: {}, decorators: [ moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => new MockUserSessionService(undefined, true), - }, - ], - }), - ], -}; - -export const SessionNotFoundState: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService( - createPersistentSessionWithState( - SessionPreparationState.NotFound, - SessionState.NotFound, - ), - ), - }, - ], - }), - ], -}; - -export const SessionPreparationPendingState: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService( - createPersistentSessionWithState( - SessionPreparationState.Pending, - SessionState.Pending, - ), - ), - }, - ], - }), - ], -}; - -export const SessionPreparationRunningState: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService( - createPersistentSessionWithState( - SessionPreparationState.Running, - SessionState.Pending, - ), - ), - }, - ], - }), - ], -}; - -export const SessionRunningState: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService( - createPersistentSessionWithState( - SessionPreparationState.Completed, - SessionState.Running, - ), - ), - }, - ], + providers: [mockUserSessionServiceProvider([])], }), ], }; -export const SessionTerminatingSoon: Story = { +export const FewActiveSessions: Story = { args: {}, decorators: [ moduleMetadata({ providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService({ - ...createPersistentSessionWithState( - SessionPreparationState.Completed, - SessionState.Running, - ), - idle_state: { - available: true, - terminate_after_minutes: 90, - idle_for_minutes: 80, - unavailable_reason: null, - }, - }), - }, + mockUserSessionServiceProvider([ + mockPersistentSession, + mockReadonlySession, + ]), ], }), ], }; -export const SessionTerminatedState: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService( - createPersistentSessionWithState( - SessionPreparationState.Completed, - SessionState.Terminated, - ), - ), - }, - ], - }), - ], -}; - -export const SessionPendingState: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService( - createPersistentSessionWithState( - SessionPreparationState.Completed, - SessionState.Pending, - ), - ), - }, - ], - }), - ], -}; - -export const SessionFailedState: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService( - createPersistentSessionWithState( - SessionPreparationState.Completed, - SessionState.Failed, - ), - ), - }, - ], - }), - ], -}; - -export const SessionUnknownState: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService( - createPersistentSessionWithState( - SessionPreparationState.Completed, - SessionState.Unknown, - ), - ), - }, - ], - }), - ], -}; - -export const SessionWithFeedbackEnabled: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => new MockUserSessionService(mockPersistentSession), - }, - mockFeedbackWrapperServiceProvider(mockFeedbackConfig), - ], - }), - ], -}; - -export const ReadonlySessionSuccessState: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => new MockUserSessionService(mockReadonlySession), - }, - ], - }), - ], -}; - -export const SessionSharingEnabled: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService({ - ...mockPersistentSession, - connection_method: { - ...mockHttpConnectionMethod, - sharing: { enabled: true }, - }, - }), - }, - ], - }), - ], -}; - -export const SessionSharedWithUser: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService({ - ...mockPersistentSession, - connection_method: { - ...mockHttpConnectionMethod, - sharing: { enabled: true }, - }, - shared_with: [ - { - user: { - id: 1, - name: 'user_1', - role: 'administrator', - email: null, - idp_identifier: 'user_1', - beta_tester: false, - }, - created_at: '2024-04-29T15:00:00Z', - }, - { - user: { - id: 2, - name: 'user_2', - role: 'user', - email: null, - idp_identifier: 'user_2', - beta_tester: false, - }, - created_at: '2024-04-29T15:00:00Z', - }, - ], - }), - }, - ], - }), - ], -}; -export const SessionSharedWithUserTerminatingSoon: Story = { +export const GroupedByProjectSessions: Story = { args: {}, decorators: [ moduleMetadata({ providers: [ - { - provide: UserSessionService, - useFactory: () => - new MockUserSessionService({ - ...mockPersistentSession, - connection_method: { - ...mockHttpConnectionMethod, - sharing: { enabled: true }, - }, - idle_state: { - available: true, - terminate_after_minutes: 90, - idle_for_minutes: 80, - unavailable_reason: null, - }, - shared_with: [ - { - user: { - id: 1, - name: 'user_1', - role: 'administrator', - email: null, - idp_identifier: 'user_1', - beta_tester: false, - }, - created_at: '2024-04-29T15:00:00Z', - }, - { - user: { - id: 2, - name: 'user_2', - role: 'user', - email: null, - idp_identifier: 'user_2', - beta_tester: false, - }, - created_at: '2024-04-29T15:00:00Z', - }, - ], - }), - }, + mockUserSessionServiceProvider([ + mockReadonlySession, + { ...mockPersistentSession, project: mockProject }, + { + ...mockPersistentSession, + project: mockProject, + }, + ]), ], }), ], }; -export const SharedSession: Story = { +export const GroupedByTrainingSessions: Story = { args: {}, decorators: [ moduleMetadata({ providers: [ - { - provide: UserSessionService, - useFactory: () => new MockUserSessionService(mockPersistentSession), - }, - mockOwnUserWrapperServiceProvider({ ...mockUser, id: 2 }), + mockUserSessionServiceProvider([ + mockReadonlySession, + { + ...mockPersistentSession, + project: { + ...mockProject, + type: ProjectType.Training, + name: 'mockTraining', + }, + }, + { + ...mockPersistentSession, + project: mockProject, + }, + ]), ], }), ], diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.component.html b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.component.html new file mode 100644 index 0000000000..f16569b65a --- /dev/null +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.component.html @@ -0,0 +1,145 @@ + + +@let sessionState = + sessionService.beautifyState(session.preparation_state, session.state); + +
+
+ +
+

+ {{ session.version.tool.name }} ({{ session.version.name }}) + + via + {{ session.connection_method?.name }} + +

+
+ @if (isPersistentSession(session)) { + Persistent workspace session created + + } @else if (isReadonlySession(session)) { + Read-Only session created + + } +
+
+ +
+ @if ((feedbackService.feedbackConfig$ | async)?.on_session_card) { + + } +
+
+ +
+

+ {{ sessionState.icon }} + {{ sessionState.text }} +

+ @let remainingMinutes = minutesUntilSessionTermination(session); + @if (remainingMinutes !== null && remainingMinutes < 30) { +
+ warning +
+ This session will automatically terminate + + if you do not interact with it. +
+
+ } + + @if (session.shared_with.length > 0 && !isSessionShared(session)) { +
+ share +
+ You're sharing this session with + @for ( + sharedSession of session.shared_with; + track sharedSession.user.id + ) { + + {{ sharedSession.user.name }} + + @if (!$last) { + + + } + } +
+
+ } + + @if (isSessionShared(session)) { +
+ share +
+ This session is shared with you and was created by + {{ session.owner.name }} + +
+
+ } +
+
+ + + @if (!isSessionShared(session)) { + + + + + + } +
+
diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.component.ts b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.component.ts new file mode 100644 index 0000000000..ab0d081fd2 --- /dev/null +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.component.ts @@ -0,0 +1,143 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { NgClass, AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatButton, MatIconButton } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialog } from '@angular/material/dialog'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; +import { RouterLink } from '@angular/router'; +import { addMinutes, differenceInMinutes } from 'date-fns'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { RelativeTimeComponent } from 'src/app/general/relative-time/relative-time.component'; +import { Session } from 'src/app/openapi'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { DeleteSessionDialogComponent } from 'src/app/sessions/delete-session-dialog/delete-session-dialog.component'; +import { FeedbackWrapperService } from 'src/app/sessions/feedback/feedback.service'; +import { + isPersistentSession, + isReadonlySession, + SessionService, +} from 'src/app/sessions/service/session.service'; +import { UserSessionService } from 'src/app/sessions/service/user-session.service'; +import { SessionWithSelection } from 'src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component'; +import { ConnectionDialogComponent } from 'src/app/sessions/user-sessions-wrapper/active-sessions/connection-dialog/connection-dialog.component'; +import { FileBrowserDialogComponent } from 'src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.component'; +import { SessionSharingDialogComponent } from 'src/app/sessions/user-sessions-wrapper/active-sessions/session-sharing-dialog/session-sharing-dialog.component'; + +@Component({ + selector: 'app-session-card', + standalone: true, + imports: [ + NgxSkeletonLoaderModule, + RouterLink, + MatIcon, + NgClass, + MatButton, + AsyncPipe, + MatIconButton, + MatTooltip, + MatCheckboxModule, + FormsModule, + RelativeTimeComponent, + ], + templateUrl: './session-card.component.html', + styles: ` + .error { + @apply bg-error; + } + + .warning { + @apply bg-warning; + } + + .success { + @apply bg-success; + } + + .primary { + @apply bg-primary; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SessionCardComponent { + @Input({ required: true }) session!: SessionWithSelection; + + isReadonlySession = isReadonlySession; + isPersistentSession = isPersistentSession; + + constructor( + public sessionService: SessionService, + public feedbackService: FeedbackWrapperService, + private userSessionService: UserSessionService, + private userWrapperService: OwnUserWrapperService, + private dialog: MatDialog, + ) {} + + uploadFileDialog(session: Session): void { + this.dialog.open(FileBrowserDialogComponent, { data: session }); + } + + openDeletionDialog(sessions: Session[]): void { + const dialogRef = this.dialog.open(DeleteSessionDialogComponent, { + data: sessions, + }); + + dialogRef.afterClosed().subscribe((result: boolean) => { + if (result) { + if (this.feedbackService.shouldShowPostSessionPrompt()) { + this.feedbackService.showDialog( + sessions, + 'After session termination', + ); + } + + this.userSessionService.loadSessions(); + } + }); + } + + openShareDialog(session: Session): void { + this.dialog.open(SessionSharingDialogComponent, { + data: session, + }); + } + + isSessionShared(session: Session): boolean { + return session.owner.id != this.userWrapperService.user?.id; + } + + openConnectDialog(session: Session): void { + this.dialog.open(ConnectionDialogComponent, { + data: session, + }); + } + + minutesUntilSessionTermination(session: Session): number | null { + if (session.idle_state.available) { + if (session.idle_state.idle_for_minutes === -1) { + // session was never connected to, use creation time + return ( + session.idle_state.terminate_after_minutes - + differenceInMinutes(session.created_at, Date.now()) + ); + } else { + return ( + session.idle_state.terminate_after_minutes - + session.idle_state.idle_for_minutes! + ); + } + } else { + return null; + } + } + + protected readonly Date = Date; + protected readonly addMinutes = addMinutes; + protected readonly differenceInMinutes = differenceInMinutes; +} diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.stories.ts b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.stories.ts new file mode 100644 index 0000000000..8c02f19d3d --- /dev/null +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/session-card/session-card.stories.ts @@ -0,0 +1,279 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { + Meta, + StoryObj, + componentWrapperDecorator, + moduleMetadata, +} from '@storybook/angular'; +import MockDate from 'mockdate'; +import { SessionPreparationState, SessionState } from 'src/app/openapi'; +import { + mockFeedbackConfig, + mockFeedbackWrapperServiceProvider, +} from 'src/storybook/feedback'; +import { + mockPersistentSession, + mockReadonlySession, +} from 'src/storybook/session'; +import { mockHttpConnectionMethod } from 'src/storybook/tool'; +import { + mockOwnUserWrapperServiceProvider, + mockUser, +} from 'src/storybook/user'; +import { SessionCardComponent } from './session-card.component'; + +const meta: Meta = { + title: 'Session Components/Session Card', + component: SessionCardComponent, + decorators: [ + moduleMetadata({ + providers: [mockOwnUserWrapperServiceProvider(mockUser)], + }), + componentWrapperDecorator( + (story) => `
${story}
`, + ), + ], + beforeEach: () => { + MockDate.set(new Date('2024-05-01')); + }, +}; + +export default meta; +type Story = StoryObj; + +export const SessionNotFoundState: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + preparation_state: SessionPreparationState.NotFound, + state: SessionState.NotFound, + }, + }, +}; + +export const SessionPreparationPendingState: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + preparation_state: SessionPreparationState.Pending, + state: SessionState.Pending, + }, + }, +}; + +export const SessionPreparationRunningState: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + preparation_state: SessionPreparationState.Running, + state: SessionState.Pending, + }, + }, +}; + +export const SessionRunningState: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + preparation_state: SessionPreparationState.Completed, + state: SessionState.Running, + }, + }, +}; + +export const SessionTerminatingSoon: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + preparation_state: SessionPreparationState.Completed, + state: SessionState.Running, + idle_state: { + available: true, + terminate_after_minutes: 90, + idle_for_minutes: 80, + unavailable_reason: null, + }, + }, + }, +}; + +export const SessionTerminatedState: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + preparation_state: SessionPreparationState.Completed, + state: SessionState.Terminated, + }, + }, +}; + +export const SessionPendingState: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + preparation_state: SessionPreparationState.Completed, + state: SessionState.Pending, + }, + }, +}; + +export const SessionFailedState: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + preparation_state: SessionPreparationState.Completed, + state: SessionState.Failed, + }, + }, +}; + +export const SessionUnknownState: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + preparation_state: SessionPreparationState.Completed, + state: SessionState.Unknown, + }, + }, +}; + +export const SessionWithFeedbackEnabled: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + }, + }, + decorators: [ + moduleMetadata({ + providers: [mockFeedbackWrapperServiceProvider(mockFeedbackConfig)], + }), + ], +}; + +export const ReadonlySessionSuccessState: Story = { + args: { + session: { + selected: false, + ...mockReadonlySession, + }, + }, +}; + +export const SessionSharingEnabled: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + connection_method: { + ...mockHttpConnectionMethod, + sharing: { enabled: true }, + }, + }, + }, +}; + +export const SessionSharedWithUser: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + connection_method: { + ...mockHttpConnectionMethod, + sharing: { enabled: true }, + }, + shared_with: [ + { + user: { + id: 1, + name: 'user_1', + role: 'administrator', + email: null, + idp_identifier: 'user_1', + beta_tester: false, + }, + created_at: '2024-04-29T15:00:00Z', + }, + { + user: { + id: 2, + name: 'user_2', + role: 'user', + email: null, + idp_identifier: 'user_2', + beta_tester: false, + }, + created_at: '2024-04-29T15:00:00Z', + }, + ], + }, + }, +}; +export const SessionSharedWithUserTerminatingSoon: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + connection_method: { + ...mockHttpConnectionMethod, + sharing: { enabled: true }, + }, + idle_state: { + available: true, + terminate_after_minutes: 90, + idle_for_minutes: 80, + unavailable_reason: null, + }, + shared_with: [ + { + user: { + id: 1, + name: 'user_1', + role: 'administrator', + email: null, + idp_identifier: 'user_1', + beta_tester: false, + }, + created_at: '2024-04-29T15:00:00Z', + }, + { + user: { + id: 2, + name: 'user_2', + role: 'user', + email: null, + idp_identifier: 'user_2', + beta_tester: false, + }, + created_at: '2024-04-29T15:00:00Z', + }, + ], + }, + }, +}; + +export const SharedSession: Story = { + args: { + session: { + selected: false, + ...mockPersistentSession, + }, + }, + decorators: [ + moduleMetadata({ + providers: [mockOwnUserWrapperServiceProvider({ ...mockUser, id: 2 })], + }), + ], +}; diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.ts index 8677383b96..44e59eb62c 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.ts @@ -19,7 +19,7 @@ import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; import { MatSelect } from '@angular/material/select'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { map, Observable } from 'rxjs'; -import { Session, Tool, ToolVersion } from 'src/app/openapi'; +import { Session, SessionType, Tool, ToolVersion } from 'src/app/openapi'; import { SessionService } from 'src/app/sessions/service/session.service'; import { UserSessionService } from 'src/app/sessions/service/user-session.service'; import { @@ -93,13 +93,13 @@ export class CreatePersistentSessionComponent implements OnInit { this.requestInProgress = true; this.sessionService - .createSession( - this.toolSelectionForm.controls.toolId.value!, - this.toolSelectionForm.controls.versionId.value!, - this.toolSelectionForm.controls.connectionMethodId.value!, - 'persistent', - [], - ) + .createSession({ + tool_id: this.toolSelectionForm.controls.toolId.value!, + version_id: this.toolSelectionForm.controls.versionId.value!, + connection_method_id: + this.toolSelectionForm.controls.connectionMethodId.value!, + session_type: SessionType.Persistent, + }) .subscribe({ next: () => { this.userSessionService.loadSessions(); diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog/create-readonly-session-dialog.component.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog/create-readonly-session-dialog.component.ts index 9f09085fc2..92ed51d145 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog/create-readonly-session-dialog.component.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog/create-readonly-session-dialog.component.ts @@ -18,7 +18,7 @@ import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; import { Router } from '@angular/router'; import { UntilDestroy } from '@ngneat/until-destroy'; import { ToastService } from 'src/app/helpers/toast/toast.service'; -import { Tool, ToolModel, ToolVersion } from 'src/app/openapi'; +import { SessionType, Tool, ToolModel, ToolVersion } from 'src/app/openapi'; import { getPrimaryGitModel } from 'src/app/projects/models/service/model.service'; import { SessionService } from 'src/app/sessions/service/session.service'; import { @@ -112,12 +112,13 @@ export class CreateReadonlySessionDialogComponent implements OnInit { } this.sessionService - .createSession( - this.data.tool.id, - this.data.toolVersion.id, - this.form.controls.connectionMethodId.value!, - 'readonly', - included.map((m) => { + .createSession({ + tool_id: this.data.tool.id, + version_id: this.data.toolVersion.id, + connection_method_id: this.form.controls.connectionMethodId.value!, + session_type: SessionType.Readonly, + project_slug: this.data.projectSlug, + provisioning: included.map((m) => { return { project_slug: this.data.projectSlug, model_slug: m.model.slug, @@ -126,7 +127,7 @@ export class CreateReadonlySessionDialogComponent implements OnInit { deep_clone: m.deepClone, }; }), - ) + }) .subscribe({ next: (session) => { this.dialog.close(); diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-session-history/create-session-history.component.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-session-history/create-session-history.component.ts index 74a3925edb..8f8b965191 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-session-history/create-session-history.component.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-session-history/create-session-history.component.ts @@ -7,7 +7,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { Subscription, filter, take } from 'rxjs'; -import { Tool, ToolVersion } from 'src/app/openapi'; +import { SessionType, Tool, ToolVersion } from 'src/app/openapi'; import { SessionService } from 'src/app/sessions/service/session.service'; import { SessionHistoryService, @@ -117,13 +117,12 @@ export class CreateSessionHistoryComponent implements OnInit, OnDestroy { requestSession(session: ResolvedSessionRequestHistory) { session.loading = true; this.sessionService - .createSession( - session.tool.id, - session.version.id, - session.connectionMethod.id!, - 'persistent', - [], - ) + .createSession({ + tool_id: session.tool.id, + version_id: session.version.id, + connection_method_id: session.connectionMethod.id!, + session_type: SessionType.Persistent, + }) .subscribe({ next: () => { session.loading = false; diff --git a/frontend/src/storybook/session.ts b/frontend/src/storybook/session.ts index 78f9d3f7af..2c8cfeaae4 100644 --- a/frontend/src/storybook/session.ts +++ b/frontend/src/storybook/session.ts @@ -6,41 +6,33 @@ import { Session, SessionPreparationState, SessionState, + SessionType, } from 'src/app/openapi'; import { mockHttpConnectionMethod, mockToolVersionWithTool } from './tool'; import { mockUser } from './user'; -export const mockPersistentSession = createPersistentSessionWithState( - SessionPreparationState.Completed, - SessionState.Running, -); +export const mockPersistentSession = { + id: 'vfurvsrldxfwwsqdiqvnufonh', + created_at: '2024-04-29T15:00:00Z', + idle_state: { + available: true, + terminate_after_minutes: 90, + idle_for_minutes: 30, + unavailable_reason: null, + }, + type: SessionType.Persistent, + version: mockToolVersionWithTool, + preparation_state: SessionPreparationState.Completed, + state: SessionState.Running, + owner: mockUser, + connection_method: mockHttpConnectionMethod, + warnings: [], + connection_method_id: 'default', + shared_with: [], + project: null, +}; export const mockReadonlySession: Readonly = { ...mockPersistentSession, - type: 'readonly', + type: SessionType.Readonly, }; - -export function createPersistentSessionWithState( - preparationState: SessionPreparationState, - state: SessionState, -): Session { - return { - id: 'vfurvsrldxfwwsqdiqvnufonh', - created_at: '2024-04-29T15:00:00Z', - idle_state: { - available: true, - terminate_after_minutes: 90, - idle_for_minutes: 30, - unavailable_reason: null, - }, - type: 'persistent', - version: mockToolVersionWithTool, - preparation_state: preparationState, - state: state, - owner: mockUser, - connection_method: mockHttpConnectionMethod, - warnings: [], - connection_method_id: 'default', - shared_with: [], - }; -}