Added technician config to frontend #127

This commit is contained in:
Sven Heidemann 2023-08-07 14:59:19 +02:00
parent 33ed4328a1
commit e0896700e7
12 changed files with 351 additions and 157 deletions

View File

@ -1,12 +1,12 @@
{
"name": "kdb-web",
"version": "1.0.dev251",
"version": "1.0.dev127_config_in_wi",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kdb-web",
"version": "1.0.dev251",
"version": "1.0.dev127_config_in_wi",
"dependencies": {
"@angular/animations": "^15.1.4",
"@angular/common": "^15.1.4",
@ -21,6 +21,7 @@
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@types/socket.io-client": "^3.0.0",
"primeflex": "^3.3.1",
"primeicons": "^6.0.1",
"primeng": "^15.2.0",
"rxjs": "~7.5.0",
@ -9302,6 +9303,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/primeflex": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/primeflex/-/primeflex-3.3.1.tgz",
"integrity": "sha512-zaOq3YvcOYytbAmKv3zYc+0VNS9Wg5d37dfxZnveKBFPr7vEIwfV5ydrpiouTft8MVW6qNjfkaQphHSnvgQbpQ=="
},
"node_modules/primeicons": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-6.0.1.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "kdb-web",
"version": "1.0.dev268_achievements",
"version": "1.0.dev127_config_in_wi",
"scripts": {
"ng": "ng",
"update-version": "ts-node-esm update-version.ts",
@ -30,6 +30,7 @@
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@types/socket.io-client": "^3.0.0",
"primeflex": "^3.3.1",
"primeicons": "^6.0.1",
"primeng": "^15.2.0",
"rxjs": "~7.5.0",
@ -51,4 +52,4 @@
"tslib": "^2.4.1",
"typescript": "~4.9.5"
}
}
}

View File

@ -0,0 +1,11 @@
import { DataWithHistory } from "../data/data.model";
export interface TechnicianConfig extends DataWithHistory {
id?: number;
helpCommandReferenceUrl?: string;
waitForRestart?: number;
waitForShutdown?: number;
cacheMaxMessages?: number;
pingURLs?: string[];
technicianIds?: string[];
}

View File

@ -165,4 +165,30 @@ export class Mutations {
}
}
`;
static updateTechnicianConfig = `
mutation updateTechnicianConfig($id: ID, $helpCommandReferenceUrl: String, $waitForRestart: Int, $waitForShutdown: Int, $cacheMaxMessages: Int, $pingURLs: [String], $technicianIds: [String]) {
technicianConfig {
updateTechnicianConfig(input: {
id: $id,
helpCommandReferenceUrl: $helpCommandReferenceUrl,
waitForRestart: $waitForRestart,
waitForShutdown: $waitForShutdown,
cacheMaxMessages: $cacheMaxMessages,
pingURLs: $pingURLs,
technicianIds: $technicianIds
}) {
id
helpCommandReferenceUrl
waitForRestart
waitForShutdown
cacheMaxMessages
pingURLs
technicianIds
}
}
}
`;
}

View File

@ -1,5 +1,22 @@
export class Queries {
static technicianConfigQuery = `
query technicianConfigQuery {
technicianConfig {
id
helpCommandReferenceUrl
waitForRestart
waitForShutdown
cacheMaxMessages
pingURLs
technicianIds
createdAt
modifiedAt
}
}
`;
static guildsQuery = `
query GuildsQuery($id: ID) {
guilds(filter: {id: $id}) {

View File

@ -4,12 +4,17 @@ import { AutoRole, AutoRoleRule } from "../data/auto_role.model";
import { Guild } from "../data/discord.model";
import { Level } from "../data/level.model";
import { Achievement, AchievementAttribute } from "../data/achievement.model";
import { TechnicianConfig } from "../config/technician-config.model";
export interface Query {
serverCount: number;
servers: Server[];
}
export interface TechnicianConfigQuery {
technicianConfig: TechnicianConfig;
}
export interface SingleDiscordQuery {
guilds: Guild[];
}

View File

@ -3,6 +3,7 @@ import { AutoRole, AutoRoleRule } from "../data/auto_role.model";
import { Level } from "../data/level.model";
import { Server } from "../data/server.model";
import { Achievement } from "../data/achievement.model";
import { TechnicianConfig } from "../config/technician-config.model";
export interface GraphQLResult {
data: {
@ -47,6 +48,12 @@ export interface LevelMutationResult {
};
}
export interface TechnicianConfigMutationResult {
technicianConfig: {
updateTechnicianConfig?: TechnicianConfig
};
}
export interface AchievementMutationResult {
achievement: {
createAchievement?: Achievement

View File

@ -1,125 +1,168 @@
<h1>
{{'admin.settings.header' | translate}}
{{'admin.settings.header' | translate}}
</h1>
<div class="content-wrapper">
<div class="content-header">
<h2>
{{'admin.settings.website.header' | translate}}
</h2>
<div class="content-header">
<h2>
{{'admin.settings.website.header' | translate}}
</h2>
</div>
<div class="content">
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.frontend_version' | translate}}:</div>
<div class="content-data-value">{{data.webVersion}}</div>
</div>
</div>
<div class="content">
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.frontend_version' | translate}}:</div>
<div class="content-data-value">{{data.webVersion}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.backend_version' | translate}}:</div>
<div class="content-data-value">{{data.apiVersion}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.config_path' | translate}}:</div>
<div class="content-data-value">{{data.configPath}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.frontend_base_url' | translate}}:</div>
<div class="content-data-value">{{data.webBaseURL}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.backend_base_url' | translate}}:</div>
<div class="content-data-value">{{data.apiBaseURL}}</div>
</div>
</div>
<div class="content-divider"></div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.token_expire_time' | translate}}:</div>
<div class="content-data-value">{{data.tokenExpireTime}} {{'general.minutes' | translate}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.refresh_token_expire_time' | translate}}:</div>
<div class="content-data-value">{{data.refreshTokenExpireTime}} {{'general.days' | translate}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.backend_version' | translate}}:</div>
<div class="content-data-value">{{data.apiVersion}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.config_path' | translate}}:</div>
<div class="content-data-value">{{data.configPath}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.frontend_base_url' | translate}}:</div>
<div class="content-data-value">{{data.webBaseURL}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.backend_base_url' | translate}}:</div>
<div class="content-data-value">{{data.apiBaseURL}}</div>
</div>
</div>
<div class="content-divider"></div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.token_expire_time' | translate}}:</div>
<div class="content-data-value">{{data.tokenExpireTime}} {{'general.minutes' | translate}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.refresh_token_expire_time' | translate}}:</div>
<div class="content-data-value">{{data.refreshTokenExpireTime}} {{'general.days' | translate}}</div>
</div>
</div>
</div>
</div>
<div class="content-wrapper">
<div class="content-header">
<h2>
{{'admin.settings.email.header' | translate}}
</h2>
<div class="content-header">
<h2>
{{'admin.settings.email.header' | translate}}
</h2>
</div>
<div class="content">
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.user' | translate}}:</div>
<div class="content-data-value">{{data.mailUser}}</div>
</div>
</div>
<div class="content">
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.user' | translate}}:</div>
<div class="content-data-value">{{data.mailUser}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.host' | translate}}:</div>
<div class="content-data-value">{{data.mailHost}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.port' | translate}}:</div>
<div class="content-data-value">{{data.mailPort}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.transceiver' | translate}}:</div>
<div class="content-data-value">{{data.mailTransceiver}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.email_address' | translate}}:</div>
<div class="content-data-value">{{data.mailTransceiverAddress}}</div>
</div>
</div>
<div class="content-row">
<form [formGroup]="testMailForm" class="content-column">
<div class="content-data-name">
<div class="input-field content-input-field">
<input type="email" pInputText formControlName="mail" placeholder="{{'common.email' | translate}}" autocomplete="email">
</div>
</div>
<div class="content-data-value">
<div class="login-form-submit">
<button pButton icon="pi pi-save" label="{{'common.email' | translate}}" class="btn login-form-submit-btn"
(click)="testMail()" [disabled]="testMailForm.invalid"></button>
</div>
</div>
</form>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.host' | translate}}:</div>
<div class="content-data-value">{{data.mailHost}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.port' | translate}}:</div>
<div class="content-data-value">{{data.mailPort}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.transceiver' | translate}}:</div>
<div class="content-data-value">{{data.mailTransceiver}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.email.email_address' | translate}}:</div>
<div class="content-data-value">{{data.mailTransceiverAddress}}</div>
</div>
</div>
<div class="content-row">
<form [formGroup]="testMailForm" class="content-column">
<div class="content-data-name">
<div class="input-field content-input-field">
<input type="email" pInputText formControlName="mail" placeholder="{{'common.email' | translate}}" autocomplete="email">
</div>
</div>
<div class="content-data-value">
<div class="login-form-submit">
<button pButton icon="pi pi-send" label="{{'common.email' | translate}}" class="btn login-form-submit-btn"
(click)="testMail()" [disabled]="testMailForm.invalid"></button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="content-wrapper">
<div class="content-header">
<h2>
{{'admin.settings.bot.header' | translate}}
</h2>
</div>
<div class="content">
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.bot.help_url' | translate}}:</div>
<input class="content-data-value" type="text" pInputText [(ngModel)]="config.helpCommandReferenceUrl" placeholder="{{'admin.settings.bot.help_url' | translate}}">
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.bot.wait_for_restart' | translate}}:</div>
<input class="content-data-value" type="number" pInputText [(ngModel)]="config.waitForRestart" placeholder="{{'admin.settings.bot.wait_for_restart' | translate}}">
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.bot.wait_for_shutdown' | translate}}:</div>
<input class="content-data-value" type="number" pInputText [(ngModel)]="config.waitForShutdown" placeholder="{{'admin.settings.bot.wait_for_shutdown' | translate}}">
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.bot.cache_max_messages' | translate}}:</div>
<input class="content-data-value" type="number" pInputText [(ngModel)]="config.cacheMaxMessages" placeholder="{{'admin.settings.bot.cache_max_messages' | translate}}">
</div>
</div>
<div class="content-row">
<button pButton icon="pi pi-save" label="{{'common.save' | translate}}" class="btn login-form-submit-btn"
(click)="saveTechnicianConfig()" [disabled]="technicianConfigForm.invalid"></button>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
import { TranslateService } from "@ngx-translate/core";
import { catchError } from "rxjs/operators";
import { SettingsDTO } from "src/app/models/config/settings.dto";
@ -9,66 +9,111 @@ import { GuiService } from "src/app/services/gui/gui.service";
import { SettingsService } from "src/app/services/settings/settings.service";
import { SpinnerService } from "src/app/services/spinner/spinner.service";
import { ToastService } from "src/app/services/toast/toast.service";
import { throwError } from "rxjs";
import { forkJoin, throwError } from "rxjs";
import { TechnicianConfig } from "../../../../../models/config/technician-config.model";
import { TechnicianConfigQuery } from "../../../../../models/graphql/query.model";
import { Queries } from "../../../../../models/graphql/queries.model";
import { DataService } from "../../../../../services/data/data.service";
import { LevelMutationResult, TechnicianConfigMutationResult } from "../../../../../models/graphql/result.model";
import { Mutations } from "../../../../../models/graphql/mutations.model";
import { AuthService } from "../../../../../services/auth/auth.service";
import { ConfirmationDialogService } from "../../../../../services/confirmation-dialog/confirmation-dialog.service";
import { SidebarService } from "../../../../../services/sidebar/sidebar.service";
import { ActivatedRoute } from "@angular/router";
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
selector: "app-settings",
templateUrl: "./settings.component.html",
styleUrls: ["./settings.component.scss"]
})
export class SettingsComponent implements OnInit {
testMailForm!: FormGroup;
technicianConfigForm: FormGroup = this.formBuilder.group({
helpCommandReferenceUrl: [null, [Validators.required]],
waitForRestart: [null, [Validators.required]],
waitForShutdown: [null, [Validators.required]],
cacheMaxMessages: [null, [Validators.required]],
pingUrls: this.formBuilder.array([])
});
data: SettingsDTO = {
webVersion: '',
apiVersion: '',
configPath: '',
webBaseURL: '',
apiBaseURL: '',
webVersion: "",
apiVersion: "",
configPath: "",
webBaseURL: "",
apiBaseURL: "",
tokenExpireTime: 0,
refreshTokenExpireTime: 0,
mailUser: '',
mailUser: "",
mailPort: 0,
mailHost: '',
mailTransceiver: '',
mailTransceiverAddress: '',
mailHost: "",
mailTransceiver: "",
mailTransceiverAddress: ""
};
config: TechnicianConfig = {
helpCommandReferenceUrl: "",
waitForRestart: 0,
waitForShutdown: 0,
cacheMaxMessages: 0,
pingURLs: [],
technicianIds: []
};
constructor(
private dataService: DataService,
private settingsService: SettingsService,
private spinnerService: SpinnerService,
private guiService: GuiService,
private formBuilder: FormBuilder,
private toastService: ToastService,
private translate: TranslateService
) { }
private translate: TranslateService,
private authService: AuthService,
private spinner: SpinnerService,
) {
}
ngOnInit(): void {
this.spinnerService.showSpinner();
this.initForms();
this.guiService.getSettings()
.pipe(catchError(error => {
forkJoin([
this.guiService.getSettings().pipe(catchError(error => {
this.spinnerService.hideSpinner();
return throwError(() => error);
}))
.subscribe(settings => {
this.spinnerService.hideSpinner();
this.data = settings;
this.data.webVersion = this.settingsService.getWebVersion()?.getVersionString() ?? '0.0.0';
this.data.apiBaseURL = this.settingsService.getApiURL();
if (!this.data.apiBaseURL.endsWith('/')) {
this.data.apiBaseURL += '/';
}
});
})),
this.dataService.query<TechnicianConfigQuery>(Queries.technicianConfigQuery)
]).subscribe(data => {
this.data = data[0];
this.data.webVersion = this.settingsService.getWebVersion()?.getVersionString() ?? "0.0.0";
this.data.apiBaseURL = this.settingsService.getApiURL();
if (!this.data.apiBaseURL.endsWith("/")) {
this.data.apiBaseURL += "/";
}
this.config = data[1].technicianConfig;
this.initForms();
this.spinnerService.hideSpinner();
});
}
initForms(): void {
this.testMailForm = this.formBuilder.group({
mail: [null, [Validators.required, Validators.email]],
mail: [null, [Validators.required, Validators.email]]
});
this.technicianConfigForm = this.formBuilder.group({
helpCommandReferenceUrl: [this.config.helpCommandReferenceUrl, [Validators.required]],
waitForRestart: [this.config.waitForRestart, [Validators.required]],
waitForShutdown: [this.config.waitForShutdown, [Validators.required]],
cacheMaxMessages: [this.config.cacheMaxMessages, [Validators.required]],
pingUrls: this.formBuilder.array([])
});
const pingUrls = <FormArray>this.technicianConfigForm.controls["pingUrls"];
for (const url of this.config.pingURLs ?? []) {
pingUrls.push(new FormControl(url, [Validators.required]));
}
}
testMail(): void {
@ -82,27 +127,27 @@ export class SettingsComponent implements OnInit {
this.guiService.sendTestMail(mail)
.pipe(catchError(error => {
let header = this.translate.instant('admin.settings.message.error');
let message = this.translate.instant('admin.settings.message.could_not_send_mail');
let header = this.translate.instant("admin.settings.message.error");
let message = this.translate.instant("admin.settings.message.could_not_send_mail");
if (error.error !== null) {
const err: ErrorDTO = error.error;
if (err.errorCode === ServiceErrorCode.ConnectionFailed) {
header = this.translate.instant('admin.settings.message.connection_failed');
message = this.translate.instant('admin.settings.message.connection_to_mail_failed');
header = this.translate.instant("admin.settings.message.connection_failed");
message = this.translate.instant("admin.settings.message.connection_to_mail_failed");
error.error = null;
}
if (err.errorCode === ServiceErrorCode.InvalidUser) {
header = this.translate.instant('admin.settings.message.connection_failed');
message = this.translate.instant('admin.settings.message.mail_login_failed');
header = this.translate.instant("admin.settings.message.connection_failed");
message = this.translate.instant("admin.settings.message.mail_login_failed");
error.error = null;
}
if (err.errorCode === ServiceErrorCode.MailError) {
header = this.translate.instant('admin.settings.message.send_failed');
message = this.translate.instant('admin.settings.message.test_mail_not_send');
header = this.translate.instant("admin.settings.message.send_failed");
message = this.translate.instant("admin.settings.message.test_mail_not_send");
error.error = null;
}
}
@ -113,9 +158,29 @@ export class SettingsComponent implements OnInit {
}))
.subscribe(res => {
this.spinnerService.hideSpinner();
this.toastService.success(this.translate.instant('admin.settings.message.success'), this.translate.instant('admin.settings.message.send_mail'));
this.toastService.success(this.translate.instant("admin.settings.message.success"), this.translate.instant("admin.settings.message.send_mail"));
this.testMailForm.reset();
});
}
saveTechnicianConfig() {
this.spinner.showSpinner();
this.dataService.mutation<TechnicianConfigMutationResult>(Mutations.updateTechnicianConfig, {
helpCommandReferenceUrl: this.config.helpCommandReferenceUrl,
waitForRestart: this.config.waitForRestart,
waitForShutdown: this.config.waitForShutdown,
cacheMaxMessages: this.config.cacheMaxMessages,
pingURLs: this.config.pingURLs,
technicianIds: this.config.technicianIds,
}
).pipe(catchError(err => {
this.spinner.hideSpinner();
this.toastService.error(this.translate.instant("admin.settings.message.technician_config_create_failed"), this.translate.instant("admin.settings.message.technician_config_create_failed_d"));
return throwError(err);
})).subscribe(result => {
this.spinner.hideSpinner();
this.toastService.success(this.translate.instant("admin.settings.message.technician_config_create"), this.translate.instant("admin.settings.message.technician_config_create_d"));
});
}
}

View File

@ -24,6 +24,7 @@ import { InputNumberModule } from "primeng/inputnumber";
import { ImageModule } from "primeng/image";
import { SidebarModule } from "primeng/sidebar";
import { HistoryBtnComponent } from './components/history-btn/history-btn.component';
import { DataViewModule, DataViewLayoutOptions } from 'primeng/dataview';
@NgModule({
@ -56,6 +57,7 @@ import { HistoryBtnComponent } from './components/history-btn/history-btn.compon
InputNumberModule,
ImageModule,
SidebarModule,
DataViewModule,
],
exports: [
ButtonModule,
@ -82,7 +84,9 @@ import { HistoryBtnComponent } from './components/history-btn/history-btn.compon
InputNumberModule,
ImageModule,
SidebarModule,
HistoryBtnComponent
HistoryBtnComponent,
DataViewModule,
DataViewLayoutOptions
]
})
export class SharedModule { }

View File

@ -5,7 +5,7 @@
"WebVersion": {
"Major": "1",
"Minor": "0",
"Micro": "dev268_achievements"
"Micro": "dev127_config_in_wi"
},
"Themes": [
{
@ -25,4 +25,4 @@
"Name": "sh-edraft-dark-theme"
}
]
}
}

View File

@ -177,6 +177,14 @@ header {
font-size: 18px;
}
.content-data-value-row {
display: flex;
flex: 1;
flex-direction: column;
font-size: 18px;
}
.content-divider {
margin: 5px 0;
}
@ -477,6 +485,7 @@ footer {
.right {
width: 50%;
text-align: right;
.p-button-label {
font-weight: unset !important;
}