Skip to content

Commit

Permalink
create activity dashboard(#199)
Browse files Browse the repository at this point in the history
  • Loading branch information
iam-flo committed Jan 17, 2025
1 parent d4b159b commit 587e6c6
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 1 deletion.
4 changes: 3 additions & 1 deletion webapp/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }
]
}
];
1 change: 1 addition & 0 deletions webapp/src/app/core/header/header.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
</a>
@if (user()?.roles?.includes('admin')) {
<a hlmBtn variant="link" routerLink="/workspace">Workspace</a>
<a hlmBtn variant="link" [routerLink]="'/activity/' + user()!.username">Activity</a>
}
</div>
@if (user()?.roles?.includes('mentor_access')) {
Expand Down
45 changes: 45 additions & 0 deletions webapp/src/app/home/activity/activity-dashboard.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<div class="flex flex-col items-center">
<div class="grid grid-cols-1 xl:grid-cols-5 gap-y-4 xl:gap-8">
<div class="space-y-2 col-span-1">
<div class="flex flex-col gap-2 mb-4">
<h1 class="text-xl font-semibold">Activities</h1>
<p>
You currently have <span class="font-semibold">{{ numberOfPullRequests() }}</span> open pull requests and
<span class="font-semibold">{{ numberOfBadPractices() }}</span> detected bad practices.
</p>
</div>
</div>
<div class="col-span-3">
<div class="flex flex-col justify-between gap-2">
<span class="flex flex-row justify-between items-center">
<h1 class="text-xl font-semibold">Your open pull requests</h1>
<button hlmBtn variant="outline" aria-describedby=">Detect bad practices" class="gap-2" (click)="this.detectBadPractices()">
<lucide-angular [img]="RefreshCcw" class="size-4" />
<span>Detect bad practices</span>
</button>
</span>
<div class="flex flex-col gap-4">
@if (query.data()?.pullRequests) {
@for (pullRequest of query.data()?.pullRequests; track pullRequest.id) {
<app-pull-request-bad-practice-card
[title]="pullRequest.title"
[number]="pullRequest.number"
[additions]="pullRequest.additions"
[deletions]="pullRequest.deletions"
[htmlUrl]="pullRequest.htmlUrl"
[repositoryName]="pullRequest.repository?.name"
[createdAt]="pullRequest.createdAt"
[state]="pullRequest.state"
[isDraft]="pullRequest.isDraft"
[isMerged]="pullRequest.isMerged"
[pullRequestLabels]="pullRequest.labels"
[badPractices]="pullRequest.badPractices"
>
</app-pull-request-bad-practice-card>
}
}
</div>
</div>
</div>
</div>
</div>
39 changes: 39 additions & 0 deletions webapp/src/app/home/activity/activity-dashboard.component.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="flex flex-row justify-start items-center gap-2">
<div>
@if (resolved()) {
<ng-icon [svg]="octCheck" size="24" class="text-github-success-foreground" />
} @else {
<ng-icon [svg]="octX" size="24" class="text-github-danger-foreground" />
}
</div>
<div class="flex flex-col">
<h1 class="text-lg font-semibold">{{ title() }}</h1>
<p class="text-sm">{{ description() }}</p>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -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<string>();
description = input<string>();
resolved = input<boolean>();
}
26 changes: 26 additions & 0 deletions webapp/src/app/user/bad-practice-card/bad-practice-card.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Meta, StoryObj } from '@storybook/angular';
import { BadPracticeCardComponent } from './bad-practice-card.component';

const meta: Meta<BadPracticeCardComponent> = {
component: BadPracticeCardComponent,
tags: ['autodocs'] // Auto-generate docs if enabled
};

export default meta;

type Story = StoryObj<BadPracticeCardComponent>;

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.'
}
};*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<brn-collapsible>
<a hlmCard variant="profile">
<div>
<div class="flex justify-between items-center text-sm text-github-muted-foreground">
<span class="font-medium flex justify-center items-center space-x-1">
@if (isLoading()) {
<hlm-skeleton class="size-5 bg-green-500/30"></hlm-skeleton>
<hlm-skeleton class="h-4 w-16 lg:w-36"></hlm-skeleton>
} @else {
<ng-icon [svg]="issueIconAndColor().icon" size="18" [class]="'mr-1 ' + issueIconAndColor().color"></ng-icon>
{{ repositoryName() }} #{{ number() }} on {{ displayCreated().format('MMM D') }}
}
</span>
<span class="font-medium flex justify-center items-center gap-1">
@if (isLoading()) {
<hlm-skeleton class="h-4 w-16 lg:w-36"></hlm-skeleton>
<hlm-skeleton class="size-5"></hlm-skeleton>
} @else {
@if (badPractices()?.length == 1) {
<span> {{ badPractices()?.length }} bad practice detected </span>
} @else {
<span> {{ badPractices()?.length }} bad practices detected </span>
}
<button brnCollapsibleTrigger hlmBtn variant="ghost">
<ng-icon [svg]="octFold" size="18" class="text-github-muted-foreground"></ng-icon>
</button>
}
</span>
</div>
<div class="flex justify-between font-medium">
<span>
@if (isLoading()) {
<hlm-skeleton class="h-6 w-3/4"></hlm-skeleton>
} @else {
<div [innerHTML]="displayTitle()" class="truncate"></div>
}
</span>
<span class="flex items-center space-x-2">
@if (isLoading()) {
<hlm-skeleton class="h-4 w-8 bg-green-500/30"></hlm-skeleton>
<hlm-skeleton class="h-4 w-8 bg-destructive/20"></hlm-skeleton>
} @else {
<span class="text-github-success-foreground font-bold">+{{ additions() }}</span>
<span class="text-github-danger-foreground font-bold">-{{ deletions() }}</span>
}
</span>
</div>
@if (!isLoading()) {
<div class="flex flex-wrap gap-2 p-0 space-x-0">
@for (label of pullRequestLabels(); track label.name) {
<app-github-label [label]="label"></app-github-label>
}
</div>
}
</div>
@if (!isLoading()) {
<div class="gap-2 p-0 space-x-0 text-left">
<brn-collapsible-content>
@for (badpractice of badPractices(); track badpractice.title) {
<brn-separator hlmSeparator />
<app-bad-practice-card [title]="badpractice.title" [description]="badpractice.description" [resolved]="false"></app-bad-practice-card>
}
</brn-collapsible-content>
</div>
}
</a>
</brn-collapsible>
Original file line number Diff line number Diff line change
@@ -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<string>();
number = input<number>();
additions = input<number>();
deletions = input<number>();
htmlUrl = input<string>();
repositoryName = input<string>();
createdAt = input<string>();
state = input<PullRequestInfo.StateEnum>();
isDraft = input<boolean>();
isMerged = input<boolean>();
pullRequestLabels = input<Array<LabelInfo>>();
badPractices = input<Array<PullRequestBadPractice>>();

displayCreated = computed(() => dayjs(this.createdAt()));
displayTitle = computed(() => (this.title() ?? '').replace(/`([^`]+)`/g, '<code class="textCode">$1</code>'));
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 };
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Meta, StoryObj } from '@storybook/angular';
import { PullRequestBadPracticeCardComponent } from './pull-request-bad-practice-card.component';

const meta: Meta<PullRequestBadPracticeCardComponent> = {
component: PullRequestBadPracticeCardComponent,
tags: ['autodocs'] // Auto-generate docs if enabled
};

export default meta;

type Story = StoryObj<PullRequestBadPracticeCardComponent>;

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
}
};

0 comments on commit 587e6c6

Please sign in to comment.