Feature/platform management (#1922)
* Added platform management to admin control panel * Update changelog
This commit is contained in:
parent
8ba15f8f72
commit
e1892d2870
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Introduced the option to update the cash balance of an account when adding an activity
|
- Introduced the option to update the cash balance of an account when adding an activity
|
||||||
|
- Added support for the management of platforms in the admin control panel
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ import { RedisCacheModule } from './redis-cache/redis-cache.module';
|
|||||||
import { SubscriptionModule } from './subscription/subscription.module';
|
import { SubscriptionModule } from './subscription/subscription.module';
|
||||||
import { SymbolModule } from './symbol/symbol.module';
|
import { SymbolModule } from './symbol/symbol.module';
|
||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
|
import { PlatformModule } from './platform/platform.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -63,6 +64,7 @@ import { UserModule } from './user/user.module';
|
|||||||
InfoModule,
|
InfoModule,
|
||||||
LogoModule,
|
LogoModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
|
PlatformModule,
|
||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
|
@ -7,7 +7,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf
|
|||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { PlatformModule } from '@ghostfolio/api/services/platform/platform.module';
|
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
@ -6,7 +6,7 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
|||||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { PlatformService } from '@ghostfolio/api/services/platform/platform.service';
|
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
@ -130,7 +130,7 @@ export class ImportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
this.platformService.get()
|
this.platformService.getPlatforms()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (const account of accountsDto) {
|
for (const account of accountsDto) {
|
||||||
|
9
apps/api/src/app/platform/create-platform.dto.ts
Normal file
9
apps/api/src/app/platform/create-platform.dto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreatePlatformDto {
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
url: string;
|
||||||
|
}
|
113
apps/api/src/app/platform/platform.controller.ts
Normal file
113
apps/api/src/app/platform/platform.controller.ts
Normal file
@ -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 { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PlatformController } from './platform.controller';
|
||||||
import { PlatformService } from './platform.service';
|
import { PlatformService } from './platform.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
controllers: [PlatformController],
|
||||||
exports: [PlatformService],
|
exports: [PlatformService],
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
providers: [PlatformService]
|
providers: [PlatformService]
|
45
apps/api/src/app/platform/platform.service.ts
Normal file
45
apps/api/src/app/platform/platform.service.ts
Normal file
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
12
apps/api/src/app/platform/update-platform.dto.ts
Normal file
12
apps/api/src/app/platform/update-platform.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
222
apps/client/src/app/components/platform/platform.component.ts
Normal file
222
apps/client/src/app/components/platform/platform.component.ts
Normal file
@ -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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
26
apps/client/src/app/components/platform/platform.module.ts
Normal file
26
apps/client/src/app/components/platform/platform.module.ts
Normal file
@ -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 {}
|
@ -4,6 +4,7 @@ import { AdminJobsComponent } from '@ghostfolio/client/components/admin-jobs/adm
|
|||||||
import { AdminMarketDataComponent } from '@ghostfolio/client/components/admin-market-data/admin-market-data.component';
|
import { AdminMarketDataComponent } from '@ghostfolio/client/components/admin-market-data/admin-market-data.component';
|
||||||
import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-overview/admin-overview.component';
|
import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-overview/admin-overview.component';
|
||||||
import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component';
|
import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component';
|
||||||
|
import { AdminPlatformComponent } from '@ghostfolio/client/components/platform/platform.component';
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
import { AdminPageComponent } from './admin-page.component';
|
import { AdminPageComponent } from './admin-page.component';
|
||||||
@ -24,7 +25,16 @@ const routes: Routes = [
|
|||||||
component: AdminOverviewComponent,
|
component: AdminOverviewComponent,
|
||||||
title: $localize`Admin Control`
|
title: $localize`Admin Control`
|
||||||
},
|
},
|
||||||
{ path: 'users', component: AdminUsersComponent, title: $localize`Users` }
|
{
|
||||||
|
path: 'users',
|
||||||
|
component: AdminUsersComponent,
|
||||||
|
title: $localize`Users`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'platforms',
|
||||||
|
component: AdminPlatformComponent,
|
||||||
|
title: $localize`Platforms`
|
||||||
|
}
|
||||||
],
|
],
|
||||||
component: AdminPageComponent,
|
component: AdminPageComponent,
|
||||||
path: ''
|
path: ''
|
||||||
|
@ -31,6 +31,11 @@ export class AdminPageComponent implements OnDestroy, OnInit {
|
|||||||
path: 'overview'
|
path: 'overview'
|
||||||
},
|
},
|
||||||
{ iconName: 'people-outline', label: $localize`Users`, path: 'users' },
|
{ iconName: 'people-outline', label: $localize`Users`, path: 'users' },
|
||||||
|
{
|
||||||
|
iconName: 'briefcase-outline',
|
||||||
|
label: $localize`Platforms`,
|
||||||
|
path: 'platforms'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
iconName: 'server-outline',
|
iconName: 'server-outline',
|
||||||
label: $localize`Market Data`,
|
label: $localize`Market Data`,
|
||||||
|
@ -5,6 +5,7 @@ import { GfAdminJobsModule } from '@ghostfolio/client/components/admin-jobs/admi
|
|||||||
import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module';
|
import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module';
|
||||||
import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module';
|
import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module';
|
||||||
import { GfAdminUsersModule } from '@ghostfolio/client/components/admin-users/admin-users.module';
|
import { GfAdminUsersModule } from '@ghostfolio/client/components/admin-users/admin-users.module';
|
||||||
|
import { GfAdminPlatformModule } from '@ghostfolio/client/components/platform/platform.module';
|
||||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||||
|
|
||||||
import { AdminPageRoutingModule } from './admin-page-routing.module';
|
import { AdminPageRoutingModule } from './admin-page-routing.module';
|
||||||
@ -19,6 +20,7 @@ import { AdminPageComponent } from './admin-page.component';
|
|||||||
GfAdminJobsModule,
|
GfAdminJobsModule,
|
||||||
GfAdminMarketDataModule,
|
GfAdminMarketDataModule,
|
||||||
GfAdminOverviewModule,
|
GfAdminOverviewModule,
|
||||||
|
GfAdminPlatformModule,
|
||||||
GfAdminUsersModule,
|
GfAdminUsersModule,
|
||||||
MatTabsModule
|
MatTabsModule
|
||||||
],
|
],
|
||||||
|
@ -2,6 +2,8 @@ import { HttpClient, HttpParams } from '@angular/common/http';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||||
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
||||||
|
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
|
||||||
|
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
|
||||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
@ -10,7 +12,7 @@ import {
|
|||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData, Platform } from '@prisma/client';
|
||||||
import { JobStatus } from 'bull';
|
import { JobStatus } from 'bull';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { Observable, map } from 'rxjs';
|
import { Observable, map } from 'rxjs';
|
||||||
@ -37,6 +39,10 @@ export class AdminService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public deletePlatform(aId: string) {
|
||||||
|
return this.http.delete<void>(`/api/v1/platform/${aId}`);
|
||||||
|
}
|
||||||
|
|
||||||
public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||||
return this.http.delete<void>(
|
return this.http.delete<void>(
|
||||||
`/api/v1/admin/profile-data/${dataSource}/${symbol}`
|
`/api/v1/admin/profile-data/${dataSource}/${symbol}`
|
||||||
@ -74,6 +80,10 @@ export class AdminService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchPlatforms() {
|
||||||
|
return this.http.get<Platform[]>('/api/v1/platform');
|
||||||
|
}
|
||||||
|
|
||||||
public gather7Days() {
|
public gather7Days() {
|
||||||
return this.http.post<void>('/api/v1/admin/gather', {});
|
return this.http.post<void>('/api/v1/admin/gather', {});
|
||||||
}
|
}
|
||||||
@ -138,6 +148,10 @@ export class AdminService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public postPlatform(aPlatform: CreatePlatformDto) {
|
||||||
|
return this.http.post<Platform>(`/api/v1/platform`, aPlatform);
|
||||||
|
}
|
||||||
|
|
||||||
public putMarketData({
|
public putMarketData({
|
||||||
dataSource,
|
dataSource,
|
||||||
date,
|
date,
|
||||||
@ -156,4 +170,11 @@ export class AdminService {
|
|||||||
|
|
||||||
return this.http.put<MarketData>(url, marketData);
|
return this.http.put<MarketData>(url, marketData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public putPlatform(aPlatform: UpdatePlatformDto) {
|
||||||
|
return this.http.put<Platform>(
|
||||||
|
`/api/v1/platform/${aPlatform.id}`,
|
||||||
|
aPlatform
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,13 @@ export const permissions = {
|
|||||||
createAccess: 'createAccess',
|
createAccess: 'createAccess',
|
||||||
createAccount: 'createAccount',
|
createAccount: 'createAccount',
|
||||||
createOrder: 'createOrder',
|
createOrder: 'createOrder',
|
||||||
|
createPlatform: 'createPlatform',
|
||||||
createUserAccount: 'createUserAccount',
|
createUserAccount: 'createUserAccount',
|
||||||
deleteAccess: 'deleteAccess',
|
deleteAccess: 'deleteAccess',
|
||||||
deleteAccount: 'deleteAcccount',
|
deleteAccount: 'deleteAcccount',
|
||||||
deleteAuthDevice: 'deleteAuthDevice',
|
deleteAuthDevice: 'deleteAuthDevice',
|
||||||
deleteOrder: 'deleteOrder',
|
deleteOrder: 'deleteOrder',
|
||||||
|
deletePlatform: 'deletePlatform',
|
||||||
deleteUser: 'deleteUser',
|
deleteUser: 'deleteUser',
|
||||||
enableFearAndGreedIndex: 'enableFearAndGreedIndex',
|
enableFearAndGreedIndex: 'enableFearAndGreedIndex',
|
||||||
enableImport: 'enableImport',
|
enableImport: 'enableImport',
|
||||||
@ -26,6 +28,7 @@ export const permissions = {
|
|||||||
updateAccount: 'updateAccount',
|
updateAccount: 'updateAccount',
|
||||||
updateAuthDevice: 'updateAuthDevice',
|
updateAuthDevice: 'updateAuthDevice',
|
||||||
updateOrder: 'updateOrder',
|
updateOrder: 'updateOrder',
|
||||||
|
updatePlatform: 'updatePlatform',
|
||||||
updateUserSettings: 'updateUserSettings',
|
updateUserSettings: 'updateUserSettings',
|
||||||
updateViewMode: 'updateViewMode'
|
updateViewMode: 'updateViewMode'
|
||||||
};
|
};
|
||||||
@ -38,14 +41,17 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
permissions.createAccess,
|
permissions.createAccess,
|
||||||
permissions.createAccount,
|
permissions.createAccount,
|
||||||
permissions.createOrder,
|
permissions.createOrder,
|
||||||
|
permissions.createPlatform,
|
||||||
permissions.deleteAccess,
|
permissions.deleteAccess,
|
||||||
permissions.deleteAccount,
|
permissions.deleteAccount,
|
||||||
permissions.deleteAuthDevice,
|
permissions.deleteAuthDevice,
|
||||||
permissions.deleteOrder,
|
permissions.deleteOrder,
|
||||||
|
permissions.deletePlatform,
|
||||||
permissions.deleteUser,
|
permissions.deleteUser,
|
||||||
permissions.updateAccount,
|
permissions.updateAccount,
|
||||||
permissions.updateAuthDevice,
|
permissions.updateAuthDevice,
|
||||||
permissions.updateOrder,
|
permissions.updateOrder,
|
||||||
|
permissions.updatePlatform,
|
||||||
permissions.updateUserSettings,
|
permissions.updateUserSettings,
|
||||||
permissions.updateViewMode
|
permissions.updateViewMode
|
||||||
];
|
];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user