From 450e58ee44ff7142cb6216f72353fa19f60efe43 Mon Sep 17 00:00:00 2001 From: Florian Ehrenstorfer Date: Fri, 17 Jan 2025 18:23:33 +0100 Subject: [PATCH] create activity dashboard(#199) --- webapp/src/app/app.routes.ts | 4 +- .../src/app/core/header/header.component.html | 1 + .../activity-dashboard.component.html | 45 ++++++++++ .../activity/activity-dashboard.component.ts | 39 +++++++++ .../bad-practice-card.component.html | 13 +++ .../bad-practice-card.component.ts | 20 +++++ .../bad-practice-card.stories.ts | 26 ++++++ ...l-request-bad-practice-card.component.html | 67 +++++++++++++++ ...ull-request-bad-practice-card.component.ts | 85 +++++++++++++++++++ .../pull-request-bad-practice-card.stories.ts | 70 +++++++++++++++ 10 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 webapp/src/app/home/activity/activity-dashboard.component.html create mode 100644 webapp/src/app/home/activity/activity-dashboard.component.ts create mode 100644 webapp/src/app/user/bad-practice-card/bad-practice-card.component.html create mode 100644 webapp/src/app/user/bad-practice-card/bad-practice-card.component.ts create mode 100644 webapp/src/app/user/bad-practice-card/bad-practice-card.stories.ts create mode 100644 webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.component.html create mode 100644 webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.component.ts create mode 100644 webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.stories.ts diff --git a/webapp/src/app/app.routes.ts b/webapp/src/app/app.routes.ts index bba4e278..b5645aca 100644 --- a/webapp/src/app/app.routes.ts +++ b/webapp/src/app/app.routes.ts @@ -13,6 +13,7 @@ import { PrivacyComponent } from '@app/legal/privacy.component'; import { AdminGuard } from '@app/core/security/admin.guard'; import { AuthGuard } from '@app/core/security/auth.guard'; import { MentorGuard } from '@app/core/security/mentor.guard'; +import { ActivityDashboardComponent } from '@app/home/activity/activity-dashboard.component'; export const routes: Routes = [ // Public routes @@ -49,7 +50,8 @@ export const routes: Routes = [ { path: 'user/:id', component: UserProfileComponent }, { path: 'settings', component: SettingsComponent }, { path: 'mentor', component: MentorComponent, canActivate: [MentorGuard] }, - { path: 'workspace', component: WorkspaceComponent, canActivate: [AdminGuard] } + { path: 'workspace', component: WorkspaceComponent, canActivate: [AdminGuard] }, + { path: 'activity/:id', component: ActivityDashboardComponent } ] } ]; diff --git a/webapp/src/app/core/header/header.component.html b/webapp/src/app/core/header/header.component.html index 13f914d0..b91890ad 100644 --- a/webapp/src/app/core/header/header.component.html +++ b/webapp/src/app/core/header/header.component.html @@ -9,6 +9,7 @@ @if (user()?.roles?.includes('admin')) { Workspace + Activity } @if (user()?.roles?.includes('mentor_access')) { diff --git a/webapp/src/app/home/activity/activity-dashboard.component.html b/webapp/src/app/home/activity/activity-dashboard.component.html new file mode 100644 index 00000000..e52ba271 --- /dev/null +++ b/webapp/src/app/home/activity/activity-dashboard.component.html @@ -0,0 +1,45 @@ +
+
+
+
+

Activities

+

+ You currently have {{ numberOfPullRequests() }} open pull requests and + {{ numberOfBadPractices() }} detected bad practices. +

+
+
+
+
+ +

Your open pull requests

+ +
+
+ @if (query.data()?.pullRequests) { + @for (pullRequest of query.data()?.pullRequests; track pullRequest.id) { + + + } + } +
+
+
+
+
diff --git a/webapp/src/app/home/activity/activity-dashboard.component.ts b/webapp/src/app/home/activity/activity-dashboard.component.ts new file mode 100644 index 00000000..5ac2bde0 --- /dev/null +++ b/webapp/src/app/home/activity/activity-dashboard.component.ts @@ -0,0 +1,39 @@ +import { Component, computed, inject } from '@angular/core'; +import { ActivityService } from '@app/core/modules/openapi'; +import { injectQuery } from '@tanstack/angular-query-experimental'; +import { combineLatest, lastValueFrom, map, timer } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { PullRequestBadPracticeCardComponent } from '@app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.component'; +import { LucideAngularModule, RefreshCcw } from 'lucide-angular'; +import { HlmButtonDirective } from '@spartan-ng/ui-button-helm'; + +@Component({ + selector: 'app-activity-dashboard', + standalone: true, + imports: [PullRequestBadPracticeCardComponent, LucideAngularModule, HlmButtonDirective], + templateUrl: './activity-dashboard.component.html', + styles: `` +}) +export class ActivityDashboardComponent { + activityService = inject(ActivityService); + + protected userLogin: string | null = null; + protected numberOfPullRequests = computed(() => this.query.data()?.pullRequests?.length ?? 0); + protected numberOfBadPractices = computed(() => this.query.data()?.pullRequests?.reduce((acc, pr) => acc + (pr.badPractices?.length ?? 0), 0) ?? 0); + + constructor(private route: ActivatedRoute) { + this.userLogin = this.route.snapshot.paramMap.get('id'); + } + + query = injectQuery(() => ({ + queryKey: ['user', { id: this.userLogin }], + enabled: !!this.userLogin, + queryFn: async () => lastValueFrom(combineLatest([this.activityService.getActivityByUser(this.userLogin!), timer(400)]).pipe(map(([activity]) => activity))) + })); + + detectBadPractices = () => { + console.log('Detecting bad practices'); + //this.activityService.detectBadPractices(this.userLogin!).subscribe(); + }; + protected readonly RefreshCcw = RefreshCcw; +} diff --git a/webapp/src/app/user/bad-practice-card/bad-practice-card.component.html b/webapp/src/app/user/bad-practice-card/bad-practice-card.component.html new file mode 100644 index 00000000..0e317fde --- /dev/null +++ b/webapp/src/app/user/bad-practice-card/bad-practice-card.component.html @@ -0,0 +1,13 @@ +
+
+ @if (resolved()) { + + } @else { + + } +
+
+

{{ title() }}

+

{{ description() }}

+
+
diff --git a/webapp/src/app/user/bad-practice-card/bad-practice-card.component.ts b/webapp/src/app/user/bad-practice-card/bad-practice-card.component.ts new file mode 100644 index 00000000..0954450b --- /dev/null +++ b/webapp/src/app/user/bad-practice-card/bad-practice-card.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { HlmCardModule } from '@spartan-ng/ui-card-helm'; +import { NgIcon } from '@ng-icons/core'; +import { octCheck, octX } from '@ng-icons/octicons'; + +@Component({ + selector: 'app-bad-practice-card', + standalone: true, + imports: [HlmCardModule, NgIcon], + templateUrl: './bad-practice-card.component.html', + styles: `` +}) +export class BadPracticeCardComponent { + protected readonly octCheck = octCheck; + protected readonly octX = octX; + + title = input(); + description = input(); + resolved = input(); +} diff --git a/webapp/src/app/user/bad-practice-card/bad-practice-card.stories.ts b/webapp/src/app/user/bad-practice-card/bad-practice-card.stories.ts new file mode 100644 index 00000000..2674cfab --- /dev/null +++ b/webapp/src/app/user/bad-practice-card/bad-practice-card.stories.ts @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from '@storybook/angular'; +import { BadPracticeCardComponent } from './bad-practice-card.component'; + +const meta: Meta = { + component: BadPracticeCardComponent, + tags: ['autodocs'] // Auto-generate docs if enabled +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Avoid using any type', + description: 'Using the any type defeats the purpose of TypeScript.', + resolved: false + } +}; +/* +export const isLoading: Story = { + args: { + title: 'Avoid using any type', + description: 'Using the any type defeats the purpose of TypeScript.' + } +};*/ diff --git a/webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.component.html b/webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.component.html new file mode 100644 index 00000000..b6a8d136 --- /dev/null +++ b/webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.component.html @@ -0,0 +1,67 @@ + + +
+
+ + @if (isLoading()) { + + + } @else { + + {{ repositoryName() }} #{{ number() }} on {{ displayCreated().format('MMM D') }} + } + + + @if (isLoading()) { + + + } @else { + @if (badPractices()?.length == 1) { + {{ badPractices()?.length }} bad practice detected + } @else { + {{ badPractices()?.length }} bad practices detected + } + + } + +
+
+ + @if (isLoading()) { + + } @else { +
+ } +
+ + @if (isLoading()) { + + + } @else { + +{{ additions() }} + -{{ deletions() }} + } + +
+ @if (!isLoading()) { +
+ @for (label of pullRequestLabels(); track label.name) { + + } +
+ } +
+ @if (!isLoading()) { +
+ + @for (badpractice of badPractices(); track badpractice.title) { + + + } + +
+ } +
+
diff --git a/webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.component.ts b/webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.component.ts new file mode 100644 index 00000000..598ccb05 --- /dev/null +++ b/webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.component.ts @@ -0,0 +1,85 @@ +import { Component, computed, input } from '@angular/core'; +import { PullRequestInfo, LabelInfo, PullRequestBadPractice } from '@app/core/modules/openapi'; +import { NgIcon } from '@ng-icons/core'; +import { octCheck, octComment, octFileDiff, octGitPullRequest, octGitPullRequestClosed, octGitPullRequestDraft, octGitMerge, octX, octFold } from '@ng-icons/octicons'; +import { HlmCardModule } from '@spartan-ng/ui-card-helm'; +import { HlmSkeletonComponent } from '@spartan-ng/ui-skeleton-helm'; + +import dayjs from 'dayjs'; +import { BadPracticeCardComponent } from '@app/user/bad-practice-card/bad-practice-card.component'; +import { BrnSeparatorComponent } from '@spartan-ng/ui-separator-brain'; +import { HlmSeparatorDirective } from '@spartan-ng/ui-separator-helm'; +import { BrnCollapsibleComponent, BrnCollapsibleContentComponent, BrnCollapsibleTriggerDirective } from '@spartan-ng/ui-collapsible-brain'; +import { HlmButtonDirective } from '@spartan-ng/ui-button-helm'; +import { GithubLabelComponent } from '@app/ui/github-label/github-label.component'; +import { cn } from '@app/utils'; + +@Component({ + selector: 'app-pull-request-bad-practice-card', + templateUrl: './pull-request-bad-practice-card.component.html', + imports: [ + NgIcon, + HlmCardModule, + HlmSkeletonComponent, + BadPracticeCardComponent, + BrnSeparatorComponent, + HlmSeparatorDirective, + BrnCollapsibleComponent, + BrnCollapsibleContentComponent, + BrnCollapsibleTriggerDirective, + HlmButtonDirective, + GithubLabelComponent + ], + standalone: true +}) +export class PullRequestBadPracticeCardComponent { + protected readonly octCheck = octCheck; + protected readonly octX = octX; + protected readonly octComment = octComment; + protected readonly octFileDiff = octFileDiff; + protected readonly octFold = octFold; + + isLoading = input(false); + class = input(''); + title = input(); + number = input(); + additions = input(); + deletions = input(); + htmlUrl = input(); + repositoryName = input(); + createdAt = input(); + state = input(); + isDraft = input(); + isMerged = input(); + pullRequestLabels = input>(); + badPractices = input>(); + + displayCreated = computed(() => dayjs(this.createdAt())); + displayTitle = computed(() => (this.title() ?? '').replace(/`([^`]+)`/g, '$1')); + computedClass = computed(() => cn('w-full', !this.isLoading() ? 'hover:bg-accent/50 cursor-pointer' : '', this.class())); + + issueIconAndColor = computed(() => { + var icon: string; + var color: string; + + if (this.state() === PullRequestInfo.StateEnum.Open) { + if (this.isDraft()) { + icon = octGitPullRequestDraft; + color = 'text-github-muted-foreground'; + } else { + icon = octGitPullRequest; + color = 'text-github-open-foreground'; + } + } else { + if (this.isMerged()) { + icon = octGitMerge; + color = 'text-github-done-foreground'; + } else { + icon = octGitPullRequestClosed; + color = 'text-github-closed-foreground'; + } + } + + return { icon, color }; + }); +} diff --git a/webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.stories.ts b/webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.stories.ts new file mode 100644 index 00000000..54482f24 --- /dev/null +++ b/webapp/src/app/user/pull-request-bad-practice-card/pull-request-bad-practice-card.stories.ts @@ -0,0 +1,70 @@ +import { Meta, StoryObj } from '@storybook/angular'; +import { PullRequestBadPracticeCardComponent } from './pull-request-bad-practice-card.component'; + +const meta: Meta = { + component: PullRequestBadPracticeCardComponent, + tags: ['autodocs'] // Auto-generate docs if enabled +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Add feature X', + number: 12, + additions: 10, + deletions: 5, + htmlUrl: 'http://example.com', + state: 'OPEN', + isDraft: false, + isMerged: false, + repositoryName: 'Artemis', + createdAt: '2024-01-01', + pullRequestLabels: [ + { id: 1, name: 'bug', color: 'f00000' }, + { id: 2, name: 'enhancement', color: '008000' } + ], + badPractices: [ + { + title: 'Avoid using any type', + description: 'Using the any type defeats the purpose of TypeScript.' + }, + { + title: 'Unchecked checkbox in description', + description: 'Unchecked checkboxes in the description are not allowed.' + } + ] + } +}; + +export const isLoading: Story = { + args: { + title: 'Add feature X', + number: 12, + additions: 10, + deletions: 5, + htmlUrl: 'http://example.com', + state: 'OPEN', + isDraft: false, + isMerged: false, + repositoryName: 'Artemis', + createdAt: '2024-01-01', + pullRequestLabels: [ + { id: 1, name: 'bug', color: 'f00000' }, + { id: 2, name: 'enhancement', color: '008000' } + ], + badPractices: [ + { + title: 'Avoid using any type', + description: 'Using the any type defeats the purpose of TypeScript.' + }, + { + title: 'Unchecked checkbox in description', + description: 'Unchecked checkboxes in the description are not allowed.' + } + ], + isLoading: true + } +};