mirror of https://github.com/ghostfolio/ghostfolio
FabioAzevedo0619
2 years ago
committed by
GitHub
24 changed files with 678 additions and 16 deletions
@ -0,0 +1,9 @@ |
|||
import { IsString } from 'class-validator'; |
|||
|
|||
export class CreatePlatformDto { |
|||
@IsString() |
|||
name: string; |
|||
|
|||
@IsString() |
|||
url: string; |
|||
} |
@ -0,0 +1,113 @@ |
|||
import { |
|||
Body, |
|||
Controller, |
|||
Delete, |
|||
Get, |
|||
HttpException, |
|||
Inject, |
|||
Param, |
|||
Post, |
|||
Put, |
|||
UseGuards |
|||
} from '@nestjs/common'; |
|||
import { PlatformService } from './platform.service'; |
|||
import { AuthGuard } from '@nestjs/passport'; |
|||
import { Platform } from '@prisma/client'; |
|||
import { CreatePlatformDto } from './create-platform.dto'; |
|||
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; |
|||
import { RequestWithUser } from '@ghostfolio/common/types'; |
|||
import { REQUEST } from '@nestjs/core'; |
|||
import { getReasonPhrase, StatusCodes } from 'http-status-codes'; |
|||
import { UpdatePlatformDto } from './update-platform.dto'; |
|||
|
|||
@Controller('platform') |
|||
export class PlatformController { |
|||
public constructor( |
|||
private readonly platformService: PlatformService, |
|||
@Inject(REQUEST) private readonly request: RequestWithUser |
|||
) {} |
|||
|
|||
@Get() |
|||
@UseGuards(AuthGuard('jwt')) |
|||
public async getPlatforms(): Promise<Platform[]> { |
|||
return this.platformService.getPlatforms(); |
|||
} |
|||
|
|||
@Post() |
|||
@UseGuards(AuthGuard('jwt')) |
|||
public async createPlatform( |
|||
@Body() data: CreatePlatformDto |
|||
): Promise<Platform> { |
|||
if ( |
|||
!hasPermission(this.request.user.permissions, permissions.createPlatform) |
|||
) { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.FORBIDDEN), |
|||
StatusCodes.FORBIDDEN |
|||
); |
|||
} |
|||
return this.platformService.createPlatform(data); |
|||
} |
|||
|
|||
@Put(':id') |
|||
@UseGuards(AuthGuard('jwt')) |
|||
public async updatePlatform( |
|||
@Param('id') id: string, |
|||
@Body() data: UpdatePlatformDto |
|||
) { |
|||
if ( |
|||
!hasPermission(this.request.user.permissions, permissions.updatePlatform) |
|||
) { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.FORBIDDEN), |
|||
StatusCodes.FORBIDDEN |
|||
); |
|||
} |
|||
|
|||
const originalPlatform = await this.platformService.getPlatform({ |
|||
id |
|||
}); |
|||
|
|||
if (!originalPlatform) { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.FORBIDDEN), |
|||
StatusCodes.FORBIDDEN |
|||
); |
|||
} |
|||
|
|||
return this.platformService.updatePlatform({ |
|||
data: { |
|||
...data |
|||
}, |
|||
where: { |
|||
id |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Delete(':id') |
|||
@UseGuards(AuthGuard('jwt')) |
|||
public async deletePlatform(@Param('id') id: string) { |
|||
if ( |
|||
!hasPermission(this.request.user.permissions, permissions.deletePlatform) |
|||
) { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.FORBIDDEN), |
|||
StatusCodes.FORBIDDEN |
|||
); |
|||
} |
|||
|
|||
const originalPlatform = await this.platformService.getPlatform({ |
|||
id |
|||
}); |
|||
|
|||
if (!originalPlatform) { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.FORBIDDEN), |
|||
StatusCodes.FORBIDDEN |
|||
); |
|||
} |
|||
|
|||
return this.platformService.deletePlatform({ id }); |
|||
} |
|||
} |
@ -1,9 +1,11 @@ |
|||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { PlatformController } from './platform.controller'; |
|||
import { PlatformService } from './platform.service'; |
|||
|
|||
@Module({ |
|||
controllers: [PlatformController], |
|||
exports: [PlatformService], |
|||
imports: [PrismaModule], |
|||
providers: [PlatformService] |
@ -0,0 +1,45 @@ |
|||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; |
|||
import { Injectable } from '@nestjs/common'; |
|||
import { Platform, Prisma } from '@prisma/client'; |
|||
|
|||
@Injectable() |
|||
export class PlatformService { |
|||
public constructor(private readonly prismaService: PrismaService) {} |
|||
|
|||
public async getPlatforms(): Promise<Platform[]> { |
|||
return this.prismaService.platform.findMany(); |
|||
} |
|||
|
|||
public async getPlatform( |
|||
platformWhereUniqueInput: Prisma.PlatformWhereUniqueInput |
|||
): Promise<Platform> { |
|||
return this.prismaService.platform.findUnique({ |
|||
where: platformWhereUniqueInput |
|||
}); |
|||
} |
|||
|
|||
public async createPlatform(data: Prisma.PlatformCreateInput) { |
|||
return this.prismaService.platform.create({ |
|||
data |
|||
}); |
|||
} |
|||
|
|||
public async updatePlatform({ |
|||
data, |
|||
where |
|||
}: { |
|||
data: Prisma.PlatformUpdateInput; |
|||
where: Prisma.PlatformWhereUniqueInput; |
|||
}): Promise<Platform> { |
|||
return this.prismaService.platform.update({ |
|||
data, |
|||
where |
|||
}); |
|||
} |
|||
|
|||
public async deletePlatform( |
|||
where: Prisma.PlatformWhereUniqueInput |
|||
): Promise<Platform> { |
|||
return this.prismaService.platform.delete({ where }); |
|||
} |
|||
} |
@ -0,0 +1,12 @@ |
|||
import { IsString } from 'class-validator'; |
|||
|
|||
export class UpdatePlatformDto { |
|||
@IsString() |
|||
id: string; |
|||
|
|||
@IsString() |
|||
name: string; |
|||
|
|||
@IsString() |
|||
url: string; |
|||
} |
@ -1,11 +0,0 @@ |
|||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; |
|||
import { Injectable } from '@nestjs/common'; |
|||
|
|||
@Injectable() |
|||
export class PlatformService { |
|||
public constructor(private readonly prismaService: PrismaService) {} |
|||
|
|||
public async get() { |
|||
return this.prismaService.platform.findMany(); |
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Subject } from 'rxjs'; |
|||
|
|||
import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces'; |
|||
|
|||
@Component({ |
|||
host: { class: 'h-100' }, |
|||
selector: 'gf-create-or-update-platform-dialog', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
styleUrls: ['./create-or-update-platform-dialog.scss'], |
|||
templateUrl: 'create-or-update-platform-dialog.html' |
|||
}) |
|||
export class CreateOrUpdatePlatformDialog { |
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor( |
|||
public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog>, |
|||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams |
|||
) {} |
|||
|
|||
public onCancel() { |
|||
this.dialogRef.close(); |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
} |
@ -0,0 +1,29 @@ |
|||
<form #addPlatformForm="ngForm" class="d-flex flex-column h-100"> |
|||
<h1 *ngIf="data.platform.id" i18n mat-dialog-title>Update platform</h1> |
|||
<h1 *ngIf="!data.platform.id" i18n mat-dialog-title>Add platform</h1> |
|||
<div class="flex-grow-1 py-3" mat-dialog-content> |
|||
<div> |
|||
<mat-form-field appearance="outline" class="w-100"> |
|||
<mat-label i18n>Name</mat-label> |
|||
<input matInput name="name" required [(ngModel)]="data.platform.name" /> |
|||
</mat-form-field> |
|||
</div> |
|||
<div> |
|||
<mat-form-field appearance="outline" class="w-100"> |
|||
<mat-label i18n>Url</mat-label> |
|||
<input matInput name="url" required [(ngModel)]="data.platform.url" /> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="justify-content-end" mat-dialog-actions> |
|||
<button i18n mat-button (click)="onCancel()">Cancel</button> |
|||
<button |
|||
color="primary" |
|||
mat-flat-button |
|||
[disabled]="!addPlatformForm.form.valid" |
|||
[mat-dialog-close]="data" |
|||
> |
|||
<ng-container i18n>Save</ng-container> |
|||
</button> |
|||
</div> |
|||
</form> |
@ -0,0 +1,22 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; |
|||
import { CreateOrUpdatePlatformDialog } from './create-or-update-account-platform.component'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { MatDialogModule } from '@angular/material/dialog'; |
|||
import { MatFormFieldModule } from '@angular/material/form-field'; |
|||
import { MatInputModule } from '@angular/material/input'; |
|||
|
|||
@NgModule({ |
|||
declarations: [CreateOrUpdatePlatformDialog], |
|||
imports: [ |
|||
CommonModule, |
|||
FormsModule, |
|||
MatButtonModule, |
|||
MatDialogModule, |
|||
MatFormFieldModule, |
|||
MatInputModule, |
|||
ReactiveFormsModule |
|||
] |
|||
}) |
|||
export class GfCreateOrUpdatePlatformDialogModule {} |
@ -0,0 +1,7 @@ |
|||
:host { |
|||
display: block; |
|||
|
|||
.mat-mdc-dialog-content { |
|||
max-height: unset; |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
import { Platform } from '@prisma/client'; |
|||
|
|||
export interface CreateOrUpdatePlatformDialogParams { |
|||
platform: Platform; |
|||
} |
@ -0,0 +1,92 @@ |
|||
<div class="container"> |
|||
<div class="row"> |
|||
<div class="col"> |
|||
<table |
|||
class="gf-table w-100" |
|||
mat-table |
|||
matSort |
|||
matSortActive="name" |
|||
matSortDirection="asc" |
|||
[dataSource]="dataSource" |
|||
> |
|||
<ng-container matColumnDef="name"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="name" |
|||
> |
|||
<ng-container i18n>Name</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
<gf-symbol-icon |
|||
*ngIf="element.url" |
|||
class="d-inline mr-1" |
|||
[tooltip]="element.name" |
|||
[url]="element.url" |
|||
></gf-symbol-icon> |
|||
<span>{{ element.name }}</span> |
|||
</td></ng-container |
|||
> |
|||
|
|||
<ng-container matColumnDef="url"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="url" |
|||
> |
|||
<ng-container i18n>Url</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
{{ element.url }} |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="actions"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1 text-center" |
|||
i18n |
|||
mat-header-cell |
|||
></th> |
|||
<td *matCellDef="let element" class="px-1 text-center" mat-cell> |
|||
<button |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
[matMenuTriggerFor]="platformMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-vertical"></ion-icon> |
|||
</button> |
|||
<mat-menu #platformMenu="matMenu" xPosition="before"> |
|||
<button mat-menu-item (click)="onUpdatePlatform(element)"> |
|||
<ion-icon class="mr-2" name="create-outline"></ion-icon> |
|||
<span i18n>Edit</span> |
|||
</button> |
|||
<button mat-menu-item (click)="onDeletePlatform(element.id)"> |
|||
<ion-icon class="mr-2" name="trash-outline"></ion-icon> |
|||
<span i18n>Delete</span> |
|||
</button> |
|||
</mat-menu> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> |
|||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
|
|||
<div *ngIf="hasPermissionToCreatePlatform" class="fab-container"> |
|||
<a |
|||
class="align-items-center d-flex justify-content-center" |
|||
color="primary" |
|||
mat-fab |
|||
[queryParams]="{ createDialog: true }" |
|||
[routerLink]="[]" |
|||
> |
|||
<ion-icon name="add-outline" size="large"></ion-icon> |
|||
</a> |
|||
</div> |
|||
</div> |
@ -0,0 +1,12 @@ |
|||
@import 'apps/client/src/styles/ghostfolio-style'; |
|||
|
|||
:host { |
|||
display: block; |
|||
|
|||
.fab-container { |
|||
position: fixed; |
|||
right: 2rem; |
|||
bottom: 4rem; |
|||
z-index: 999; |
|||
} |
|||
} |
@ -0,0 +1,222 @@ |
|||
import { |
|||
ChangeDetectorRef, |
|||
Component, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { MatSort } from '@angular/material/sort'; |
|||
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; |
|||
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; |
|||
import { get } from 'lodash'; |
|||
import { MatTableDataSource } from '@angular/material/table'; |
|||
import { ActivatedRoute, Router } from '@angular/router'; |
|||
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
|||
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; |
|||
import { Platform, Platform as PlatformModel } from '@prisma/client'; |
|||
import { Subject, takeUntil } from 'rxjs'; |
|||
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog/create-or-update-account-platform.component'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { DeviceDetectorService } from 'ngx-device-detector'; |
|||
import { AdminService } from '@ghostfolio/client/services/admin.service'; |
|||
|
|||
@Component({ |
|||
selector: 'gf-platform-overview', |
|||
styleUrls: ['./platform.component.scss'], |
|||
templateUrl: './platform.component.html' |
|||
}) |
|||
export class AdminPlatformComponent implements OnInit, OnDestroy { |
|||
@ViewChild(MatSort) sort: MatSort; |
|||
|
|||
public dataSource: MatTableDataSource<Platform> = new MatTableDataSource(); |
|||
public deviceType: string; |
|||
public displayedColumns = ['name', 'url', 'actions']; |
|||
public hasPermissionToCreatePlatform: boolean; |
|||
public hasPermissionToDeletePlatform: boolean; |
|||
public platforms: PlatformModel[]; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor( |
|||
private adminService: AdminService, |
|||
private changeDetectorRef: ChangeDetectorRef, |
|||
private deviceService: DeviceDetectorService, |
|||
private dialog: MatDialog, |
|||
private route: ActivatedRoute, |
|||
private router: Router, |
|||
private userService: UserService |
|||
) { |
|||
this.route.queryParams |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((params) => { |
|||
if (params['createDialog'] && this.hasPermissionToCreatePlatform) { |
|||
this.openCreatePlatformDialog(); |
|||
} else if (params['editDialog']) { |
|||
if (this.platforms) { |
|||
const platform = this.platforms.find(({ id }) => { |
|||
return id === params['platformId']; |
|||
}); |
|||
|
|||
this.openUpdatePlatformDialog(platform); |
|||
} else { |
|||
this.router.navigate(['.'], { relativeTo: this.route }); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.deviceType = this.deviceService.getDeviceInfo().deviceType; |
|||
|
|||
this.userService.stateChanged |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((state) => { |
|||
if (state?.user) { |
|||
const user = state.user; |
|||
|
|||
this.hasPermissionToCreatePlatform = hasPermission( |
|||
user.permissions, |
|||
permissions.createPlatform |
|||
); |
|||
this.hasPermissionToDeletePlatform = hasPermission( |
|||
user.permissions, |
|||
permissions.deletePlatform |
|||
); |
|||
|
|||
this.changeDetectorRef.markForCheck(); |
|||
} |
|||
}); |
|||
|
|||
this.fetchPlatforms(); |
|||
} |
|||
|
|||
public onDeletePlatform(aId: string) { |
|||
const confirmation = confirm( |
|||
$localize`Do you really want to delete this platform?` |
|||
); |
|||
|
|||
if (confirmation) { |
|||
this.deletePlatform(aId); |
|||
} |
|||
} |
|||
|
|||
public onUpdatePlatform(aPlatform: PlatformModel) { |
|||
this.router.navigate([], { |
|||
queryParams: { platformId: aPlatform.id, editDialog: true } |
|||
}); |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
|
|||
private deletePlatform(aId: string) { |
|||
this.adminService |
|||
.deletePlatform(aId) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe({ |
|||
next: () => { |
|||
this.userService |
|||
.get(true) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(); |
|||
|
|||
this.fetchPlatforms(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private fetchPlatforms() { |
|||
this.adminService |
|||
.fetchPlatforms() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((platforms) => { |
|||
this.platforms = platforms; |
|||
this.dataSource = new MatTableDataSource(platforms); |
|||
this.dataSource.sort = this.sort; |
|||
this.dataSource.sortingDataAccessor = get; |
|||
|
|||
this.changeDetectorRef.markForCheck(); |
|||
}); |
|||
} |
|||
|
|||
private openCreatePlatformDialog() { |
|||
const dialogRef = this.dialog.open(CreateOrUpdatePlatformDialog, { |
|||
data: { |
|||
platform: { |
|||
name: null, |
|||
url: null |
|||
} |
|||
}, |
|||
|
|||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', |
|||
width: this.deviceType === 'mobile' ? '100vw' : '50rem' |
|||
}); |
|||
|
|||
dialogRef |
|||
.afterClosed() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((data) => { |
|||
const platform: CreatePlatformDto = data?.platform; |
|||
|
|||
if (platform) { |
|||
this.adminService |
|||
.postPlatform(platform) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe({ |
|||
next: () => { |
|||
this.userService |
|||
.get(true) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(); |
|||
|
|||
this.fetchPlatforms(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
this.router.navigate(['.'], { relativeTo: this.route }); |
|||
}); |
|||
} |
|||
|
|||
private openUpdatePlatformDialog({ id, name, url }) { |
|||
const dialogRef = this.dialog.open(CreateOrUpdatePlatformDialog, { |
|||
data: { |
|||
platform: { |
|||
id, |
|||
name, |
|||
url |
|||
} |
|||
}, |
|||
|
|||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', |
|||
width: this.deviceType === 'mobile' ? '100vw' : '50rem' |
|||
}); |
|||
|
|||
dialogRef |
|||
.afterClosed() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((data) => { |
|||
const platform: UpdatePlatformDto = data?.platform; |
|||
|
|||
if (platform) { |
|||
this.adminService |
|||
.putPlatform(platform) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe({ |
|||
next: () => { |
|||
this.userService |
|||
.get(true) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(); |
|||
|
|||
this.fetchPlatforms(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
this.router.navigate(['.'], { relativeTo: this.route }); |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
|||
import { AdminPlatformComponent } from './platform.component'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { RouterModule } from '@angular/router'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { MatSortModule } from '@angular/material/sort'; |
|||
import { MatTableModule } from '@angular/material/table'; |
|||
import { GfCreateOrUpdatePlatformDialogModule } from './create-or-update-platform-dialog/create-or-update-platform-dialog.module'; |
|||
import { MatMenuModule } from '@angular/material/menu'; |
|||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; |
|||
|
|||
@NgModule({ |
|||
declarations: [AdminPlatformComponent], |
|||
imports: [ |
|||
CommonModule, |
|||
GfCreateOrUpdatePlatformDialogModule, |
|||
GfSymbolIconModule, |
|||
MatButtonModule, |
|||
MatMenuModule, |
|||
MatSortModule, |
|||
MatTableModule, |
|||
RouterModule |
|||
], |
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
|||
}) |
|||
export class GfAdminPlatformModule {} |
Loading…
Reference in new issue