From 86f9de9cb169b8c7f726d824ab37c8b97fdebf66 Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Sat, 21 Sep 2024 18:08:38 +0200 Subject: [PATCH] fix: Display long usernames correctly in user overview - Rename the existing userService to ownUserWrapperService - Create a new userWrapperService and userWrapperComponent, which keeps track of the list of registered users and the currently selected user via path parameters - Migrate the components to use the new userWrapperService - Remove bottom padding for project user and user components - Improve responsive design --- .../capellacollab/projects/users/models.py | 2 +- backend/capellacollab/users/models.py | 2 +- .../frontend/responsive-design/mobile-view.md | 3 - frontend/src/app/app-routing.module.ts | 31 ++-- .../auth-redirect/auth-redirect.component.ts | 4 +- .../app/general/header/header.component.ts | 4 +- .../src/app/general/header/header.stories.ts | 15 +- .../nav-bar-menu/nav-bar-menu.component.ts | 4 +- .../app/openapi/model/project-user-role.ts | 4 +- frontend/src/app/openapi/model/role.ts | 6 +- .../pipeline-deletion-dialog.component.ts | 4 +- .../pipeline-deletion.dialog.stories.ts | 11 +- .../model-diagram-code-block.component.ts | 4 +- .../model-detail/model-detail.component.html | 15 +- .../model-detail/model-detail.component.ts | 4 +- .../model-detail/model-detail.stories.ts | 18 ++- .../model-source/choose-source.component.ts | 4 +- .../model-overview.component.html | 2 +- .../model-overview.component.ts | 4 +- .../model-overview/model-overview.stories.ts | 11 +- .../project-user-settings.component.html | 4 +- .../project-user-settings.component.ts | 9 +- .../project-user-settings.stories.ts | 2 +- .../project-overview.component.html | 4 +- .../project-wrapper.component.css | 4 - .../project-wrapper.component.ts | 1 - .../src/app/services/user/user.service.ts | 2 +- .../sessions/service/user-session.service.ts | 4 +- .../active-sessions.component.ts | 4 +- .../active-sessions.stories.ts | 13 +- .../connection-dialog.component.ts | 4 +- .../connection-dialog.stories.ts | 8 +- .../user-settings.component.html | 129 ----------------- .../t4c-settings/t4c-settings.component.html | 4 +- .../src/app/settings/settings.component.html | 2 +- .../user-settings.component.html | 132 ++++++++++++++++++ .../user-settings/user-settings.component.ts | 97 +++++++------ .../user-settings/user-settings.stories.ts | 69 +++++++++ .../user-wrapper/user-wrapper.component.ts | 62 ++++++++ .../user-wrapper/user-wrapper.service.ts | 40 ++++++ .../common-projects.component.html | 66 ++++----- .../common-projects.component.ts | 45 +++--- .../common-projects.stories.ts | 18 ++- .../user-information.component.html | 3 +- .../user-information.component.ts | 52 +++---- .../user-information.stories.ts | 21 +-- .../user-workspaces.component.html | 14 +- .../user-workspaces.component.ts | 84 ++++++----- .../user-workspaces.stories.ts | 79 ++++++++--- .../users-profile.component.html | 48 ++++--- .../users-profile/users-profile.component.ts | 41 ++---- frontend/src/storybook/user.ts | 24 +++- frontend/src/styles.css | 7 +- 53 files changed, 756 insertions(+), 492 deletions(-) delete mode 100644 frontend/src/app/projects/project-wrapper/project-wrapper.component.css delete mode 100644 frontend/src/app/settings/core/user-settings/user-settings.component.html create mode 100644 frontend/src/app/users/user-settings/user-settings.component.html rename frontend/src/app/{settings/core => users}/user-settings/user-settings.component.ts (73%) create mode 100644 frontend/src/app/users/user-settings/user-settings.stories.ts create mode 100644 frontend/src/app/users/user-wrapper/user-wrapper.component.ts create mode 100644 frontend/src/app/users/user-wrapper/user-wrapper.service.ts diff --git a/backend/capellacollab/projects/users/models.py b/backend/capellacollab/projects/users/models.py index fd2ccd743..f6935fc0d 100644 --- a/backend/capellacollab/projects/users/models.py +++ b/backend/capellacollab/projects/users/models.py @@ -19,8 +19,8 @@ class ProjectUserRole(enum.Enum): - USER = "user" MANAGER = "manager" + USER = "user" ADMIN = "administrator" diff --git a/backend/capellacollab/users/models.py b/backend/capellacollab/users/models.py index 4a9c54b8a..24660736d 100644 --- a/backend/capellacollab/users/models.py +++ b/backend/capellacollab/users/models.py @@ -21,8 +21,8 @@ class Role(str, enum.Enum): - USER = "user" ADMIN = "administrator" + USER = "user" class BaseUser(core_pydantic.BaseModel): diff --git a/docs/docs/development/frontend/responsive-design/mobile-view.md b/docs/docs/development/frontend/responsive-design/mobile-view.md index 08630db7a..8f6752947 100644 --- a/docs/docs/development/frontend/responsive-design/mobile-view.md +++ b/docs/docs/development/frontend/responsive-design/mobile-view.md @@ -37,9 +37,6 @@ uses flexbox on devices larger than `768px`. significant effort. - **Leverage Flexboxes**: Ideally, utilize `class="flex flex-wrap"` to ensure content adjusts appropriately on various screens. -- **Set Boundaries**: Implement `max-w-[90vw]` to avoid content spilling out - of view. You can change the `class="90vw"` to another value, depending on - your needs. - **Centering Content**: When aesthetics and usability align, consider vertically centering elements via `class="flex justify-center"` on the parent element. diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 59a2b5dc7..6a6166a31 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -17,6 +17,7 @@ import { ConfigurationSettingsComponent } from 'src/app/settings/core/configurat import { PipelinesOverviewComponent } from 'src/app/settings/core/pipelines-overview/pipelines-overview.component'; import { CreateToolComponent } from 'src/app/settings/core/tools-settings/create-tool/create-tool.component'; import { BasicAuthTokenComponent } from 'src/app/users/basic-auth-token/basic-auth-token.component'; +import { UserWrapperComponent } from 'src/app/users/user-wrapper/user-wrapper.component'; import { UsersProfileComponent } from 'src/app/users/users-profile/users-profile.component'; import { EventsComponent } from './events/events.component'; import { AuthRedirectComponent } from './general/auth/auth-redirect/auth-redirect.component'; @@ -39,7 +40,6 @@ import { SessionsComponent } from './sessions/sessions.component'; import { AlertSettingsComponent } from './settings/core/alert-settings/alert-settings.component'; import { ToolDetailsComponent } from './settings/core/tools-settings/tool-details/tool-details.component'; import { ToolsSettingsComponent } from './settings/core/tools-settings/tools-settings.component'; -import { UserSettingsComponent } from './settings/core/user-settings/user-settings.component'; import { PureVariantsComponent } from './settings/integrations/pure-variants/pure-variants.component'; import { EditGitSettingsComponent } from './settings/modelsources/git-settings/edit-git-settings/edit-git-settings.component'; import { GitSettingsComponent } from './settings/modelsources/git-settings/git-settings.component'; @@ -47,6 +47,7 @@ import { EditT4CInstanceComponent } from './settings/modelsources/t4c-settings/e import { T4CSettingsWrapperComponent } from './settings/modelsources/t4c-settings/t4c-settings-wrapper/t4c-settings-wrapper.component'; import { T4CSettingsComponent } from './settings/modelsources/t4c-settings/t4c-settings.component'; import { SettingsComponent } from './settings/settings.component'; +import { UserSettingsComponent } from './users/user-settings/user-settings.component'; export const routes: Routes = [ { @@ -454,19 +455,29 @@ export const routes: Routes = [ data: { breadcrumb: 'Events' }, component: EventsComponent, }, + { + path: 'users', + data: { breadcrumb: 'Users' }, + component: UserSettingsComponent, + }, { path: 'user', - data: { breadcrumb: 'User' }, + data: { breadcrumb: 'Users', redirect: '/users' }, children: [ { - path: '', - data: { breadcrumb: undefined }, - component: UserSettingsComponent, - }, - { - path: ':userId', - data: { breadcrumb: (data: Data) => data?.user?.name || 'User' }, - component: UsersProfileComponent, + path: ':user', + data: { + breadcrumb: (data: Data) => data.user?.name, + redirect: (data: Data) => `/user/${data.user?.id}`, + }, + component: UserWrapperComponent, + children: [ + { + path: '', + data: { breadcrumb: undefined }, + component: UsersProfileComponent, + }, + ], }, ], }, diff --git a/frontend/src/app/general/auth/auth-redirect/auth-redirect.component.ts b/frontend/src/app/general/auth/auth-redirect/auth-redirect.component.ts index cdc7f3926..c6326f86c 100644 --- a/frontend/src/app/general/auth/auth-redirect/auth-redirect.component.ts +++ b/frontend/src/app/general/auth/auth-redirect/auth-redirect.component.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ToastService } from 'src/app/helpers/toast/toast.service'; import { AuthenticationService } from 'src/app/openapi'; import { AuthenticationWrapperService } from 'src/app/services/auth/auth.service'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; @Component({ selector: 'app-auth-redirect', @@ -20,7 +20,7 @@ export class AuthRedirectComponent implements OnInit { private toastService: ToastService, private authService: AuthenticationWrapperService, private authenticationService: AuthenticationService, - private userService: UserWrapperService, + private userService: OwnUserWrapperService, private router: Router, ) {} diff --git a/frontend/src/app/general/header/header.component.ts b/frontend/src/app/general/header/header.component.ts index 95ffadb35..ec9cd35b7 100644 --- a/frontend/src/app/general/header/header.component.ts +++ b/frontend/src/app/general/header/header.component.ts @@ -10,7 +10,7 @@ import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { RouterLink } from '@angular/router'; import { NavBarService } from 'src/app/general/nav-bar/nav-bar.service'; import { AuthenticationWrapperService } from '../../services/auth/auth.service'; -import { UserWrapperService } from '../../services/user/user.service'; +import { OwnUserWrapperService } from '../../services/user/user.service'; import { BreadcrumbsComponent } from '../breadcrumbs/breadcrumbs.component'; @Component({ @@ -33,7 +33,7 @@ import { BreadcrumbsComponent } from '../breadcrumbs/breadcrumbs.component'; export class HeaderComponent { constructor( public authService: AuthenticationWrapperService, - public userService: UserWrapperService, + public userService: OwnUserWrapperService, public navBarService: NavBarService, ) {} } diff --git a/frontend/src/app/general/header/header.stories.ts b/frontend/src/app/general/header/header.stories.ts index 444b8a6b7..bde591362 100644 --- a/frontend/src/app/general/header/header.stories.ts +++ b/frontend/src/app/general/header/header.stories.ts @@ -4,8 +4,8 @@ */ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { of } from 'rxjs'; -import { UserWrapperService } from 'src/app/services/user/user.service'; -import { mockUser, MockUserService } from 'src/storybook/user'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { mockUser, MockOwnUserWrapperService } from 'src/storybook/user'; import { NavBarItem, NavBarService } from '../nav-bar/nav-bar.service'; import { HeaderComponent } from './header.component'; @@ -62,8 +62,8 @@ export const NormalUser: Story = { moduleMetadata({ providers: [ { - provide: UserWrapperService, - useFactory: () => new MockUserService(mockUser), + provide: OwnUserWrapperService, + useFactory: () => new MockOwnUserWrapperService(mockUser), }, { provide: NavBarService, @@ -83,9 +83,12 @@ export const Administrator: Story = { moduleMetadata({ providers: [ { - provide: UserWrapperService, + provide: OwnUserWrapperService, useFactory: () => - new MockUserService({ ...mockUser, role: 'administrator' }), + new MockOwnUserWrapperService({ + ...mockUser, + role: 'administrator', + }), }, { provide: NavBarService, diff --git a/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.ts b/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.ts index b961fb79c..48fac7b3a 100644 --- a/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.ts +++ b/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.ts @@ -10,7 +10,7 @@ import { MatList, MatListItem } from '@angular/material/list'; import { RouterLink } from '@angular/router'; import { NavBarService } from 'src/app/general/nav-bar/nav-bar.service'; import { AuthenticationWrapperService } from 'src/app/services/auth/auth.service'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; @Component({ selector: 'app-nav-bar-menu', @@ -23,6 +23,6 @@ export class NavBarMenuComponent { constructor( public authService: AuthenticationWrapperService, public navBarService: NavBarService, - public userService: UserWrapperService, + public userService: OwnUserWrapperService, ) {} } diff --git a/frontend/src/app/openapi/model/project-user-role.ts b/frontend/src/app/openapi/model/project-user-role.ts index ccaa1bb7a..7e169ff66 100644 --- a/frontend/src/app/openapi/model/project-user-role.ts +++ b/frontend/src/app/openapi/model/project-user-role.ts @@ -11,11 +11,11 @@ -export type ProjectUserRole = 'user' | 'manager' | 'administrator'; +export type ProjectUserRole = 'manager' | 'user' | 'administrator'; export const ProjectUserRole = { - User: 'user' as ProjectUserRole, Manager: 'manager' as ProjectUserRole, + User: 'user' as ProjectUserRole, Administrator: 'administrator' as ProjectUserRole }; diff --git a/frontend/src/app/openapi/model/role.ts b/frontend/src/app/openapi/model/role.ts index 60b6c90b4..8b976c342 100644 --- a/frontend/src/app/openapi/model/role.ts +++ b/frontend/src/app/openapi/model/role.ts @@ -11,10 +11,10 @@ -export type Role = 'user' | 'administrator'; +export type Role = 'administrator' | 'user'; export const Role = { - User: 'user' as Role, - Administrator: 'administrator' as Role + Administrator: 'administrator' as Role, + User: 'user' as Role }; diff --git a/frontend/src/app/projects/models/backup-settings/pipeline-deletion-dialog/pipeline-deletion-dialog.component.ts b/frontend/src/app/projects/models/backup-settings/pipeline-deletion-dialog/pipeline-deletion-dialog.component.ts index 12451dd71..c54241e45 100644 --- a/frontend/src/app/projects/models/backup-settings/pipeline-deletion-dialog/pipeline-deletion-dialog.component.ts +++ b/frontend/src/app/projects/models/backup-settings/pipeline-deletion-dialog/pipeline-deletion-dialog.component.ts @@ -15,7 +15,7 @@ import { BehaviorSubject } from 'rxjs'; import { ToastService } from 'src/app/helpers/toast/toast.service'; import { Backup, ProjectsModelsBackupsService } from 'src/app/openapi'; import { PipelineWrapperService } from 'src/app/projects/models/backup-settings/service/pipeline.service'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; @Component({ selector: 'app-pipeline-deletion-dialog', @@ -41,7 +41,7 @@ export class PipelineDeletionDialogComponent { loading = new BehaviorSubject(false); constructor( - public userService: UserWrapperService, + public userService: OwnUserWrapperService, private dialogRef: DialogRef, private toastService: ToastService, private pipelinesService: ProjectsModelsBackupsService, diff --git a/frontend/src/app/projects/models/backup-settings/pipeline-deletion-dialog/pipeline-deletion.dialog.stories.ts b/frontend/src/app/projects/models/backup-settings/pipeline-deletion-dialog/pipeline-deletion.dialog.stories.ts index 5f837417b..0712bb355 100644 --- a/frontend/src/app/projects/models/backup-settings/pipeline-deletion-dialog/pipeline-deletion.dialog.stories.ts +++ b/frontend/src/app/projects/models/backup-settings/pipeline-deletion-dialog/pipeline-deletion.dialog.stories.ts @@ -5,10 +5,10 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { BehaviorSubject } from 'rxjs'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; import { mockBackup } from 'src/storybook/backups'; import { dialogWrapper } from 'src/storybook/decorators'; -import { mockUser, MockUserService } from 'src/storybook/user'; +import { mockUser, MockOwnUserWrapperService } from 'src/storybook/user'; import { PipelineDeletionDialogComponent } from './pipeline-deletion-dialog.component'; const meta: Meta = { @@ -48,9 +48,12 @@ export const AsAdmin: Story = { moduleMetadata({ providers: [ { - provide: UserWrapperService, + provide: OwnUserWrapperService, useFactory: () => - new MockUserService({ ...mockUser, role: 'administrator' }), + new MockOwnUserWrapperService({ + ...mockUser, + role: 'administrator', + }), }, ], }), diff --git a/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-code-block/model-diagram-code-block.component.ts b/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-code-block/model-diagram-code-block.component.ts index 76cbf4adb..f3c2c9df7 100644 --- a/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-code-block/model-diagram-code-block.component.ts +++ b/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-code-block/model-diagram-code-block.component.ts @@ -25,7 +25,7 @@ import { MetadataService } from 'src/app/general/metadata/metadata.service'; import { ToastService } from 'src/app/helpers/toast/toast.service'; import { Metadata, Project, ToolModel } from 'src/app/openapi'; import { getPrimaryGitModel } from 'src/app/projects/models/service/model.service'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; import { TokenService } from 'src/app/users/basic-auth-service/basic-auth-token.service'; @Component({ @@ -53,7 +53,7 @@ export class ModelDiagramCodeBlockComponent implements OnInit, AfterViewInit { constructor( private metadataService: MetadataService, - private userService: UserWrapperService, + private userService: OwnUserWrapperService, private tokenService: TokenService, private toastService: ToastService, ) {} diff --git a/frontend/src/app/projects/models/model-detail/model-detail.component.html b/frontend/src/app/projects/models/model-detail/model-detail.component.html index c6357b6e1..874d63aa1 100644 --- a/frontend/src/app/projects/models/model-detail/model-detail.component.html +++ b/frontend/src/app/projects/models/model-detail/model-detail.component.html @@ -5,7 +5,7 @@

Git repositories

- +
Use existing repository
@@ -23,7 +23,10 @@

Git repositories

> } @else { @for (gitModel of gitModelService.gitModels$ | async; track gitModel.id) { -
+
Integration {{ gitModel.id }} @@ -59,7 +62,10 @@

Git repositories

) {

TeamForCapella repositories

- +
Use existing repository
@@ -69,7 +75,7 @@

TeamForCapella repositories

- +
Create new repository
@@ -89,6 +95,7 @@

TeamForCapella repositories

Integration {{ t4cModel.id }}
diff --git a/frontend/src/app/projects/models/model-detail/model-detail.component.ts b/frontend/src/app/projects/models/model-detail/model-detail.component.ts index bcb71e26b..9c4613e36 100644 --- a/frontend/src/app/projects/models/model-detail/model-detail.component.ts +++ b/frontend/src/app/projects/models/model-detail/model-detail.component.ts @@ -12,7 +12,7 @@ import { combineLatest, filter } from 'rxjs'; import { T4CModelService } from 'src/app/projects/models/model-source/t4c/service/t4c-model.service'; import { ModelWrapperService } from 'src/app/projects/models/service/model.service'; import { GitModelService } from 'src/app/projects/project-detail/model-overview/model-detail/git-model.service'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; import { MatIconComponent } from '../../../helpers/mat-icon/mat-icon.component'; import { MatCardOverviewSkeletonLoaderComponent } from '../../../helpers/skeleton-loaders/mat-card-overview-skeleton-loader/mat-card-overview-skeleton-loader.component'; import { ProjectWrapperService } from '../../service/project.service'; @@ -39,7 +39,7 @@ export class ModelDetailComponent implements OnInit, OnDestroy { public modelService: ModelWrapperService, public gitModelService: GitModelService, public t4cModelService: T4CModelService, - public userService: UserWrapperService, + public userService: OwnUserWrapperService, ) {} ngOnInit(): void { diff --git a/frontend/src/app/projects/models/model-detail/model-detail.stories.ts b/frontend/src/app/projects/models/model-detail/model-detail.stories.ts index e84de6f88..61863e149 100644 --- a/frontend/src/app/projects/models/model-detail/model-detail.stories.ts +++ b/frontend/src/app/projects/models/model-detail/model-detail.stories.ts @@ -6,11 +6,11 @@ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { T4CModelService } from 'src/app/projects/models/model-source/t4c/service/t4c-model.service'; import { ModelWrapperService } from 'src/app/projects/models/service/model.service'; import { GitModelService } from 'src/app/projects/project-detail/model-overview/model-detail/git-model.service'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; import { MockGitModelService, mockPrimaryGitModel } from 'src/storybook/git'; import { mockModel, MockModelWrapperService } from 'src/storybook/model'; import { mockT4CModel, MockT4CModelService } from 'src/storybook/t4c'; -import { mockUser, MockUserService } from 'src/storybook/user'; +import { mockUser, MockOwnUserWrapperService } from 'src/storybook/user'; import { ModelDetailComponent } from './model-detail.component'; const meta: Meta = { @@ -48,9 +48,12 @@ export const LoadingAsAdmin: Story = { useFactory: () => new MockModelWrapperService(mockModel, []), }, { - provide: UserWrapperService, + provide: OwnUserWrapperService, useFactory: () => - new MockUserService({ ...mockUser, role: 'administrator' }), + new MockOwnUserWrapperService({ + ...mockUser, + role: 'administrator', + }), }, ], }), @@ -67,9 +70,12 @@ export const WithRepositoryAsAdmin: Story = { useFactory: () => new MockModelWrapperService(mockModel, []), }, { - provide: UserWrapperService, + provide: OwnUserWrapperService, useFactory: () => - new MockUserService({ ...mockUser, role: 'administrator' }), + new MockOwnUserWrapperService({ + ...mockUser, + role: 'administrator', + }), }, { provide: GitModelService, diff --git a/frontend/src/app/projects/models/model-source/choose-source.component.ts b/frontend/src/app/projects/models/model-source/choose-source.component.ts index 21de4722d..5f0f44e99 100644 --- a/frontend/src/app/projects/models/model-source/choose-source.component.ts +++ b/frontend/src/app/projects/models/model-source/choose-source.component.ts @@ -8,7 +8,7 @@ import { MatAnchor } from '@angular/material/button'; import { MatDivider } from '@angular/material/divider'; import { MatIcon } from '@angular/material/icon'; import { ModelWrapperService } from 'src/app/projects/models/service/model.service'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; import { ProjectWrapperService } from '../../service/project.service'; @Component({ @@ -24,6 +24,6 @@ export class ChooseSourceComponent { constructor( public projectService: ProjectWrapperService, public modelService: ModelWrapperService, - public userService: UserWrapperService, + public userService: OwnUserWrapperService, ) {} } diff --git a/frontend/src/app/projects/project-detail/model-overview/model-overview.component.html b/frontend/src/app/projects/project-detail/model-overview/model-overview.component.html index ea3183d4c..a082da6d5 100644 --- a/frontend/src/app/projects/project-detail/model-overview/model-overview.component.html +++ b/frontend/src/app/projects/project-detail/model-overview/model-overview.component.html @@ -62,7 +62,7 @@

Models

} @for (model of modelService.models$ | async; track model.id) {
= { @@ -95,9 +95,12 @@ export const AsGlobalAdmin: Story = { useFactory: () => new MockProjectUserService('manager', 'write'), }, { - provide: UserWrapperService, + provide: OwnUserWrapperService, useFactory: () => - new MockUserService({ ...mockUser, role: 'administrator' }), + new MockOwnUserWrapperService({ + ...mockUser, + role: 'administrator', + }), }, ], }), diff --git a/frontend/src/app/projects/project-detail/project-users/project-user-settings.component.html b/frontend/src/app/projects/project-detail/project-users/project-user-settings.component.html index ee5d77e77..4e654ecd4 100644 --- a/frontend/src/app/projects/project-detail/project-users/project-user-settings.component.html +++ b/frontend/src/app/projects/project-detail/project-users/project-user-settings.component.html @@ -7,7 +7,7 @@

Project Members

} -
+
- } -
- } - -
Users
- - @for (user of getUsersByRole("user"); track user.id) { -
- - account_circle -
-
{{ user.name }}
-
- {{ advanced_roles[user.role] }} -
-
-
-
- @if (user.role === "user") { - - } - -
-
- } -
-
- -
-

Create User

- In general, users are created automatically when logging in the first time. - If you want to create the user before the first login, use this form.
-
- - Username - - @if (username.errors?.required) { - Please enter an username! - } @else if (username.errors?.userAlreadyExists) { - The username already exists! - } - - - Identity Provider Identifier - - @if (idpIdentifier.errors?.required) { - Please enter an IdP identifier! - } @else if (idpIdentifier.errors?.userAlreadyExists) { - The IdP identifier already exists! - } - -
- -
-
-
diff --git a/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.html b/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.html index 2399cfab1..cc3c99ff4 100644 --- a/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.html +++ b/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.html @@ -4,7 +4,7 @@ -->
- +
Add an instance
@@ -26,7 +26,7 @@ instance of t4cInstanceService.t4cInstances$ | async; track instance.id ) { -
+
Core functionality
- +
Manage Users
supervised_user_circle diff --git a/frontend/src/app/users/user-settings/user-settings.component.html b/frontend/src/app/users/user-settings/user-settings.component.html new file mode 100644 index 000000000..db6776ec4 --- /dev/null +++ b/frontend/src/app/users/user-settings/user-settings.component.html @@ -0,0 +1,132 @@ + + +
+
+

Manage Users

+ + + Search + + search + + +
+ @for (role of userRoles; track role) { +
+ {{ roleMapping[role] }} +
+ @for ( + user of getUsersByRole(userWrapperService.users$ | async, role); + track user.id + ) { +
+ +
+ account_circle +
+
+
{{ user.name }}
+
+ {{ roleMapping[user.role] }} +
+
+
+
+ @if (user.role === "administrator") { + @if (user.id !== ownUserService.user?.id) { + + } + } @else { + + } + @if (user.id !== ownUserService.user?.id) { + + } +
+
+ } + @if ((userWrapperService.users$ | async) === undefined) { + + } + } +
+
+ +
+

Create User

+ In general, users are created automatically when logging in the first time. + If you want to create the user before the first login, use this form.
+
+ + Username + + @if (username.errors?.required) { + Please enter an username! + } @else if (username.errors?.userAlreadyExists) { + The username already exists! + } + + + Identity Provider Identifier + + @if (idpIdentifier.errors?.required) { + Please enter an IdP identifier! + } @else if (idpIdentifier.errors?.userAlreadyExists) { + The IdP identifier already exists! + } + +
+ +
+
+
diff --git a/frontend/src/app/settings/core/user-settings/user-settings.component.ts b/frontend/src/app/users/user-settings/user-settings.component.ts similarity index 73% rename from frontend/src/app/settings/core/user-settings/user-settings.component.ts rename to frontend/src/app/users/user-settings/user-settings.component.ts index 7880f4035..d60e7a02b 100644 --- a/frontend/src/app/settings/core/user-settings/user-settings.component.ts +++ b/frontend/src/app/users/user-settings/user-settings.component.ts @@ -2,16 +2,17 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ +import { AsyncPipe } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { AbstractControl, FormControl, FormGroup, ValidationErrors, - ValidatorFn, Validators, FormsModule, ReactiveFormsModule, + AsyncValidatorFn, } from '@angular/forms'; import { MatIconButton, MatButton } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; @@ -26,6 +27,8 @@ import { MatInput } from '@angular/material/input'; import { MatListSubheaderCssMatStyler } from '@angular/material/list'; import { MatTooltip } from '@angular/material/tooltip'; import { RouterLink } from '@angular/router'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { map, Observable, take } from 'rxjs'; import { ConfirmationDialogComponent } from 'src/app/helpers/confirmation-dialog/confirmation-dialog.component'; import { InputDialogComponent, @@ -33,11 +36,11 @@ import { } from 'src/app/helpers/input-dialog/input-dialog.component'; import { ToastService } from 'src/app/helpers/toast/toast.service'; import { Role, User, UsersService } from 'src/app/openapi'; -import { ProjectUserService } from 'src/app/projects/project-detail/project-users/service/project-user.service'; import { UserRole, - UserWrapperService, + OwnUserWrapperService, } from 'src/app/services/user/user.service'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; @Component({ selector: 'app-user-settings', @@ -57,34 +60,39 @@ import { ReactiveFormsModule, MatError, MatButton, + AsyncPipe, + NgxSkeletonLoaderModule, ], }) export class UserSettingsComponent implements OnInit { - users: User[] = []; search = ''; - selectedUser?: User; + + public readonly roleMapping = { + user: 'Global User', + administrator: 'Global Administrator', + }; createUserFormGroup = new FormGroup({ - username: new FormControl('', [ - Validators.required, - this.userNameAlreadyExistsValidator(), - ]), - idpIdentifier: new FormControl('', [ - Validators.required, - this.userIdPIdentifierAlreadyExistsValidator(), - ]), + username: new FormControl('', { + validators: Validators.required, + asyncValidators: this.asyncUserNameAlreadyExistsValidator(), + }), + idpIdentifier: new FormControl('', { + validators: Validators.required, + asyncValidators: this.asyncUserIdPIdentifierAlreadyExistsValidator(), + }), }); constructor( - public userService: UserWrapperService, - public projectUserService: ProjectUserService, + public ownUserService: OwnUserWrapperService, + public userWrapperService: UserWrapperService, private toastService: ToastService, private dialog: MatDialog, private usersService: UsersService, ) {} ngOnInit(): void { - this.getUsers(); + this.userWrapperService.loadUsers(); } get username(): FormControl { @@ -95,25 +103,33 @@ export class UserSettingsComponent implements OnInit { return this.createUserFormGroup.controls.idpIdentifier; } - get advanced_roles() { - return ProjectUserService.ADVANCED_ROLES; + get userRoles() { + return Object.values(Role); } - userNameAlreadyExistsValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - if (this.users.find((user) => user.name == control.value)) { - return { userAlreadyExists: true }; - } - return null; + asyncUserNameAlreadyExistsValidator(): AsyncValidatorFn { + return (control: AbstractControl): Observable => { + return this.userWrapperService.users$.pipe( + take(1), + map((users) => { + return users?.find((user) => user.name == control.value) + ? { userAlreadyExists: true } + : null; + }), + ); }; } - userIdPIdentifierAlreadyExistsValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - if (this.users.find((user) => user.idp_identifier == control.value)) { - return { userAlreadyExists: true }; - } - return null; + asyncUserIdPIdentifierAlreadyExistsValidator(): AsyncValidatorFn { + return (control: AbstractControl): Observable => { + return this.userWrapperService.users$.pipe( + take(1), + map((users) => { + return users?.find((user) => user.idp_identifier == control.value) + ? { userAlreadyExists: true } + : null; + }), + ); }; } @@ -147,7 +163,7 @@ export class UserSettingsComponent implements OnInit { 'User created', `The user ${username} has been created.`, ); - this.getUsers(); + this.userWrapperService.loadUsers(); }, }); } @@ -175,7 +191,7 @@ export class UserSettingsComponent implements OnInit { 'Role of user updated', user.name + ' has now the role administrator', ); - this.getUsers(); + this.userWrapperService.loadUsers(); }, }); } @@ -200,7 +216,7 @@ export class UserSettingsComponent implements OnInit { 'Role of user updated', user.name + ' has now the role user', ); - this.getUsers(); + this.userWrapperService.loadUsers(); }, }); } @@ -228,21 +244,18 @@ export class UserSettingsComponent implements OnInit { 'User deleted', user.name + ' has been deleted', ); - this.getUsers(); + this.userWrapperService.loadUsers(); }, }); } }); } - getUsers() { - this.usersService.getUsers().subscribe((users: User[]) => { - this.users = users; - }); - } - - getUsersByRole(role: UserRole): User[] { - return this.users.filter( + getUsersByRole( + users: User[] | null | undefined, + role: UserRole, + ): User[] | undefined { + return users?.filter( (user) => user.role == role && user.name.toLowerCase().includes(this.search.toLowerCase()), diff --git a/frontend/src/app/users/user-settings/user-settings.stories.ts b/frontend/src/app/users/user-settings/user-settings.stories.ts new file mode 100644 index 000000000..afd86b0d9 --- /dev/null +++ b/frontend/src/app/users/user-settings/user-settings.stories.ts @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { User } from 'src/app/openapi'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; +import { + MockOwnUserWrapperService, + mockUser, + MockUserWrapperService, +} from 'src/storybook/user'; +import { UserSettingsComponent } from './user-settings.component'; + +const meta: Meta = { + title: 'Settings Components/User Settings', + component: UserSettingsComponent, +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = { + args: {}, +}; + +const loggedInUser: User = { + ...mockUser, + name: 'currentlyLoggedInUser', + role: 'administrator', + id: 132, +}; + +export const Overview: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: UserWrapperService, + useFactory: () => + new MockUserWrapperService(undefined, [ + { + ...mockUser, + role: 'administrator', + name: 'globalAdministrator1', + }, + { + ...mockUser, + role: 'administrator', + name: 'globalAdministrator2', + }, + loggedInUser, + mockUser, + { + ...mockUser, + name: 'userWithReallyLongNameThatHasToBeWrapped', + }, + ]), + }, + { + provide: OwnUserWrapperService, + useFactory: () => new MockOwnUserWrapperService(loggedInUser), + }, + ], + }), + ], +}; diff --git a/frontend/src/app/users/user-wrapper/user-wrapper.component.ts b/frontend/src/app/users/user-wrapper/user-wrapper.component.ts new file mode 100644 index 000000000..a2a9d86fb --- /dev/null +++ b/frontend/src/app/users/user-wrapper/user-wrapper.component.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, +} from '@angular/core'; +import { ActivatedRoute, RouterOutlet } from '@angular/router'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { map } from 'rxjs'; +import { BreadcrumbsService } from 'src/app/general/breadcrumbs/breadcrumbs.service'; +import { User } from 'src/app/openapi'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; + +@UntilDestroy() +@Component({ + selector: 'app-user-wrapper', + standalone: true, + imports: [CommonModule, RouterOutlet], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UserWrapperComponent implements OnInit, OnDestroy { + constructor( + private route: ActivatedRoute, + public userWrapperService: UserWrapperService, + private breadcrumbsService: BreadcrumbsService, + ) {} + + ngOnInit() { + this.updateUserOnRouteUpdate(); + this.updateBreadcrumb(); + } + + ngOnDestroy(): void { + this.userWrapperService.resetUser(); + this.breadcrumbsService.updatePlaceholder({ user: undefined }); + } + + updateUserOnRouteUpdate() { + this.route.params + .pipe( + map((params) => params.user), + untilDestroyed(this), + ) + .subscribe((userID: string) => { + this.userWrapperService.loadUser(parseInt(userID)); + }); + } + + updateBreadcrumb() { + this.userWrapperService.user$ + .pipe(untilDestroyed(this)) + .subscribe((user: User | undefined) => + this.breadcrumbsService.updatePlaceholder({ user }), + ); + } +} diff --git a/frontend/src/app/users/user-wrapper/user-wrapper.service.ts b/frontend/src/app/users/user-wrapper/user-wrapper.service.ts new file mode 100644 index 000000000..5d53944b2 --- /dev/null +++ b/frontend/src/app/users/user-wrapper/user-wrapper.service.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { User, UsersService } from 'src/app/openapi'; + +@Injectable({ + providedIn: 'root', +}) +export class UserWrapperService { + _user = new BehaviorSubject(undefined); + user$ = this._user.asObservable(); + + private _users = new BehaviorSubject(undefined); + public readonly users$ = this._users.asObservable(); + + constructor(private userService: UsersService) {} + + loadUser(userID: number) { + this.resetUser(); + this.userService + .getUser(userID) + .subscribe((user: User) => this._user.next(user)); + } + + loadUsers() { + this.resetUsers(); + this.userService.getUsers().subscribe((users) => this._users.next(users)); + } + + resetUser() { + this._user.next(undefined); + } + + resetUsers() { + this._users.next(undefined); + } +} diff --git a/frontend/src/app/users/users-profile/common-projects/common-projects.component.html b/frontend/src/app/users/users-profile/common-projects/common-projects.component.html index 605deb53b..99b9a0795 100644 --- a/frontend/src/app/users/users-profile/common-projects/common-projects.component.html +++ b/frontend/src/app/users/users-profile/common-projects/common-projects.component.html @@ -2,38 +2,40 @@ ~ SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors ~ SPDX-License-Identifier: Apache-2.0 --> -@if (user && userService.user && user.id !== userService.user.id) { -
-

Common Projects

-
- -
- @if ((commonProjects$ | async) === undefined) { - Loading... - } @else if ((commonProjects$ | async)?.length === 0) { - You do not have any common projects. - } @else {} -
- @for (project of commonProjects$ | async; track project.slug) { -
-
- {{ project.name }}
- @if (project.description) { - {{ - project.description - }} - } @else { - No description provided - } +@if (userWrapperService.user$ | async; as user) { + @if (userService.user && user.id !== userService.user.id) { +
+

Common Projects

+
+ +
+ @if ((commonProjects$ | async) === undefined) { + Loading... + } @else if ((commonProjects$ | async)?.length === 0) { + You do not have any common projects. + } @else {} +
+ @for (project of commonProjects$ | async; track project.slug) { +
+
+ {{ project.name }}
+ @if (project.description) { + {{ + project.description + }} + } @else { + No description provided + } +
-
- } + } +
-
+ } } diff --git a/frontend/src/app/users/users-profile/common-projects/common-projects.component.ts b/frontend/src/app/users/users-profile/common-projects/common-projects.component.ts index bd06aa185..7ad7bbbd9 100644 --- a/frontend/src/app/users/users-profile/common-projects/common-projects.component.ts +++ b/frontend/src/app/users/users-profile/common-projects/common-projects.component.ts @@ -3,12 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { MatDividerModule } from '@angular/material/divider'; import { RouterLink } from '@angular/router'; -import { BehaviorSubject } from 'rxjs'; -import { Project, User, UsersService } from 'src/app/openapi'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { BehaviorSubject, of, switchMap } from 'rxjs'; +import { Project, UsersService } from 'src/app/openapi'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; @Component({ selector: 'app-common-projects', @@ -17,29 +18,27 @@ import { UserWrapperService } from 'src/app/services/user/user.service'; templateUrl: './common-projects.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CommonProjectsComponent { - _user: User | undefined; - - @Input() - set user(value: User | undefined) { - this._user = value; - if (value && value.id !== this.userService.user?.id) { - this.usersService.getCommonProjects(value.id).subscribe({ - next: (projects) => this.commonProjects.next(projects), - error: () => this.commonProjects.next(undefined), - }); - } - } - - get user(): User | undefined { - return this._user; - } - +export class CommonProjectsComponent implements OnInit { commonProjects = new BehaviorSubject(undefined); public readonly commonProjects$ = this.commonProjects.asObservable(); constructor( - public userService: UserWrapperService, + public userService: OwnUserWrapperService, + public userWrapperService: UserWrapperService, private usersService: UsersService, ) {} + + ngOnInit(): void { + this.userWrapperService.user$ + .pipe( + switchMap((user) => { + if (!user) return of(undefined); + return this.usersService.getCommonProjects(user.id); + }), + ) + .subscribe({ + next: (projects) => this.commonProjects.next(projects), + error: () => this.commonProjects.next(undefined), + }); + } } diff --git a/frontend/src/app/users/users-profile/common-projects/common-projects.stories.ts b/frontend/src/app/users/users-profile/common-projects/common-projects.stories.ts index 3c8638b82..1b8b327fe 100644 --- a/frontend/src/app/users/users-profile/common-projects/common-projects.stories.ts +++ b/frontend/src/app/users/users-profile/common-projects/common-projects.stories.ts @@ -4,9 +4,14 @@ */ import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; import { of } from 'rxjs'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; import { mockProject } from 'src/storybook/project'; -import { MockUserService, mockUser } from 'src/storybook/user'; +import { + MockOwnUserWrapperService, + mockUser, + MockUserWrapperService, +} from 'src/storybook/user'; import { CommonProjectsComponent } from './common-projects.component'; const meta: Meta = { @@ -15,16 +20,17 @@ const meta: Meta = { decorators: [ moduleMetadata({ providers: [ + { + provide: OwnUserWrapperService, + useFactory: () => new MockOwnUserWrapperService(mockUser), + }, { provide: UserWrapperService, - useFactory: () => new MockUserService(mockUser), + useFactory: () => new MockUserWrapperService({ ...mockUser, id: 0 }), }, ], }), ], - args: { - _user: { ...mockUser, id: 0 }, - }, }; export default meta; diff --git a/frontend/src/app/users/users-profile/user-information/user-information.component.html b/frontend/src/app/users/users-profile/user-information/user-information.component.html index f2ca48a82..0688b7eeb 100644 --- a/frontend/src/app/users/users-profile/user-information/user-information.component.html +++ b/frontend/src/app/users/users-profile/user-information/user-information.component.html @@ -2,7 +2,8 @@ ~ SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors ~ SPDX-License-Identifier: Apache-2.0 --> -@if (user && userService.user?.role === "administrator") { + +@if (userWrapperService.user$ | async; as user) {

User information

diff --git a/frontend/src/app/users/users-profile/user-information/user-information.component.ts b/frontend/src/app/users/users-profile/user-information/user-information.component.ts index 28b7348a8..c9e7776b9 100644 --- a/frontend/src/app/users/users-profile/user-information/user-information.component.ts +++ b/frontend/src/app/users/users-profile/user-information/user-information.component.ts @@ -7,15 +7,17 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, - Input, + OnInit, ViewChild, } from '@angular/core'; import { MatDividerModule } from '@angular/material/divider'; import { MatPaginator } from '@angular/material/paginator'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { HistoryEvent, User, UsersService } from 'src/app/openapi'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { of, switchMap } from 'rxjs'; +import { HistoryEvent, UsersService } from 'src/app/openapi'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; @Component({ selector: 'app-user-information', @@ -36,27 +38,7 @@ import { UserWrapperService } from 'src/app/services/user/user.service'; templateUrl: './user-information.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class UserInformationComponent implements AfterViewInit { - _user: User | undefined; - - @Input() - set user(value: User | undefined) { - this._user = value; - if (value && this.userService.user?.role === 'administrator') { - this.usersService.getUserEvents(value.id).subscribe({ - next: (userEvents) => { - this.userEvents = userEvents; - this.historyEventDataSource.data = userEvents; - this.historyEventDataSource.paginator = this.paginator; - }, - }); - } - } - - get user(): User | undefined { - return this._user; - } - +export class UserInformationComponent implements OnInit, AfterViewInit { userEvents?: HistoryEvent[]; @ViewChild(MatPaginator) paginator: MatPaginator | null = null; @@ -75,7 +57,27 @@ export class UserInformationComponent implements AfterViewInit { } constructor( - public userService: UserWrapperService, + public userService: OwnUserWrapperService, + public userWrapperService: UserWrapperService, private usersService: UsersService, ) {} + + ngOnInit(): void { + this.userWrapperService.user$ + .pipe( + switchMap((user) => { + if (!user) return of(undefined); + return this.usersService.getUserEvents(user.id); + }), + ) + .subscribe({ + next: (userEvents) => { + this.userEvents = userEvents; + if (userEvents) { + this.historyEventDataSource.data = userEvents; + } + this.historyEventDataSource.paginator = this.paginator; + }, + }); + } } diff --git a/frontend/src/app/users/users-profile/user-information/user-information.stories.ts b/frontend/src/app/users/users-profile/user-information/user-information.stories.ts index d97b9aa16..5a5701409 100644 --- a/frontend/src/app/users/users-profile/user-information/user-information.stories.ts +++ b/frontend/src/app/users/users-profile/user-information/user-information.stories.ts @@ -5,9 +5,14 @@ import { MatTableDataSource } from '@angular/material/table'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { EventType, HistoryEvent } from 'src/app/openapi'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; import { mockProject } from 'src/storybook/project'; -import { mockUser, MockUserService } from 'src/storybook/user'; +import { + mockUser, + MockOwnUserWrapperService, + MockUserWrapperService, +} from 'src/storybook/user'; import { UserInformationComponent } from './user-information.component'; const meta: Meta = { @@ -17,19 +22,20 @@ const meta: Meta = { moduleMetadata({ providers: [ { - provide: UserWrapperService, + provide: OwnUserWrapperService, useFactory: () => - new MockUserService({ + new MockOwnUserWrapperService({ ...mockUser, role: 'administrator', }), }, + { + provide: UserWrapperService, + useFactory: () => new MockUserWrapperService({ ...mockUser, id: 0 }), + }, ], }), ], - args: { - _user: { ...mockUser, id: 0 }, - }, }; export default meta; @@ -86,7 +92,6 @@ const events: HistoryEvent[] = [ export const EventsAndLastLogin: Story = { args: { - user: mockUser, userEvents: events, historyEventDataSource: new MatTableDataSource(events), }, diff --git a/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.component.html b/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.component.html index 0edc25d07..81c98b652 100644 --- a/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.component.html +++ b/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.component.html @@ -3,7 +3,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -@if (user && userService.user?.role === "administrator") { +@if (this.userWrapperService.user$ | async; as user) {

User workspaces

@@ -11,13 +11,10 @@

User workspaces

- @if (workspaces === undefined) { + @if ((workspaces | async) === undefined) { Loading... - } @else if (workspaces.length === 0) { - The user doesn't have any workspaces. A workspace is auto-created when - the user requests a persistent workspace session. } @else { - @for (workspace of workspaces; track workspace.id) { + @for (workspace of workspaces | async; track workspace.id) {
@@ -33,7 +30,7 @@

+ } @empty { + The user doesn't have any workspaces. A workspace is auto-created when + the user requests a persistent workspace session. } }
diff --git a/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.component.ts b/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.component.ts index 963520c2c..c638da6ca 100644 --- a/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.component.ts +++ b/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.component.ts @@ -3,16 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; +import { BehaviorSubject, of, switchMap } from 'rxjs'; import { ConfirmationDialogComponent } from 'src/app/helpers/confirmation-dialog/confirmation-dialog.component'; import { DisplayValueComponent } from 'src/app/helpers/display-value/display-value.component'; import { ToastService } from 'src/app/helpers/toast/toast.service'; import { User, UsersService, Workspace } from 'src/app/openapi'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; @Component({ selector: 'app-user-workspaces', @@ -30,70 +32,62 @@ import { UserWrapperService } from 'src/app/services/user/user.service'; display: block; } `, + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class UserWorkspacesComponent { - _user: User | undefined; +export class UserWorkspacesComponent implements OnInit { + workspaces = new BehaviorSubject(undefined); - workspaces: Workspace[] | undefined = undefined; - - @Input() - set user(value: User | undefined) { - this._user = value; - this.reloadWorkspaces(); - } - - get user(): User | undefined { - return this._user; - } - - reloadWorkspaces() { - this.workspaces = undefined; - if ( - this._user === undefined || - this.userService.user === undefined || - this.userService.user.role !== 'administrator' - ) - return; - - this.usersService.getWorkspacesForUser(this._user.id).subscribe({ - next: (workspaces) => { - this.workspaces = workspaces; - }, - error: () => (this.workspaces = undefined), - }); + loadWorkspaces() { + this.workspaces.next(undefined); + this.userWrapperService.user$ + .pipe( + switchMap((user) => { + if (!user) return of(undefined); + return this.usersService.getWorkspacesForUser(user.id); + }), + ) + .subscribe({ + next: (workspaces) => { + this.workspaces.next(workspaces); + }, + error: () => this.workspaces.next(undefined), + }); } constructor( - public userService: UserWrapperService, + public ownUserService: OwnUserWrapperService, + public userWrapperService: UserWrapperService, private usersService: UsersService, private dialog: MatDialog, private toastService: ToastService, ) {} - deleteWorkspace(workspace: Workspace) { + ngOnInit(): void { + this.loadWorkspaces(); + } + + deleteWorkspace(user: User, workspace: Workspace) { const dialogRef = this.dialog.open(ConfirmationDialogComponent, { data: { title: 'Delete workspace', text: - `Do you really want to delete the workspace ${workspace.id} of user '${this._user!.name}'? ` + + `Do you really want to delete the workspace ${workspace.id} of user '${user.name}'? ` + 'This will irrevocably remove all files in the workspace.', }, }); dialogRef.afterClosed().subscribe((result: boolean) => { if (result) { - this.usersService - .deleteWorkspace(workspace.id, this._user!.id) - .subscribe({ - next: () => { - this.toastService.showSuccess( - 'Workspace deleted successfully.', - `The workspace ${workspace.id} of user '${this._user!.name}' was deleted.`, - ); + this.usersService.deleteWorkspace(workspace.id, user.id).subscribe({ + next: () => { + this.toastService.showSuccess( + 'Workspace deleted successfully.', + `The workspace ${workspace.id} of user '${user.name}' was deleted.`, + ); - this.reloadWorkspaces(); - }, - }); + this.loadWorkspaces(); + }, + }); } }); } diff --git a/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.stories.ts b/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.stories.ts index 3ba27f494..d017a7815 100644 --- a/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.stories.ts +++ b/frontend/src/app/users/users-profile/user-workspaces/user-workspaces.stories.ts @@ -3,10 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; -import { UserWrapperService } from 'src/app/services/user/user.service'; -import { mockUser, MockUserService } from 'src/storybook/user'; +import { Observable, of } from 'rxjs'; +import { UsersService, Workspace } from 'src/app/openapi'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; +import { + mockUser, + MockOwnUserWrapperService, + MockUserWrapperService, +} from 'src/storybook/user'; import { UserWorkspacesComponent } from './user-workspaces.component'; +class MockUserService { + _workspaces: Workspace[] = []; + + public getWorkspacesForUser(_userId: number): Observable { + return of(this._workspaces); + } + + constructor(workspaces: Workspace[]) { + this._workspaces = workspaces; + } +} + const meta: Meta = { title: 'Settings Components/Users Profile/User Workspaces', component: UserWorkspacesComponent, @@ -14,16 +33,20 @@ const meta: Meta = { moduleMetadata({ providers: [ { - provide: UserWrapperService, + provide: OwnUserWrapperService, useFactory: () => - new MockUserService({ ...mockUser, role: 'administrator' }), + new MockOwnUserWrapperService({ + ...mockUser, + role: 'administrator', + }), + }, + { + provide: UserWrapperService, + useFactory: () => new MockUserWrapperService({ ...mockUser, id: 0 }), }, ], }), ], - args: { - _user: { ...mockUser, id: 0 }, - }, }; export default meta; @@ -34,19 +57,35 @@ export const Loading: Story = { }; export const NoWorkspaces: Story = { - args: { - workspaces: [], - }, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: UsersService, + useFactory: () => new MockUserService([]), + }, + ], + }), + ], }; -export const Workspace: Story = { - args: { - workspaces: [ - { - id: 1, - pvc_name: 'persistent-volume-429d805a-6904-4217-b035-8e3def3506ce', - size: '20Gi', - }, - ], - }, +export const WorkspaceOverview: Story = { + decorators: [ + moduleMetadata({ + providers: [ + { + provide: UsersService, + useFactory: () => + new MockUserService([ + { + id: 1, + pvc_name: + 'persistent-volume-429d805a-6904-4217-b035-8e3def3506ce', + size: '20Gi', + }, + ]), + }, + ], + }), + ], }; diff --git a/frontend/src/app/users/users-profile/users-profile.component.html b/frontend/src/app/users/users-profile/users-profile.component.html index bb56979c0..0315636f2 100644 --- a/frontend/src/app/users/users-profile/users-profile.component.html +++ b/frontend/src/app/users/users-profile/users-profile.component.html @@ -2,27 +2,33 @@ ~ SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors ~ SPDX-License-Identifier: Apache-2.0 --> -
-
- Dummy profile picture -
-
-

Profile of {{ user?.name }}

- @if (user?.created) { -
- Joined the Capella Collaboration Manager in - {{ user?.created | date: "y" }} -
- } -
-
+@if (userWrapperService.user$ | async; as user) { +
+
+ Dummy profile picture +
+
+

Profile of {{ user?.name }}

+ @if (user?.created) { +
+ Joined the Capella Collaboration Manager in + {{ user?.created | date: "y" }} +
+ } +
+
+}
- - - + @if (ownUserService.user?.role === "administrator") { + + } + + @if (ownUserService.user?.role === "administrator") { + + }
diff --git a/frontend/src/app/users/users-profile/users-profile.component.ts b/frontend/src/app/users/users-profile/users-profile.component.ts index c98a3850d..7b3a9e357 100644 --- a/frontend/src/app/users/users-profile/users-profile.component.ts +++ b/frontend/src/app/users/users-profile/users-profile.component.ts @@ -2,14 +2,12 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute, RouterLink } from '@angular/router'; +import { AsyncPipe, DatePipe } from '@angular/common'; +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; import { UntilDestroy } from '@ngneat/until-destroy'; -import { filter, map } from 'rxjs'; -import { BreadcrumbsService } from 'src/app/general/breadcrumbs/breadcrumbs.service'; -import { User, UsersService } from 'src/app/openapi'; -import { UserWrapperService } from 'src/app/services/user/user.service'; +import { OwnUserWrapperService } from 'src/app/services/user/user.service'; +import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.service'; import { CommonProjectsComponent } from './common-projects/common-projects.component'; import { UserInformationComponent } from './user-information/user-information.component'; import { UserWorkspacesComponent } from './user-workspaces/user-workspaces.component'; @@ -25,33 +23,12 @@ import { UserWorkspacesComponent } from './user-workspaces/user-workspaces.compo CommonProjectsComponent, UserInformationComponent, UserWorkspacesComponent, + AsyncPipe, ], }) -export class UsersProfileComponent implements OnInit, OnDestroy { - user: User | undefined; - +export class UsersProfileComponent { constructor( - public userService: UserWrapperService, - private usersService: UsersService, - private route: ActivatedRoute, - private breadcrumbsService: BreadcrumbsService, + public ownUserService: OwnUserWrapperService, + public userWrapperService: UserWrapperService, ) {} - - ngOnInit() { - this.route.params - .pipe( - filter((params) => params['userId']), - map((params) => params['userId']), - ) - .subscribe((userId: number) => { - this.usersService.getUser(userId).subscribe((user) => { - this.user = user; - this.breadcrumbsService.updatePlaceholder({ user: user }); - }); - }); - } - - ngOnDestroy(): void { - this.breadcrumbsService.updatePlaceholder({ user: undefined }); - } } diff --git a/frontend/src/storybook/user.ts b/frontend/src/storybook/user.ts index 236208ecf..3c3fb2401 100644 --- a/frontend/src/storybook/user.ts +++ b/frontend/src/storybook/user.ts @@ -6,7 +6,7 @@ import { BehaviorSubject } from 'rxjs'; import { User } from 'src/app/openapi'; import { UserRole, - UserWrapperService, + OwnUserWrapperService, } from 'src/app/services/user/user.service'; export const mockUser: Readonly = { @@ -19,7 +19,9 @@ export const mockUser: Readonly = { last_login: '2024-04-29T14:59:00Z', }; -export class MockUserService implements Partial { +export class MockOwnUserWrapperService + implements Partial +{ _user = new BehaviorSubject(undefined); user$ = this._user.asObservable(); user: User | undefined = mockUser; @@ -34,3 +36,21 @@ export class MockUserService implements Partial { return roles.indexOf(requiredRole) <= roles.indexOf(this.user!.role); } } + +export class MockUserWrapperService implements Partial { + _user = new BehaviorSubject(undefined); + user$ = this._user.asObservable(); + + _users = new BehaviorSubject(undefined); + users$ = this._users.asObservable(); + + constructor( + user: User | undefined = undefined, + users: User[] | undefined = undefined, + ) { + this._user.next(user); + this._users.next(users); + } + + loadUsers() {} // eslint-disable-line @typescript-eslint/no-empty-function +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index c37b10946..7c5c0a06b 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -210,12 +210,7 @@ Angular Material Card Styles } .mat-card-overview { - padding: 0 !important; - width: fit-content; - width: 400px; - max-width: 85vw; - height: 250px; - user-select: none; + @apply h-[250px] w-full select-none !p-0 sm:w-[400px]; } .mat-card-overview.new {