Feature/add system message (#519)

* Add system message

* Update changelog
pull/525/head
Thomas Kaul 3 years ago committed by GitHub
parent aed8f5cf04
commit 4d9a223491
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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.user?.permissions,
permissions.createUserAccount
);
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…
Cancel
Save