From 0e91470db1c9fd64fea9ebc5f89a2a16b7946f17 Mon Sep 17 00:00:00 2001 From: Ege Kocabas <48245934+egekocabas@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:39:54 +0100 Subject: [PATCH] feat: UI Refactoring (#359) --- client/angular.json | 3 +- client/public/favicon.ico | Bin 15086 -> 0 bytes client/{src => public}/favicon.png | Bin client/src/app/app.routes.ts | 24 ++-- .../connect-repo/connect-repo.component.html | 72 ---------- .../connect-repo/connect-repo.component.ts | 131 ----------------- .../environment-list-view.component.html | 12 +- .../components/footer/footer.component.html | 10 ++ .../app/components/footer/footer.component.ts | 9 ++ .../navigation-bar.component.html | 103 +++++++++++++ .../navigation-bar.component.ts | 95 ++++++++++++ .../page-heading/page-heading.component.html | 12 +- .../page-heading/page-heading.component.ts | 2 +- .../profile-nav-section.component.html | 27 +++- .../profile-nav-section.component.spec.ts | 1 + .../profile-nav-section.component.ts | 28 ++-- .../pull-request-status-icon.component.html | 3 + .../pull-request-status-icon.component.ts | 38 +++++ .../pull-request-table.component.html | 4 +- .../pull-request-table.component.ts | 17 +-- .../report-problem-button.component.html | 2 +- .../user-avatar/user-avatar.component.html | 32 +++-- .../user-avatar/user-avatar.component.ts | 5 +- .../user-lock-info.component.html | 10 +- .../core/modules/markdown/markdown.pipe.ts | 2 +- .../services/keycloak/keycloak.service.ts | 18 +++ .../src/app/pages/about/about.component.html | 126 ++++++++++++++++ client/src/app/pages/about/about.component.ts | 101 +++++++++++++ .../branch-details.component.html | 11 +- .../branch-details.component.spec.ts | 3 +- .../branch-details.component.ts | 4 +- .../app/pages/imprint/imprint.component.html | 68 +++++++++ .../app/pages/imprint/imprint.component.ts | 9 ++ .../main-layout/main-layout.component.html | 42 ++---- .../main-layout/main-layout.component.ts | 135 ++++++++++-------- .../app/pages/privacy/privacy.component.html | 127 ++++++++++++++++ .../app/pages/privacy/privacy.component.ts | 9 ++ .../pull-request-details.component.html | 39 +++-- .../pull-request-details.component.ts | 16 ++- .../repository-overview.component.html | 121 ++++++++++------ .../repository-overview.component.ts | 33 ++--- client/src/icons.module.ts | 2 + 42 files changed, 1049 insertions(+), 457 deletions(-) delete mode 100644 client/public/favicon.ico rename client/{src => public}/favicon.png (100%) delete mode 100644 client/src/app/components/connect-repo/connect-repo.component.html delete mode 100644 client/src/app/components/connect-repo/connect-repo.component.ts create mode 100644 client/src/app/components/footer/footer.component.html create mode 100644 client/src/app/components/footer/footer.component.ts create mode 100644 client/src/app/components/navigation-bar/navigation-bar.component.html create mode 100644 client/src/app/components/navigation-bar/navigation-bar.component.ts create mode 100644 client/src/app/components/pull-request-status-icon/pull-request-status-icon.component.html create mode 100644 client/src/app/components/pull-request-status-icon/pull-request-status-icon.component.ts create mode 100644 client/src/app/pages/about/about.component.html create mode 100644 client/src/app/pages/about/about.component.ts create mode 100644 client/src/app/pages/imprint/imprint.component.html create mode 100644 client/src/app/pages/imprint/imprint.component.ts create mode 100644 client/src/app/pages/privacy/privacy.component.html create mode 100644 client/src/app/pages/privacy/privacy.component.ts diff --git a/client/angular.json b/client/angular.json index 32d6d7a0b..3ccebafd8 100644 --- a/client/angular.json +++ b/client/angular.json @@ -20,7 +20,6 @@ "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "css", "assets": [ - "src/favicon.png", { "glob": "**/*", "input": "public" @@ -92,4 +91,4 @@ "cli": { "analytics": false } -} \ No newline at end of file +} diff --git a/client/public/favicon.ico b/client/public/favicon.ico deleted file mode 100644 index 57614f9c967596fad0a3989bec2b1deff33034f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 Nr__JTFzvMr^FKp4S3v*( diff --git a/client/src/favicon.png b/client/public/favicon.png similarity index 100% rename from client/src/favicon.png rename to client/public/favicon.png diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts index eb6fb90f6..d337db982 100644 --- a/client/src/app/app.routes.ts +++ b/client/src/app/app.routes.ts @@ -6,22 +6,28 @@ import { maintainerGuard } from './core/routeGuards/maintainer.guard'; export const routes: Routes = [ { path: '', - pathMatch: 'full', - redirectTo: 'repo', - }, - { - path: 'repo', + loadComponent: () => import('./pages/main-layout/main-layout.component').then(m => m.MainLayoutComponent), canActivateChild: [RepositoryFilterGuard], children: [ { path: '', pathMatch: 'full', - redirectTo: 'list', + loadComponent: () => import('./pages/repository-overview/repository-overview.component').then(m => m.RepositoryOverviewComponent), + }, + { + path: 'about', + loadComponent: () => import('./pages/about/about.component').then(m => m.AboutComponent), + }, + { + path: 'privacy', + loadComponent: () => import('./pages/privacy/privacy.component').then(m => m.PrivacyComponent), + }, + { + path: 'imprint', + loadComponent: () => import('./pages/imprint/imprint.component').then(m => m.ImprintComponent), }, - { path: 'list', loadComponent: () => import('./pages/repository-overview/repository-overview.component').then(m => m.RepositoryOverviewComponent) }, { - path: ':repositoryId', - loadComponent: () => import('./pages/main-layout/main-layout.component').then(m => m.MainLayoutComponent), + path: 'repo/:repositoryId', children: [ { path: '', loadComponent: () => import('./pages/ci-cd/ci-cd.component').then(m => m.CiCdComponent) }, { diff --git a/client/src/app/components/connect-repo/connect-repo.component.html b/client/src/app/components/connect-repo/connect-repo.component.html deleted file mode 100644 index 36e8dba44..000000000 --- a/client/src/app/components/connect-repo/connect-repo.component.html +++ /dev/null @@ -1,72 +0,0 @@ - -
- - @if (currentStep() === 'token_input') { -
-

Please enter your GitHub personal access token:

- - - The token needs repo and org permissions. - Generate a new token - - -
- } - @if (currentStep() === 'org_selection') { -
-
Select an organization:
- @if (loading()) { -
- -
- } @else { -
- @for (org of organizations(); track org.id) { -
- -
- -
-
{{ org.login }}
-
{{ org.description }}
-
-
-
-
- } -
- } -
- } - - - @if (currentStep() === 'repo_selection') { -
-
- -
Select repositories from {{ selectedOrg()?.login }}
-
- - @if (loading()) { -
- -
- } @else { -
- @for (repo of repositories(); track repo.id) { - -
-
-
{{ repo.name }}
-
{{ repo.description }}
-
- -
-
- } -
- } -
- } -
-
diff --git a/client/src/app/components/connect-repo/connect-repo.component.ts b/client/src/app/components/connect-repo/connect-repo.component.ts deleted file mode 100644 index 1ab058c65..000000000 --- a/client/src/app/components/connect-repo/connect-repo.component.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Component, inject, signal } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { GithubOrg, GithubRepo, GithubService } from '@app/core/services/github.service'; -import { RepositoryService } from '@app/core/services/repository.service'; -import { IconsModule } from 'icons.module'; -import { ButtonModule } from 'primeng/button'; -import { CardModule } from 'primeng/card'; -import { DialogModule } from 'primeng/dialog'; -import { DropdownModule } from 'primeng/dropdown'; -import { finalize } from 'rxjs'; - -@Component({ - selector: 'app-connect-repo', - imports: [ButtonModule, DialogModule, DropdownModule, CardModule, IconsModule, FormsModule], - templateUrl: './connect-repo.component.html', -}) -export class ConnectRepoComponent { - private githubService = inject(GithubService); - private repositoryService = inject(RepositoryService); - - visible = false; - loading = signal(false); - currentStep = signal<'token_input' | 'org_selection' | 'repo_selection'>('token_input'); - githubToken = ''; - organizations = signal([]); - repositories = signal([]); - selectedOrg = signal(null); - connectingRepoId = signal(null); - - show() { - this.visible = true; - } - - hide() { - this.visible = false; - this.reset(); - } - - private reset() { - this.currentStep.set('token_input'); - this.githubToken = ''; - this.organizations.set([]); - this.repositories.set([]); - this.selectedOrg.set(null); - this.loading.set(false); - } - - private loadOrganizations() { - this.loading.set(true); - this.githubService - .getOrganizations() - .pipe(finalize(() => this.loading.set(false))) - .subscribe({ - next: orgs => { - this.organizations.set(orgs); - }, - error: error => { - console.error('Failed to fetch organizations:', error); - // Handle error (show toast or message) - }, - }); - } - - validateAndSetToken() { - if (!this.githubToken) return; - - this.loading.set(true); - this.githubService - .validateToken(this.githubToken) - .pipe(finalize(() => this.loading.set(false))) - .subscribe({ - next: isValid => { - if (isValid) { - this.githubService.setToken(this.githubToken); - this.currentStep.set('org_selection'); - this.loadOrganizations(); - } else { - console.log('Invalid token'); - // Show error message (invalid token) - } - }, - error: () => { - console.error('Failed to validate token'); - // Show error message - }, - }); - } - - selectOrganization(org: GithubOrg) { - this.selectedOrg.set(org); - this.loading.set(true); - - this.githubService - .getOrgRepositories(org.login) - .pipe(finalize(() => this.loading.set(false))) - .subscribe({ - next: repos => { - this.repositories.set(repos); - this.currentStep.set('repo_selection'); - }, - error: error => { - console.error('Failed to fetch repositories:', error); - // Handle error (show toast or message) - }, - }); - } - - selectRepository(repo: GithubRepo) { - this.connectingRepoId.set(repo.id); - - this.repositoryService - .connectRepository({ - name: repo.name, - id: repo.id, - description: repo.description || undefined, - nameWithOwner: repo.full_name, - htmlUrl: repo.html_url, - }) - .pipe(finalize(() => this.connectingRepoId.set(repo.id))) - .subscribe({ - next: () => { - // Handle success (show toast or message) - this.hide(); - }, - error: error => { - console.error('Failed to connect repository:', error); - // Handle error (show toast or message) - }, - }); - } -} diff --git a/client/src/app/components/environments/environment-list/environment-list-view.component.html b/client/src/app/components/environments/environment-list/environment-list-view.component.html index 309ce55ed..de66f2678 100644 --- a/client/src/app/components/environments/environment-list/environment-list-view.component.html +++ b/client/src/app/components/environments/environment-list/environment-list-view.component.html @@ -36,7 +36,7 @@ @if (environment.latestDeployment; as deployment) {
- + @if (environment.latestDeployment.user?.name) { {{ environment.latestDeployment.user?.name }} deployed } @@ -59,7 +59,7 @@ @if (environment.locked) {
- +
@if (environment.lockedAt) { @@ -83,7 +83,7 @@ @if (userCanDeploy(environment)) { } @@ -92,7 +92,11 @@ @if (!environment.enabled) { } - } diff --git a/client/src/app/components/footer/footer.component.html b/client/src/app/components/footer/footer.component.html new file mode 100644 index 000000000..ebc64ae6e --- /dev/null +++ b/client/src/app/components/footer/footer.component.html @@ -0,0 +1,10 @@ + diff --git a/client/src/app/components/footer/footer.component.ts b/client/src/app/components/footer/footer.component.ts new file mode 100644 index 000000000..493274723 --- /dev/null +++ b/client/src/app/components/footer/footer.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-footer', + imports: [RouterLink], + templateUrl: './footer.component.html', +}) +export class FooterComponent {} diff --git a/client/src/app/components/navigation-bar/navigation-bar.component.html b/client/src/app/components/navigation-bar/navigation-bar.component.html new file mode 100644 index 000000000..6be80440a --- /dev/null +++ b/client/src/app/components/navigation-bar/navigation-bar.component.html @@ -0,0 +1,103 @@ + +
+ +
+ + + + @if (isExpanded()) { + Home + } + + + + + @if (repositoryId()) { + +
+ + + + + @if (isExpanded()) { + {{ repositoryQuery.data()?.name }} + } +
+ + + + +
+ @for (item of items(); track item.label) { + + + @if (isExpanded()) { + {{ item.label }} + } + + } +
+ } @else { + + + } + + + + + + + + + + +
+ + +
+ +
+
diff --git a/client/src/app/components/navigation-bar/navigation-bar.component.ts b/client/src/app/components/navigation-bar/navigation-bar.component.ts new file mode 100644 index 000000000..ab93c92db --- /dev/null +++ b/client/src/app/components/navigation-bar/navigation-bar.component.ts @@ -0,0 +1,95 @@ +import { Component, computed, inject, input, signal } from '@angular/core'; +import { Avatar } from 'primeng/avatar'; +import { Divider } from 'primeng/divider'; +import { HeliosIconComponent } from '@app/components/helios-icon/helios-icon.component'; +import { ProfileNavSectionComponent } from '@app/components/profile-nav-section/profile-nav-section.component'; +import { RouterLink, RouterLinkActive } from '@angular/router'; +import { NgClass, SlicePipe } from '@angular/common'; +import { TablerIconComponent } from 'angular-tabler-icons'; +import { Tooltip } from 'primeng/tooltip'; +import { UserLockInfoComponent } from '@app/components/user-lock-info/user-lock-info.component'; +import { KeycloakService } from '@app/core/services/keycloak/keycloak.service'; +import { PermissionService } from '@app/core/services/permission.service'; +import { injectQuery } from '@tanstack/angular-query-experimental'; +import { getRepositoryByIdOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen'; + +@Component({ + selector: 'app-navigation-bar', + imports: [ + Avatar, + Divider, + HeliosIconComponent, + ProfileNavSectionComponent, + RouterLink, + SlicePipe, + TablerIconComponent, + Tooltip, + UserLockInfoComponent, + RouterLinkActive, + NgClass, + ], + templateUrl: './navigation-bar.component.html', +}) +export class NavigationBarComponent { + private keycloakService = inject(KeycloakService); + private permissionService = inject(PermissionService); + + // Toggle sidebar state + isExpanded = signal(this.loadSidebarState()); + + repositoryId = input.required(); + + repositoryQuery = injectQuery(() => ({ + ...getRepositoryByIdOptions({ path: { id: this.repositoryId() ?? 0 } }), + enabled: () => this.repositoryId() !== undefined, + })); + + isLoggedIn = computed(() => this.keycloakService.isLoggedIn()); + + items = computed(() => { + return [ + { + label: 'CI/CD', + icon: 'arrow-guide', + path: ['repo', this.repositoryId(), 'ci-cd'], + }, + { + label: 'Environments', + icon: 'server-cog', + path: ['repo', this.repositoryId(), 'environment'], + }, + ...(this.keycloakService.profile && this.permissionService.isAtLeastMaintainer() + ? [ + { + label: 'Release Management', + icon: 'rocket', + path: ['repo', this.repositoryId(), 'release'], + }, + { + label: 'Project Settings', + icon: 'adjustments-alt', + path: ['repo', this.repositoryId(), 'settings'], + }, + ] + : []), + ]; + }); + + login() { + this.keycloakService.login(); + } + + toggleSidebar() { + const newState = !this.isExpanded(); + this.isExpanded.set(newState); + this.saveSidebarState(newState); + } + + private saveSidebarState(state: boolean) { + localStorage.setItem('sidebarExpanded', JSON.stringify(state)); + } + + private loadSidebarState(): boolean { + return JSON.parse(localStorage.getItem('sidebarExpanded') || 'false'); + } +} diff --git a/client/src/app/components/page-heading/page-heading.component.html b/client/src/app/components/page-heading/page-heading.component.html index 09add34c1..5e5b251b0 100644 --- a/client/src/app/components/page-heading/page-heading.component.html +++ b/client/src/app/components/page-heading/page-heading.component.html @@ -1,9 +1,11 @@
- - - {{ repositoryQuery.data()?.nameWithOwner }} - - + @if (repositoryId()) { + + + {{ repositoryQuery.data()?.nameWithOwner }} + + + }

diff --git a/client/src/app/components/page-heading/page-heading.component.ts b/client/src/app/components/page-heading/page-heading.component.ts index 87f7100b1..6c5744f2c 100644 --- a/client/src/app/components/page-heading/page-heading.component.ts +++ b/client/src/app/components/page-heading/page-heading.component.ts @@ -9,7 +9,7 @@ import { DividerModule } from 'primeng/divider'; templateUrl: './page-heading.component.html', }) export class PageHeadingComponent { - repositoryId = input(); + repositoryId = input(); repositoryQuery = injectQuery(() => ({ ...getRepositoryByIdOptions({ path: { id: this.repositoryId() || 0 } }), diff --git a/client/src/app/components/profile-nav-section/profile-nav-section.component.html b/client/src/app/components/profile-nav-section/profile-nav-section.component.html index 218f4edb3..841e9b34e 100644 --- a/client/src/app/components/profile-nav-section/profile-nav-section.component.html +++ b/client/src/app/components/profile-nav-section/profile-nav-section.component.html @@ -1,14 +1,29 @@ -
@if (isLoggedIn()) { - +
+ + + @if (isExpanded()) { + {{ fullName() }} + } +
- } @else { - }
diff --git a/client/src/app/components/profile-nav-section/profile-nav-section.component.spec.ts b/client/src/app/components/profile-nav-section/profile-nav-section.component.spec.ts index 29be8d828..3234dca27 100644 --- a/client/src/app/components/profile-nav-section/profile-nav-section.component.spec.ts +++ b/client/src/app/components/profile-nav-section/profile-nav-section.component.spec.ts @@ -25,6 +25,7 @@ describe('ProfileNavSectionComponent', () => { fixture = TestBed.createComponent(ProfileNavSectionComponent); component = fixture.componentInstance; + fixture.componentRef.setInput('isExpanded', true); await fixture.whenStable(); }); diff --git a/client/src/app/components/profile-nav-section/profile-nav-section.component.ts b/client/src/app/components/profile-nav-section/profile-nav-section.component.ts index 1364bfe1f..8898a7d07 100644 --- a/client/src/app/components/profile-nav-section/profile-nav-section.component.ts +++ b/client/src/app/components/profile-nav-section/profile-nav-section.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, inject } from '@angular/core'; +import { Component, inject, input } from '@angular/core'; import { KeycloakService } from '@app/core/services/keycloak/keycloak.service'; import { IconsModule } from 'icons.module'; import { ButtonModule } from 'primeng/button'; @@ -8,18 +8,20 @@ import { ChipModule } from 'primeng/chip'; import { DataViewModule } from 'primeng/dataview'; import { TagModule } from 'primeng/tag'; import { ToastModule } from 'primeng/toast'; -import { TooltipModule } from 'primeng/tooltip'; +import { Tooltip } from 'primeng/tooltip'; import { DividerModule } from 'primeng/divider'; import { AvatarModule } from 'primeng/avatar'; @Component({ selector: 'app-profile-nav-section', - imports: [ToastModule, TooltipModule, DividerModule, AvatarModule, DataViewModule, ButtonModule, TagModule, CommonModule, CardModule, ChipModule, IconsModule], + imports: [ToastModule, Tooltip, DividerModule, AvatarModule, DataViewModule, ButtonModule, TagModule, CommonModule, CardModule, ChipModule, IconsModule], templateUrl: './profile-nav-section.component.html', }) export class ProfileNavSectionComponent { private keycloakService = inject(KeycloakService); + isExpanded = input.required(); + isLoggedIn() { return this.keycloakService.isLoggedIn(); } @@ -28,27 +30,29 @@ export class ProfileNavSectionComponent { this.keycloakService.logout(); } - initials() { + fullName() { if (!this.isLoggedIn()) { return ''; } const profile = this.keycloakService.profile; - return `${profile?.firstName?.charAt(0) ?? ''}${profile?.lastName?.charAt(0) ?? ''}`; + return `${profile?.firstName} ${profile?.lastName}`; } - fullName() { - if (!this.isLoggedIn()) { - return ''; - } - - const profile = this.keycloakService.profile; + getProfilePictureUrl() { + return this.keycloakService.getUserGithubProfilePictureUrl(); + } - return `${profile?.firstName} ${profile?.lastName}`; + getProfileUrl() { + return this.keycloakService.getUserGithubProfileUrl(); } login() { this.keycloakService.login(); } + + openProfile() { + window.open(this.getProfileUrl()); + } } diff --git a/client/src/app/components/pull-request-status-icon/pull-request-status-icon.component.html b/client/src/app/components/pull-request-status-icon/pull-request-status-icon.component.html new file mode 100644 index 000000000..f855e4f5a --- /dev/null +++ b/client/src/app/components/pull-request-status-icon/pull-request-status-icon.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/client/src/app/components/pull-request-status-icon/pull-request-status-icon.component.ts b/client/src/app/components/pull-request-status-icon/pull-request-status-icon.component.ts new file mode 100644 index 000000000..8befe9b9a --- /dev/null +++ b/client/src/app/components/pull-request-status-icon/pull-request-status-icon.component.ts @@ -0,0 +1,38 @@ +import { Component, computed, input } from '@angular/core'; +import { TablerIconComponent } from 'angular-tabler-icons'; +import { Tooltip } from 'primeng/tooltip'; +import { PullRequestInfoDto } from '@app/core/modules/openapi'; + +@Component({ + selector: 'app-pull-request-status-icon', + imports: [TablerIconComponent, Tooltip], + templateUrl: './pull-request-status-icon.component.html', +}) +export class PullRequestStatusIconComponent { + pullRequest = input(); + tooltipPosition = input('right'); + + iconName = computed(() => { + if (!this.pullRequest()) return 'question-mark'; + if (this.pullRequest()?.isMerged) return 'git-merge'; + if (this.pullRequest()?.state === 'CLOSED') return 'git-pull-request-closed'; + if (this.pullRequest()?.isDraft) return 'git-pull-request-draft'; + return 'git-pull-request'; // Default for open PRs + }); + + iconColor = computed(() => { + if (!this.pullRequest()) return 'text-gray-400'; + if (this.pullRequest()?.isMerged) return 'text-purple-500'; + if (this.pullRequest()?.state === 'CLOSED') return 'text-red-500'; + if (this.pullRequest()?.isDraft) return 'text-gray-600'; + return 'text-green-600'; // Default for open PRs + }); + + tooltipText = computed(() => { + if (!this.pullRequest()) return 'Unknown PR status'; + if (this.pullRequest()?.isMerged) return 'Merged'; + if (this.pullRequest()?.state === 'CLOSED') return 'Closed'; + if (this.pullRequest()?.isDraft) return 'Draft'; + return 'Open'; + }); +} diff --git a/client/src/app/components/pull-request-table/pull-request-table.component.html b/client/src/app/components/pull-request-table/pull-request-table.component.html index 26aeebaf0..639885d9d 100644 --- a/client/src/app/components/pull-request-table/pull-request-table.component.html +++ b/client/src/app/components/pull-request-table/pull-request-table.component.html @@ -55,7 +55,7 @@
- + @@ -106,7 +106,7 @@ } - + diff --git a/client/src/app/components/pull-request-table/pull-request-table.component.ts b/client/src/app/components/pull-request-table/pull-request-table.component.ts index ce126491e..01974921f 100644 --- a/client/src/app/components/pull-request-table/pull-request-table.component.ts +++ b/client/src/app/components/pull-request-table/pull-request-table.component.ts @@ -21,6 +21,7 @@ import { TimeAgoPipe } from '@app/pipes/time-ago.pipe'; import { FILTER_OPTIONS_TOKEN, SearchTableService } from '@app/core/services/search-table.service'; import { TableFilterComponent } from '../table-filter/table-filter.component'; import { WorkflowRunStatusComponent } from '@app/components/workflow-run-status-component/workflow-run-status.component'; +import { PullRequestStatusIconComponent } from '@app/components/pull-request-status-icon/pull-request-status-icon.component'; const FILTER_OPTIONS = [ { name: 'All pull requests', filter: (prs: PullRequestBaseInfoDto[]) => prs }, @@ -54,6 +55,7 @@ const FILTER_OPTIONS = [ TableFilterComponent, DividerModule, WorkflowRunStatusComponent, + PullRequestStatusIconComponent, ], providers: [SearchTableService, { provide: FILTER_OPTIONS_TOKEN, useValue: FILTER_OPTIONS }], templateUrl: './pull-request-table.component.html', @@ -80,22 +82,11 @@ export class PullRequestTableComponent { query = injectQuery(() => getAllPullRequestsOptions()); - getPrIconInfo(pr: PullRequestInfoDto): { icon: string; color: string; tooltip: string } { - if (pr.isMerged) { - return { icon: 'git-merge', color: 'text-purple-500', tooltip: 'Merged' }; - } else if (pr.state === 'CLOSED') { - return { icon: 'git-pull-request-closed', color: 'text-red-500', tooltip: 'Closed' }; - } else if (pr.isDraft) { - return { icon: 'git-pull-request-draft', color: 'text-gray-600', tooltip: 'Draft' }; - } else { - return { icon: 'git-pull-request', color: 'text-green-600', tooltip: 'Open' }; - } - } - filteredPrs = computed(() => this.searchTableService.activeFilter().filter(this.query.data() || [], this.keycloak.decodedToken()?.preferred_username)); - openPRExternal(pr: PullRequestInfoDto): void { + openPRExternal(event: Event, pr: PullRequestInfoDto): void { window.open(pr.htmlUrl, '_blank'); + event.stopPropagation(); } // TODO: Find a better way to handle color of labels diff --git a/client/src/app/components/report-problem-button/report-problem-button.component.html b/client/src/app/components/report-problem-button/report-problem-button.component.html index 736cf4b98..e4e4215c3 100644 --- a/client/src/app/components/report-problem-button/report-problem-button.component.html +++ b/client/src/app/components/report-problem-button/report-problem-button.component.html @@ -1,4 +1,4 @@ -
+
-
-} -
-
+ @if (!isLoggedIn()) { + +
+ You are currently using Helios in readonly mode. + +
+ } +
-
- - - - -
- @for (item of items(); track item.label) { - - - - } -
- - - +
+
-
+
@@ -39,4 +21,6 @@
+ +
diff --git a/client/src/app/pages/main-layout/main-layout.component.ts b/client/src/app/pages/main-layout/main-layout.component.ts index 603f8d0f2..9ae235cb1 100644 --- a/client/src/app/pages/main-layout/main-layout.component.ts +++ b/client/src/app/pages/main-layout/main-layout.component.ts @@ -1,11 +1,7 @@ -import { SlicePipe } from '@angular/common'; -import { Component, computed, inject, input, numberAttribute } from '@angular/core'; -import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; -import { ProfileNavSectionComponent } from '@app/components/profile-nav-section/profile-nav-section.component'; -import { getRepositoryByIdOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen'; +import { NgClass } from '@angular/common'; +import { Component, computed, inject, OnInit, signal } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { KeycloakService } from '@app/core/services/keycloak/keycloak.service'; -import { PermissionService } from '@app/core/services/permission.service'; -import { injectQuery } from '@tanstack/angular-query-experimental'; import { IconsModule } from 'icons.module'; import { AvatarModule } from 'primeng/avatar'; import { ButtonModule } from 'primeng/button'; @@ -13,8 +9,9 @@ import { CardModule } from 'primeng/card'; import { DividerModule } from 'primeng/divider'; import { ToastModule } from 'primeng/toast'; import { TooltipModule } from 'primeng/tooltip'; -import { HeliosIconComponent } from '../../components/helios-icon/helios-icon.component'; -import { UserLockInfoComponent } from '@app/components/user-lock-info/user-lock-info.component'; +import { FooterComponent } from '@app/components/footer/footer.component'; +import { NavigationBarComponent } from '@app/components/navigation-bar/navigation-bar.component'; +import { filter } from 'rxjs'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; @Component({ @@ -22,74 +19,94 @@ import { ConfirmDialogModule } from 'primeng/confirmdialog'; standalone: true, imports: [ RouterOutlet, - RouterLink, - SlicePipe, ToastModule, - RouterLinkActive, IconsModule, ButtonModule, TooltipModule, - HeliosIconComponent, DividerModule, AvatarModule, - ConfirmDialogModule, CardModule, - ProfileNavSectionComponent, - UserLockInfoComponent, + NgClass, + FooterComponent, + NavigationBarComponent, + ConfirmDialogModule, ], templateUrl: './main-layout.component.html', }) -export class MainLayoutComponent { +export class MainLayoutComponent implements OnInit { private keycloakService = inject(KeycloakService); - private permissionService = inject(PermissionService); + private route = inject(ActivatedRoute); + private router = inject(Router); + + repositoryId = signal(undefined); + isLoggedIn = computed(() => this.keycloakService.isLoggedIn()); + dynamicHeight = computed(() => (this.isLoggedIn() ? 'h-[calc(100vh-24px)]' : 'h-[calc(100vh-48px)]')); + + ngOnInit(): void { + // Initialize on first load (Refresh) + this.updateRepositoryId(); + + // Listen for route changes (After initial load) + this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => { + this.updateRepositoryId(); + }); + } - username = computed(() => (this.keycloakService.decodedToken()?.preferred_username || '') as string); + /** + * Updates the `repositoryId` signal based on the current route. + * + * This method traverses the activated route hierarchy to find and extract + * the `repositoryId` parameter. It ensures that `repositoryId` is set correctly + * even when navigating through deeply nested routes. + * + * ### Example Route Structure: + * Given the URL: `/repo/:repositoryId/ci-cd/pr` + * The Angular route hierarchy will be: + * - `repo/:repositoryId` (firstChild) + * - `ci-cd` (firstChild) + * - `pr` (firstChild, stops here) + * + * If the `repositoryId` is found, it is stored as a number. Otherwise, the value is set to `undefined`. + * + * If the user navigates to a non-repository page (e.g., `/about`, `/privacy`), `repositoryId` is cleared. + * + * **Note:** + * Even if the last child route (e.g., 'pr') does not contain `repositoryId`, + * Angular keeps parent route parameters accessible. + * Since `'repo/:repositoryId'` is an ancestor route, its parameter is still available. + * + * @returns {void} + */ + private updateRepositoryId(): void { + let child = this.route.firstChild; - repositoryId = input.required({ transform: numberAttribute }); + // Traverse to the last child route in the hierarchy + while (child?.firstChild) { + child = child.firstChild; + } - repositoryQuery = injectQuery(() => ({ - ...getRepositoryByIdOptions({ path: { id: this.repositoryId() } }), - enabled: () => !!this.repositoryId(), - })); + // If no child route exists, reset repositoryId and return + if (!child) { + this.repositoryId.set(undefined); + return; + } - logout() { - this.keycloakService.logout(); + // Attempt to extract 'repositoryId' from the deepest child route + const idFromSnapshot = child.snapshot.paramMap.get('repositoryId'); + + // Convert to a number if 'repositoryId' exists + let repositoryId: number | undefined; + if (idFromSnapshot && !isNaN(Number(idFromSnapshot))) { + repositoryId = Number(idFromSnapshot); + } else { + repositoryId = undefined; + } + + // Set repositoryId (either a valid number or undefined) + this.repositoryId.set(repositoryId); } login() { this.keycloakService.login(); } - - isLoggedIn = computed(() => this.keycloakService.isLoggedIn()); - - items = computed(() => { - const baseItems = [ - { - label: 'CI/CD', - icon: 'arrow-guide', - path: 'ci-cd', - }, - - { - label: 'Environments', - icon: 'server-cog', - path: 'environment', - }, - ...(this.keycloakService.profile && this.permissionService.isAtLeastMaintainer() - ? [ - { - label: 'Release Management', - icon: 'rocket', - path: 'release', - }, - { - label: 'Project Settings', - icon: 'adjustments-alt', - path: 'settings', - }, - ] - : []), - ]; - return baseItems; - }); } diff --git a/client/src/app/pages/privacy/privacy.component.html b/client/src/app/pages/privacy/privacy.component.html new file mode 100644 index 000000000..5245594af --- /dev/null +++ b/client/src/app/pages/privacy/privacy.component.html @@ -0,0 +1,127 @@ + +
Privacy Policy
+
+
+ The Research Group for Applied Education Technologies (referred to as AET in the following paragraphs) from the Technical University of Munich takes the protection of private + data seriously. We process automatically collected personal data obtained when you visit our website in compliance with applicable data protection regulations, in particular: +
    +
  • The General Data Protection Regulation (GDPR)
  • +
  • The Bavarian Data Protection Act (BayDSG), which applies to public institutions in Bavaria
  • +
  • The Telecommunications Telemedia Data Protection Act (TTDSG), which governs online services and the use of cookies.
  • +
+ Below, we inform you about the type, scope and purpose of the collection and use of personal data. +
+
+ +
+
+

Logging

+
+

+ The web servers of the AET are operated by the AET itself, based in Boltzmannstr. 3, 85748 Garching b. Munich. Every time our website is accessed, the web server temporarily + processes the following information in log files: +

+
+
    +
  • IP address of the requesting computer
  • +
  • Date and time of access
  • +
  • Name, URL and transferred data volume of the retrieved file
  • +
  • Access status (requested file transferred, not found, etc.)
  • +
  • Identification data of the browser and operating system used (if transmitted by the requesting web browser)
  • +
  • Web page from which access was made (if transmitted by the requesting web browser) The processing of the data in this log file can be done as follows:
  • +
+
+

The processing of the data in this log file takes place as follows:

+
+
    +
  • The log entries are continuously updated automatically evaluated in order to be able to detect attacks on the web server and react accordingly.
  • +
  • In individual cases, i.e. in the case of reported disruptions, errors and security incidents, a manual analysis is carried out.
  • +
  • The IP addresses contained in the log entries are not merged with other databases by AET, so that no conclusions can be drawn about individual persons.
  • +
+
+ +

Use and transfer of personal data

+
+

+ Our website can be used without providing personal data. All services that might require any form of personal data (e.g. registration for events, contact forms) are offered + on external sites, linked here. The use of contact data published as part of the imprint obligation by third parties to send unsolicited advertising and information material + is hereby prohibited. The operators of the pages reserve the right to take legal action in the event of the unsolicited sending of advertising information, such as spam + mails. +

+
+ +

Revocation of your consent to data processing

+
+

+ Some data processing operations require your express consent possible. You can revoke your consent that you have already given at any time. A message bye-mail is sufficient + for the revocation. The lawfulness of the data processing that took place up until the revocation remains unaffected by the revocation. +

+
+ +

Right to file a complaint with the responsible supervisory authority

+
+

+ If you believe that the processing of your personal data violates applicable data protection laws, you have the right to lodge a complaint with a supervisory authority. +

+ Since this project is developed at the Technical University of Munich (TUM), a public institution in Bavaria, the applicable law is the + Bavarian Data Protection Act (BayDSG), which supplements the General Data Protection Regulation (GDPR). The responsible supervisory + authority for enforcing these regulations is:

+ Bavarian State Commissioner for Data Protection (BayLfD)
+ Wagmüllerstraße 18
+ 80538 Munich
+ Germany
+ Phone: +49 89 212672-0
+ Fax: +49 89 212672-50
+ Email: poststelle@datenschutz-bayern.de
+ Website: https://www.datenschutz-bayern.de

+ Alternatively, you may contact the supervisory authority in your place of residence or workplace. The supervisory authority will inform you about the progress and outcome of + your complaint, including the possibility of a judicial remedy pursuant to Article 78 GDPR. +

+
+ +

Right to data portability

+
+

+ You have the right to request the data that we process automatically on the basis of your consent or in fulfillment of a contract to be handed over to you or a third party. + The data is provided in a machine-readable format. If you request the direct transfer of the data to another person responsible, this will only be done if it is technically + feasible. +

+
+ +

Right to information, correction, blocking, and deletion

+
+

+ You have at any time within the framework of the applicable legal provisions the right to request information about your stored personal data, the origin of the data, its + recipient and the purpose of the data processing, and if necessary, a right to correction, blocking or deletion of this data. You can contact us at any time via + krusche@tum.de regarding this and other questions on the subject of personal data. +

+
+ +

SSL/TLS encryption

+
+

+ For security reasons and to protect the transmission of confidential content that you send to us send as a site operator, our website uses an SSL/TLS encryption. This means + that data that you transmit via this website cannot be read by third parties. You can recognize an encrypted connection by the"https://" address line in your browser and by + the lock symbol in the browser line. +

+
+ +

E-mail security

+
+

+ If you e-mail us, your e-mail address will only be used for correspondence with you. Please note that data transmission on the Internet can have security gaps. Complete + protection of data from access by third parties is not possible. +

+
+ +

Name and contact details of the person responsible

+
+

+ Technical University of Munich
+ Postal address: Prof. Dr. Stephan Krusche (CIT–I1) Boltzmannstraße 3 85748 Garching b. München
+ Office: 01.07.044
+ E-mail: krusche@tum.de
+

+
+
+
diff --git a/client/src/app/pages/privacy/privacy.component.ts b/client/src/app/pages/privacy/privacy.component.ts new file mode 100644 index 000000000..70f5601ce --- /dev/null +++ b/client/src/app/pages/privacy/privacy.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { PageHeadingComponent } from '@app/components/page-heading/page-heading.component'; + +@Component({ + selector: 'app-privacy', + imports: [PageHeadingComponent], + templateUrl: './privacy.component.html', +}) +export class PrivacyComponent {} diff --git a/client/src/app/pages/pull-request-details/pull-request-details.component.html b/client/src/app/pages/pull-request-details/pull-request-details.component.html index 5f0c2e95c..edb04eff3 100644 --- a/client/src/app/pages/pull-request-details/pull-request-details.component.html +++ b/client/src/app/pages/pull-request-details/pull-request-details.component.html @@ -1,21 +1,36 @@ -
+
@if (query.data(); as pr) {
- Pull Request #{{ pr.number }} -
+ + + +

+ + +

+ #{{ pr.number }} +

+
+ + + @if (pr.author) {
- - {{ pr.state }} + + + + + {{ pr.author.name || pr.author.login }} opened a pull request from + + + + + + {{ pr.headRefName }} + - @if (pr.isDraft) { - DRAFT - } - @if (pr.isMerged) { - MERGED - }
-
+ }
} @else {
diff --git a/client/src/app/pages/pull-request-details/pull-request-details.component.ts b/client/src/app/pages/pull-request-details/pull-request-details.component.ts index 0b3d4e7bf..420b92226 100644 --- a/client/src/app/pages/pull-request-details/pull-request-details.component.ts +++ b/client/src/app/pages/pull-request-details/pull-request-details.component.ts @@ -1,6 +1,5 @@ import { Component, computed, inject, input } from '@angular/core'; import { MarkdownPipe } from '@app/core/modules/markdown/markdown.pipe'; - import { PipelineComponent, PipelineSelector } from '@app/components/pipeline/pipeline.component'; import { TagModule } from 'primeng/tag'; import { IconsModule } from 'icons.module'; @@ -11,10 +10,23 @@ import { injectQuery } from '@tanstack/angular-query-experimental'; import { SkeletonModule } from 'primeng/skeleton'; import { getPullRequestByRepositoryIdAndNumberOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen'; import { KeycloakService } from '@app/core/services/keycloak/keycloak.service'; +import { UserAvatarComponent } from '@app/components/user-avatar/user-avatar.component'; +import { PullRequestStatusIconComponent } from '@app/components/pull-request-status-icon/pull-request-status-icon.component'; @Component({ selector: 'app-branch-details', - imports: [InputTextModule, TagModule, IconsModule, ButtonModule, PipelineComponent, MarkdownPipe, DeploymentSelectionComponent, SkeletonModule], + imports: [ + InputTextModule, + TagModule, + IconsModule, + ButtonModule, + PipelineComponent, + MarkdownPipe, + DeploymentSelectionComponent, + SkeletonModule, + UserAvatarComponent, + PullRequestStatusIconComponent, + ], templateUrl: './pull-request-details.component.html', }) export class PullRequestDetailsComponent { diff --git a/client/src/app/pages/repository-overview/repository-overview.component.html b/client/src/app/pages/repository-overview/repository-overview.component.html index 667ddd24a..0bb7051d6 100644 --- a/client/src/app/pages/repository-overview/repository-overview.component.html +++ b/client/src/app/pages/repository-overview/repository-overview.component.html @@ -1,48 +1,79 @@ -
-
-
- - - -
-
-
- -
-
-
Connected Repositories
-
- In the repository list you can find all of the repositories that were added to Helios. -
+ +
+
+
Connected Repositories
+
+ In the repository list you can find all of the repositories that were added to Helios. +
-
-
- @for (item of query.data(); track item.id) { -
-
-
- {{ item.name }} - -
-
{{ item.description }}
-
-
- -
-
- } @empty { -
- -
There are currently no repositories connected to Helios.
-
Please contact an administrator to connect your repository.
-
- } +@if (query.isPending() || query.isError()) { + + + + + + + + + + +
+ + +
+
+ + + +
-
- -
-
+ + + + + + + + + +} @else { + + + + Repository + + + + + + +
+ {{ project.name }} +
+ @if (project.description) { +
+ {{ project.description }} +
+ } + - -
-
+ + + + + + + + + + +
+ + There are currently no repositories connected to Helios. + Please install the Helios GitHub App to connect your repositories. +
+ + +
+ +} diff --git a/client/src/app/pages/repository-overview/repository-overview.component.ts b/client/src/app/pages/repository-overview/repository-overview.component.ts index 82df1d068..ad5a3b8d2 100644 --- a/client/src/app/pages/repository-overview/repository-overview.component.ts +++ b/client/src/app/pages/repository-overview/repository-overview.component.ts @@ -1,9 +1,6 @@ -import { Component, computed, inject, viewChild } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; -import { ConnectRepoComponent } from '@app/components/connect-repo/connect-repo.component'; -import { HeliosIconComponent } from '@app/components/helios-icon/helios-icon.component'; import { PageHeadingComponent } from '@app/components/page-heading/page-heading.component'; -import { ProfileNavSectionComponent } from '@app/components/profile-nav-section/profile-nav-section.component'; import { RepositoryInfoDto } from '@app/core/modules/openapi'; import { getAllRepositoriesOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen'; import { injectQuery } from '@tanstack/angular-query-experimental'; @@ -14,38 +11,26 @@ import { ChipModule } from 'primeng/chip'; import { DataViewModule } from 'primeng/dataview'; import { TagModule } from 'primeng/tag'; import { ToastModule } from 'primeng/toast'; +import { Skeleton } from 'primeng/skeleton'; +import { TableModule } from 'primeng/table'; @Component({ selector: 'app-repository-overview', - imports: [ - DataViewModule, - ButtonModule, - TagModule, - CardModule, - ChipModule, - IconsModule, - ConnectRepoComponent, - PageHeadingComponent, - ToastModule, - ProfileNavSectionComponent, - HeliosIconComponent, - ], + imports: [DataViewModule, ButtonModule, TagModule, CardModule, ChipModule, IconsModule, PageHeadingComponent, ToastModule, Skeleton, TableModule], templateUrl: './repository-overview.component.html', }) export class RepositoryOverviewComponent { private router = inject(Router); query = injectQuery(() => getAllRepositoriesOptions()); - repositories = computed(() => this.query.data()); - - readonly repositoryConnection = viewChild.required(ConnectRepoComponent); - - showDialog() { - this.repositoryConnection().show(); - } navigateToProject(repository: RepositoryInfoDto) { console.log('Navigating to project', repository); this.router.navigate(['repo', repository.id.toString(), 'ci-cd']); } + + openProjectExternal(event: Event, repository: RepositoryInfoDto) { + window.open(repository.htmlUrl, '_blank'); + event.stopPropagation(); + } } diff --git a/client/src/icons.module.ts b/client/src/icons.module.ts index d9030f2c4..b63d35d10 100644 --- a/client/src/icons.module.ts +++ b/client/src/icons.module.ts @@ -59,6 +59,7 @@ import { IconX, IconChevronsRight, IconCircleChevronsRight, + IconFileText, } from 'angular-tabler-icons/icons'; // Select some icons (use an object, not an array) @@ -120,6 +121,7 @@ const icons = { IconTag, IconTrash, IconX, + IconFileText, }; @NgModule({