Feature/add system message (#519)
* Add system message * Update changelog
This commit is contained in:
parent
aed8f5cf04
commit
4d9a223491
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Supported the management of additional currencies in the admin control panel
|
||||
- Introduced the system message
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
AdminMarketDataDetails
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Property } from '@prisma/client';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
@ -80,7 +81,13 @@ export class AdminService {
|
||||
}
|
||||
|
||||
public async putSetting(key: string, value: string) {
|
||||
const response = await this.propertyService.put({ key, value });
|
||||
let response: Property;
|
||||
|
||||
if (value === '') {
|
||||
response = await this.propertyService.delete({ key });
|
||||
} else {
|
||||
response = await this.propertyService.put({ key, value });
|
||||
}
|
||||
|
||||
if (key === PROPERTY_CURRENCIES) {
|
||||
await this.exchangeRateDataService.initialize();
|
||||
|
@ -5,6 +5,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
@ -21,6 +22,7 @@ import { InfoService } from './info.service';
|
||||
secret: process.env.JWT_SECRET_KEY,
|
||||
signOptions: { expiresIn: '30 days' }
|
||||
}),
|
||||
PropertyModule,
|
||||
RedisCacheModule,
|
||||
SymbolProfileModule
|
||||
],
|
||||
|
@ -4,7 +4,11 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { PROPERTY_STRIPE_CONFIG } from '@ghostfolio/common/config';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import {
|
||||
PROPERTY_STRIPE_CONFIG,
|
||||
PROPERTY_SYSTEM_MESSAGE
|
||||
} from '@ghostfolio/common/config';
|
||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||
@ -16,8 +20,8 @@ import { subDays } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
export class InfoService {
|
||||
private static DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
||||
private static CACHE_KEY_STATISTICS = 'STATISTICS';
|
||||
private static DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
@ -26,6 +30,7 @@ export class InfoService {
|
||||
private readonly dataGatheringService: DataGatheringService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly redisCacheService: RedisCacheService
|
||||
) {}
|
||||
|
||||
@ -35,6 +40,7 @@ export class InfoService {
|
||||
orderBy: { name: 'asc' },
|
||||
select: { id: true, name: true }
|
||||
});
|
||||
let systemMessage: string;
|
||||
|
||||
const globalPermissions: string[] = [];
|
||||
|
||||
@ -60,10 +66,19 @@ export class InfoService {
|
||||
info.stripePublicKey = this.configurationService.get('STRIPE_PUBLIC_KEY');
|
||||
}
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) {
|
||||
globalPermissions.push(permissions.enableSystemMessage);
|
||||
|
||||
systemMessage = (await this.propertyService.getByKey(
|
||||
PROPERTY_SYSTEM_MESSAGE
|
||||
)) as string;
|
||||
}
|
||||
|
||||
return {
|
||||
...info,
|
||||
globalPermissions,
|
||||
platforms,
|
||||
systemMessage,
|
||||
currencies: this.exchangeRateDataService.getCurrencies(),
|
||||
demoAuthToken: this.getDemoAuthToken(),
|
||||
lastDataGathering: await this.getLastDataGathering(),
|
||||
|
@ -21,6 +21,7 @@ export class ConfigurationService {
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
|
||||
ENABLE_FEATURE_STATISTICS: bool({ default: false }),
|
||||
ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }),
|
||||
ENABLE_FEATURE_SYSTEM_MESSAGE: bool({ default: false }),
|
||||
GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }),
|
||||
GOOGLE_SECRET: str({ default: 'dummySecret' }),
|
||||
JWT_SECRET_KEY: str({}),
|
||||
|
@ -12,6 +12,7 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
|
||||
ENABLE_FEATURE_STATISTICS: boolean;
|
||||
ENABLE_FEATURE_SUBSCRIPTION: boolean;
|
||||
ENABLE_FEATURE_SYSTEM_MESSAGE: boolean;
|
||||
GOOGLE_CLIENT_ID: string;
|
||||
GOOGLE_SECRET: string;
|
||||
JWT_SECRET_KEY: string;
|
||||
|
@ -6,6 +6,12 @@ import { Injectable } from '@nestjs/common';
|
||||
export class PropertyService {
|
||||
public constructor(private readonly prismaService: PrismaService) {}
|
||||
|
||||
public async delete({ key }: { key: string }) {
|
||||
return this.prismaService.property.delete({
|
||||
where: { key }
|
||||
});
|
||||
}
|
||||
|
||||
public async get() {
|
||||
const response: {
|
||||
[key: string]: object | string | string[];
|
||||
|
@ -9,18 +9,27 @@
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
<div *ngIf="canCreateAccount" class="container create-account-container">
|
||||
<div
|
||||
*ngIf="canCreateAccount || (info?.systemMessage && user)"
|
||||
class="container info-message-container"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2 text-center">
|
||||
<a class="text-center" [routerLink]="['/']">
|
||||
<a *ngIf="canCreateAccount" class="text-center" [routerLink]="['/']">
|
||||
<div
|
||||
class="create-account-box d-inline-block px-3 py-2"
|
||||
class="cursor-pointer d-inline-block info-message px-3 py-2"
|
||||
(click)="onCreateAccount()"
|
||||
>
|
||||
<span i18n>You are using the Live Demo.</span>
|
||||
<a class="ml-2" href="#" i18n>Create Account</a>
|
||||
</div></a
|
||||
>
|
||||
<div
|
||||
*ngIf="!canCreateAccount && info?.systemMessage && user"
|
||||
class="d-inline-block info-message px-3 py-2"
|
||||
>
|
||||
{{ info.systemMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,14 +8,13 @@
|
||||
min-height: 100vh;
|
||||
padding-top: 5rem;
|
||||
|
||||
.create-account-container {
|
||||
.info-message-container {
|
||||
height: 3.5rem;
|
||||
margin-top: -0.5rem;
|
||||
|
||||
.create-account-box {
|
||||
.info-message {
|
||||
background-color: rgba(0, 0, 0, $alpha-hover);
|
||||
border-radius: 2rem;
|
||||
cursor: pointer;
|
||||
font-size: 80%;
|
||||
|
||||
a {
|
||||
|
@ -5,9 +5,11 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import {
|
||||
DEFAULT_DATE_FORMAT,
|
||||
PROPERTY_CURRENCIES
|
||||
PROPERTY_CURRENCIES,
|
||||
PROPERTY_SYSTEM_MESSAGE
|
||||
} from '@ghostfolio/common/config';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import {
|
||||
differenceInSeconds,
|
||||
formatDistanceToNowStrict,
|
||||
@ -29,6 +31,8 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
public dataGatheringProgress: number;
|
||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||
public exchangeRates: { label1: string; label2: string; value: number }[];
|
||||
public hasPermissionForSystemMessage: boolean;
|
||||
public info: InfoItem;
|
||||
public lastDataGathering: string;
|
||||
public transactionCount: number;
|
||||
public userCount: number;
|
||||
@ -45,7 +49,14 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
) {
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForSystemMessage = hasPermission(
|
||||
this.info.globalPermissions,
|
||||
permissions.enableSystemMessage
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
@ -62,6 +73,21 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public formatDistanceToNow(aDateString: string) {
|
||||
if (aDateString) {
|
||||
const distanceString = formatDistanceToNowStrict(parseISO(aDateString), {
|
||||
addSuffix: true
|
||||
});
|
||||
|
||||
return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) <
|
||||
60
|
||||
? 'just now'
|
||||
: distanceString;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public onAddCurrency() {
|
||||
const currency = prompt('Please add a currency:');
|
||||
|
||||
@ -82,6 +108,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public onDeleteSystemMessage() {
|
||||
this.putSystemMessage('');
|
||||
}
|
||||
|
||||
public onFlushCache() {
|
||||
this.cacheService
|
||||
.flush()
|
||||
@ -117,19 +147,12 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
.subscribe(() => {});
|
||||
}
|
||||
|
||||
public formatDistanceToNow(aDateString: string) {
|
||||
if (aDateString) {
|
||||
const distanceString = formatDistanceToNowStrict(parseISO(aDateString), {
|
||||
addSuffix: true
|
||||
});
|
||||
public onSetSystemMessage() {
|
||||
const systemMessage = prompt('Please set your system message:');
|
||||
|
||||
return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) <
|
||||
60
|
||||
? 'just now'
|
||||
: distanceString;
|
||||
if (systemMessage) {
|
||||
this.putSystemMessage(systemMessage);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
@ -187,4 +210,17 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
private putSystemMessage(aSystemMessage: string) {
|
||||
this.dataService
|
||||
.putAdminSetting(PROPERTY_SYSTEM_MESSAGE, {
|
||||
value: aSystemMessage
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +118,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
||||
<div class="w-50" i18n>System Message</div>
|
||||
<div class="w-50">
|
||||
<div *ngIf="info.systemMessage">
|
||||
<span>{{ info.systemMessage }}</span>
|
||||
<button
|
||||
class="mini-icon mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[disabled]="dataGatheringInProgress"
|
||||
(click)="onDeleteSystemMessage()"
|
||||
>
|
||||
<ion-icon name="trash-outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
*ngIf="!info.systemMessage"
|
||||
color="accent"
|
||||
mat-flat-button
|
||||
(click)="onSetSystemMessage()"
|
||||
>
|
||||
<ion-icon
|
||||
class="mr-1"
|
||||
name="information-circle-outline"
|
||||
></ion-icon>
|
||||
<span i18n>Set System Message</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
@ -7,12 +8,22 @@ import { Subject } from 'rxjs';
|
||||
templateUrl: './admin-page.html'
|
||||
})
|
||||
export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
public constructor(private dataService: DataService) {
|
||||
const { systemMessage } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasMessage = !!systemMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
|
@ -5,11 +5,10 @@ import {
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -19,11 +18,11 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './home-page.html'
|
||||
})
|
||||
export class HomePageComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.with-create-account-container') get isDemo() {
|
||||
return this.canCreateAccount;
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public canCreateAccount: boolean;
|
||||
public hasMessage: boolean;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public tabs: { iconName: string; path: string }[] = [];
|
||||
public user: User;
|
||||
@ -35,10 +34,11 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private dataService: DataService,
|
||||
private userService: UserService
|
||||
) {
|
||||
const { systemMessage } = this.dataService.fetchInfo();
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
@ -50,10 +50,11 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
];
|
||||
this.user = state.user;
|
||||
|
||||
this.canCreateAccount = hasPermission(
|
||||
this.hasMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
);
|
||||
) || !!systemMessage;
|
||||
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
this.user.permissions,
|
||||
|
@ -10,10 +10,6 @@
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
&.with-create-account-container {
|
||||
height: calc(100vh - 5rem - 3.5rem);
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
gf-home-holdings,
|
||||
gf-home-market,
|
||||
|
@ -42,8 +42,6 @@ import { map } from 'rxjs/operators';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
private info: InfoItem;
|
||||
|
||||
public constructor(private http: HttpClient) {}
|
||||
|
||||
public createCheckoutSession({
|
||||
@ -241,7 +239,6 @@ export class DataService {
|
||||
}
|
||||
|
||||
public putAdminSetting(key: string, aData: PropertyDto) {
|
||||
console.log(key, aData);
|
||||
return this.http.put<void>(`/api/admin/settings/${key}`, aData);
|
||||
}
|
||||
|
||||
|
@ -175,3 +175,7 @@ ngx-skeleton-loader {
|
||||
.text-decoration-underline {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.with-info-message {
|
||||
height: calc(100vh - 5rem - 3.5rem) !important;
|
||||
}
|
||||
|
@ -34,5 +34,6 @@ export const PROPERTY_CURRENCIES = 'CURRENCIES';
|
||||
export const PROPERTY_LAST_DATA_GATHERING = 'LAST_DATA_GATHERING';
|
||||
export const PROPERTY_LOCKED_DATA_GATHERING = 'LOCKED_DATA_GATHERING';
|
||||
export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG';
|
||||
export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE';
|
||||
|
||||
export const UNKNOWN_KEY = 'UNKNOWN';
|
||||
|
@ -8,10 +8,7 @@ export interface InfoItem {
|
||||
demoAuthToken: string;
|
||||
globalPermissions: string[];
|
||||
lastDataGathering?: Date;
|
||||
message?: {
|
||||
text: string;
|
||||
type: string;
|
||||
};
|
||||
systemMessage?: string;
|
||||
platforms: { id: string; name: string }[];
|
||||
primaryDataSource: DataSource;
|
||||
statistics: Statistics;
|
||||
|
@ -17,6 +17,7 @@ export const permissions = {
|
||||
enableSocialLogin: 'enableSocialLogin',
|
||||
enableStatistics: 'enableStatistics',
|
||||
enableSubscription: 'enableSubscription',
|
||||
enableSystemMessage: 'enableSystemMessage',
|
||||
updateAccount: 'updateAccount',
|
||||
updateAuthDevice: 'updateAuthDevice',
|
||||
updateOrder: 'updateOrder',
|
||||
|
Loading…
x
Reference in New Issue
Block a user