Brand new Angular client

This commit is contained in:
Carlos
2022-06-15 20:24:56 +02:00
parent 544720331b
commit 7d13d219d1
109 changed files with 23573 additions and 1892 deletions

View File

@ -0,0 +1,14 @@
import { Subject } from "rxjs/internal/Subject";
import { WebSocketCommand } from "./command/WebSocketCommand";
import { ConnectionStatusEnum } from "./ConnectionStatusEnum";
import { WebSocketResponse } from "./response/WebSocketResponse";
export interface ActiveConnectionDto {
serverName: string;
subject$: Subject<WebSocketResponse>;
connectionStatus: ConnectionStatusEnum;
receivedMessages: WebSocketResponse[];
sentCommands: WebSocketCommand[];
isLoggedIn: boolean;
token?: string;
}

View File

@ -0,0 +1,5 @@
export enum ConnectionStatusEnum {
Connecting = 1,
Connected = 2,
Disconnected = 3
}

View File

@ -0,0 +1,5 @@
export interface ServerDto {
serverName: string;
serverURI: string;
serverPassword?: string;
}

View File

@ -0,0 +1,6 @@
export interface SettingsDto {
dateTimePrefix: boolean;
retrieveLogFile: boolean;
blurryUri: boolean;
widerViewport: boolean;
}

View File

@ -0,0 +1,8 @@
import { ServerDto } from "./ServerDto";
import { SettingsDto } from "./SettingsDto";
export interface StoredDataDto {
servers: ServerDto[];
language: string;
settings: SettingsDto;
}

View File

@ -0,0 +1,5 @@
export interface WebSocketCommand {
command: string;
params?: string;
token?: string;
}

View File

@ -0,0 +1,9 @@
export enum WebSocketCommandEnum {
Login = "LOGIN",
Exec = "EXEC",
Players = "PLAYERS",
CpuUsage = "CPUUSAGE",
RamUsage = "RAMUSAGE",
Tps = "TPS",
ReadLogFile = "READLOGFILE"
}

View File

@ -0,0 +1,6 @@
import { WebSocketResponse } from "./WebSocketResponse";
export interface ConsoleOutputResponse extends WebSocketResponse{
time: string;
message: string;
}

View File

@ -0,0 +1,5 @@
import { WebSocketResponse } from "./WebSocketResponse";
export interface CpuResponse extends WebSocketResponse{
usage: number;
}

View File

@ -0,0 +1,8 @@
import { WebSocketResponse } from "./WebSocketResponse";
export interface LoggedInResponse extends WebSocketResponse{
respondsTo: string;
username: string;
as: string;
token: string;
}

View File

@ -0,0 +1,5 @@
import { WebSocketResponse } from "./WebSocketResponse";
export interface LoginRequiredResponse extends WebSocketResponse{
}

View File

@ -0,0 +1,7 @@
import { WebSocketResponse } from "./WebSocketResponse";
export interface PlayersResponse extends WebSocketResponse {
connectedPlayers: number;
maxPlayers: number;
players: string[];
}

View File

@ -0,0 +1,7 @@
import { WebSocketResponse } from "./WebSocketResponse";
export interface RamResponse extends WebSocketResponse{
free: number;
used: number;
max: number;
}

View File

@ -0,0 +1,5 @@
import { WebSocketResponse } from "./WebSocketResponse";
export interface TpsResponse extends WebSocketResponse{
tps: number;
}

View File

@ -0,0 +1,5 @@
import { WebSocketResponse } from "./WebSocketResponse";
export interface UnknownCommandResponse extends WebSocketResponse{
respondsTo: string;
}

View File

@ -0,0 +1,5 @@
export interface WebSocketResponse {
status: number;
statusDescription: string;
message: string;
}

View File

@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { StorageService } from './storage.service';
@Injectable({
providedIn: 'root'
})
export class LanguageService {
constructor(
private storageService: StorageService,
private translateService: TranslateService
) { }
public setLanguage(language: string): void {
this.translateService.use(language);
this.storageService.setLanguage(language);
}
public getLanguage(): string {
return this.storageService.getLanguage();
}
}

View File

@ -0,0 +1,245 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { ServerDto } from '../_dto/ServerDto';
import { StoredDataDto } from '../_dto/StoredDataDto';
@Injectable({
providedIn: 'root'
})
export class StorageService {
//BehaviorSubjects for latest settings values
public widerViewportSubject = new BehaviorSubject<boolean>(false);
constructor() {
this.initializeLocalStorage();
}
/**
* Initialize settings
*/
private initializeLocalStorage(): void {
//If undefined, initialize localStorage
if (typeof window.localStorage["WebConsole"] === 'undefined') {
//Create empty object
var storageObj: StoredDataDto = {
servers: [],
language: "",
settings: {
dateTimePrefix: true,
retrieveLogFile: true,
blurryUri: false,
widerViewport: false
}
};
//Save to WebStorage
window.localStorage["WebConsole"] = JSON.stringify(storageObj);
}
//Initialize BehaviorSubjects
this.widerViewportSubject.next(this.getSetting(SettingsEnum.WiderViewport));
}
/**
* Get a list of all saved servers
* @returns List of all servers saved
*/
public getAllServers(): ServerDto[] {
var storageObj = JSON.parse(window.localStorage["WebConsole"]) as StoredDataDto;
return storageObj.servers;
}
/**
* Get server info given its name
* @param serverName Server name
* @returns Server details, null if not found
*/
public getServer(serverName: string): ServerDto | undefined {
return this.getAllServers().find(e => e.serverName == serverName);
}
/**
* Save a new server or, in case a server with same name already exists, update it.
* @param serverName Name of the server to add or update
* @param serverUri URI of the server
*/
public saveServer(serverName: string, serverUri: string, serverPassword?: string): void {
//Get all saved servers
let servers = this.getAllServers();
let server = servers.find(e => e.serverName == serverName);
if (server) {
//If server exists, update it
(server as ServerDto).serverURI = serverUri;
if (serverPassword)
(server as ServerDto).serverPassword = serverPassword;
else if (serverPassword == null)
(server as ServerDto).serverPassword = undefined;
} else {
//If it does not exist, add to the array
const serverToSave: ServerDto = {
serverName: serverName,
serverURI: serverUri,
serverPassword: serverPassword
}
servers.push(serverToSave);
}
//Overwrite array
this.replaceAllServers(servers);
}
moveServerToIndex(serverName: string, newIndex: number): void {
//Prevent moving if index is not valid
const listOfServers: ServerDto[] = this.getAllServers();
if (newIndex < 0 || newIndex >= listOfServers.length) {
return;
}
//Prevent moving if server does not exist
const serverIndex = listOfServers.findIndex(e => e.serverName == serverName);
if (serverIndex === -1)
return;
//Move server
const serverToMove: ServerDto = listOfServers.find(e => e.serverName == serverName) as ServerDto;
listOfServers.splice(serverIndex, 1); //Remove element from its current position
listOfServers.splice(newIndex, 0, serverToMove); //Inject element in new position
this.replaceAllServers(listOfServers);
}
/**
* Delete a server given its name
* @param serverName Name of the server
*/
public deleteServer(serverName: string): void {
//Get all servers
var servers = this.getAllServers();
//Delete it
servers = servers.filter(e => e.serverName != serverName)
//Save to LocalStorage
this.replaceAllServers(servers);
}
/**
* Save to persistence a new default language
* @param lang Language to set
*/
public setLanguage(lang: string): void {
//Retrieve saved data
var storageObj = JSON.parse(window.localStorage["WebConsole"]) as StoredDataDto;
storageObj.language = lang;
//Save to WebStorage
window.localStorage["WebConsole"] = JSON.stringify(storageObj);
}
/**
* Get saved language
* @returns Language saved
*/
public getLanguage(): string {
var storageObj = JSON.parse(window.localStorage["WebConsole"]) as StoredDataDto;
if (!storageObj.language)
return "en_US";
return storageObj.language;
}
/**
* Replace ALL servers with the provided server list
* @param newServerList New server list
*/
private replaceAllServers(newServerList: ServerDto[]) {
//Retrieve saved data
let storageObj = JSON.parse(window.localStorage["WebConsole"]) as StoredDataDto;
storageObj.servers = newServerList;
//Save to WebStorage
window.localStorage["WebConsole"] = JSON.stringify(storageObj);
}
/**
* Update setting value
* @param setting Setting to set
* @param value Value to set
*/
public setSetting(setting: SettingsEnum, value: any) {
let currentSettings = JSON.parse(window.localStorage["WebConsole"]) as StoredDataDto;
switch (setting) {
case SettingsEnum.DateTimePrefix:
currentSettings.settings.dateTimePrefix = value;
break;
case SettingsEnum.RetrieveLogFile:
currentSettings.settings.retrieveLogFile = value;
break;
case SettingsEnum.BlurryUri:
currentSettings.settings.blurryUri = value;
break;
case SettingsEnum.WiderViewport:
currentSettings.settings.widerViewport = value;
this.widerViewportSubject.next(value);
break;
}
//Save all
let storageObj = JSON.parse(window.localStorage["WebConsole"]);
storageObj.settings = currentSettings.settings;
window.localStorage["WebConsole"] = JSON.stringify(storageObj);
}
/**
* Get a setting
* @param setting Setting to get
* @returns Settings value
*/
public getSetting(setting: SettingsEnum) {
let currentSettings = JSON.parse(window.localStorage["WebConsole"]) as StoredDataDto;
switch (setting) {
case SettingsEnum.DateTimePrefix:
return currentSettings.settings.dateTimePrefix;
case SettingsEnum.RetrieveLogFile:
return currentSettings.settings.retrieveLogFile;
case SettingsEnum.BlurryUri:
return currentSettings.settings.blurryUri;
case SettingsEnum.WiderViewport:
return currentSettings.settings.widerViewport;
}
}
/**
* Import settings from a Base64-encoded JSON
* @param base64settings Encoded settings
* @returns True if imported successfully, false otherwise
*/
public importSettings(base64settings: string): boolean {
try {
const decodedJsonSettings = atob(base64settings);
window.localStorage["WebConsole"] = decodedJsonSettings;
return true;
} catch (e) {
return false;
}
}
/**
* Export settings
* @returns A Base64-encoded JSON containing settings
*/
public getExportString(): string {
return btoa(window.localStorage["WebConsole"]);
}
}
export enum SettingsEnum {
DateTimePrefix,
RetrieveLogFile,
BlurryUri,
WiderViewport
}

View File

@ -0,0 +1,204 @@
import { Injectable } from '@angular/core';
import { map, Observable, Observer, Subject } from 'rxjs';
import { AnonymousSubject } from 'rxjs/internal/Subject';
import { ActiveConnectionDto } from '../_dto/ActiveConnectionDto';
import { WebSocketCommand } from '../_dto/command/WebSocketCommand';
import { WebSocketCommandEnum } from '../_dto/command/WebSocketCommandEnum';
import { ConnectionStatusEnum } from '../_dto/ConnectionStatusEnum';
import { ConsoleOutputResponse } from '../_dto/response/ConsoleOutputResponse';
import { CpuResponse } from '../_dto/response/CpuResponse';
import { LoggedInResponse } from '../_dto/response/LoggedInResponse';
import { LoginRequiredResponse } from '../_dto/response/LoginRequiredResponse';
import { PlayersResponse } from '../_dto/response/PlayersResponse';
import { RamResponse } from '../_dto/response/RamResponse';
import { TpsResponse } from '../_dto/response/TpsResponse';
import { UnknownCommandResponse } from '../_dto/response/UnknownCommandResponse';
import { WebSocketResponse } from '../_dto/response/WebSocketResponse';
import { ServerDto } from '../_dto/ServerDto';
import { SettingsEnum, StorageService } from './storage.service';
@Injectable({
providedIn: 'root'
})
export class WebconsoleService {
//Array as Index Signature which stores ActiveConnectionDto. This object
private activeConnections: ActiveConnectionDto[] = [];
//WebSocket connections stored separately, as we want all interactions with WebSockets to be done from this service.
private webSocketClients: { [key: string]: WebSocket | undefined } = {};
//Subject used to notify subscribers when a server is connected or disconnected
private activeConnectionsChangedSubject$: Subject<void> = new Subject<void>();
constructor(
private storageService: StorageService,
) { }
/**
* Returns a list containing the server names WebConsole is currently connected to
* @returns List of Server names
*/
public getCurrentConnectedServers(): string[] {
return this.activeConnections.map(e => e.serverName);
}
/**
* Notifies subscribers when a server is connected or disconnected
* @returns Subject used to notify when a server is connected or disconnected
*/
public getActiveConnectionsChangedSubject(): Subject<void> {
return this.activeConnectionsChangedSubject$;
}
/**
* Connects to a server or returns previously created one
* @param serverName Name of the server to connect to
* @returns Created connection or previously created one, if not closed.
*/
public connect(serverName: string): ActiveConnectionDto {
//If already connected to this server, return the already created Subject
const activeConnection = this.activeConnections.find(e => e.serverName == serverName);
if (activeConnection) {
return activeConnection;
}
//If not already connected, connect to server
const server: ServerDto | undefined = this.storageService.getServer(serverName);
if (!server)
throw Error("Server not found");
console.log(`Connecting to ${serverName} (${server.serverURI})`);
const connection = this.createConnection(serverName, server.serverURI);
//Save connection and return it
this.activeConnections.push(connection);
this.activeConnectionsChangedSubject$.next();
return connection;
}
/**
* Establish WS connection
* @param serverUri WebSockets URI
* @returns Created AnonimousSubject for this server
*/
private createConnection(serverName: string, serverUri: string): ActiveConnectionDto {
const ws = new WebSocket(serverUri);
const newConnection: ActiveConnectionDto = {
serverName: serverName,
subject$: new Subject<WebSocketResponse>(),
connectionStatus: ConnectionStatusEnum.Connecting,
receivedMessages: [],
sentCommands: [],
isLoggedIn: false
}
ws.onopen = (ev) => newConnection.connectionStatus = ConnectionStatusEnum.Connected;
ws.onerror = (err) => newConnection.subject$.error(err);
ws.onclose = () => this.closeConnection(serverName);
ws.onmessage = (msg) => {
//Parse raw message to an actual Object
let parsedResponse: WebSocketResponse = this.parseResponse(msg, newConnection);
//Save response
newConnection.receivedMessages.push(parsedResponse);
//Emit to subscribers
newConnection.subject$.next(parsedResponse);
}
//Store WebSocket client
this.webSocketClients[serverName] = ws;
//Return connection
return newConnection;
}
/**
* Receives raw message from server and parses it to a actual WebSocketResponse object
* @param response
*/
private parseResponse(response: MessageEvent<string>, newConnection: ActiveConnectionDto): WebSocketResponse {
let parsedJson = JSON.parse(response.data) as WebSocketResponse;
switch (parsedJson.status) {
case 10:
//Console output
return parsedJson as ConsoleOutputResponse;
case 200:
//LoggedIn
const r = parsedJson as LoggedInResponse;
newConnection.isLoggedIn = true;
newConnection.token = r.token;
if (this.storageService.getSetting(SettingsEnum.RetrieveLogFile))
this.sendMessage(newConnection.serverName, WebSocketCommandEnum.ReadLogFile);
return r;
case 400:
//Unknown
return parsedJson as UnknownCommandResponse;
case 401:
//Login Required
return parsedJson as LoginRequiredResponse;
case 1000:
//Players
return parsedJson as PlayersResponse;
case 1001:
//CPU Usage
return parsedJson as CpuResponse;
case 1002:
//RAM usage
return parsedJson as RamResponse;
case 1003:
//TPS
return parsedJson as TpsResponse;
default:
//Not recognised response
console.error("Unrecognised response:", response);
return parsedJson;
}
}
/**
* Send a command to a given server
* @param serverName Name of the server the command is being sent to
* @param command Command to send
*/
public sendMessage(serverName: string, commandType: WebSocketCommandEnum, params?: string): void {
//Get ActiveConnection from array
const serverConnection = this.activeConnections.find(e => e.serverName === serverName);
if (!serverConnection)
throw Error(`ActiveConnection not found for server ${serverName} whilst trying to send a message.`);
//Get WebSocket client from array. If not found, throw error
const ws = this.webSocketClients[serverName];
if (!ws)
throw Error(`WebSocket client not found for server ${serverName} whilst trying to send a message.`);
//Build and send command if socket is open.
if (ws.readyState === WebSocket.OPEN) {
const command: WebSocketCommand = {
command: commandType,
params: params,
token: serverConnection.token
}
ws.send(JSON.stringify(command));
serverConnection.sentCommands.push(command);
} else {
console.error(`Message to ${serverName} NOT sent because socket is not open yet.`);
}
}
/**
* Close connection with a server. This method needs to be here in order to be able to modify activeConnections$
* @param serverName Server name which wants connection to be closed
*/
public closeConnection(serverName: string): void {
const serverConnection = this.activeConnections.find(e => e.serverName === serverName);
if (serverConnection) {
serverConnection.subject$.complete();
serverConnection.connectionStatus = ConnectionStatusEnum.Disconnected;
this.webSocketClients[serverName] = undefined;
this.activeConnections = this.activeConnections.filter(e => e.serverName !== serverName);
this.activeConnectionsChangedSubject$.next();
}
}
}

View File

@ -0,0 +1,30 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LayoutComponent } from './core/layout/layout.component';
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
//Content
{
path: '',
loadChildren: () =>
import('./content/content.module').then(
(m) => m.ContentModule
)
}
]
},
{
path: '**',
redirectTo: ''
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

View File

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'WebConsoleClient'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('WebConsoleClient');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('WebConsoleClient app is running!');
});
});

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'WebConsole Client';
}

View File

@ -0,0 +1,26 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SharedModule } from './shared/shared.module';
import { CoreModule } from './core/core.module';
import { HttpClientModule } from '@angular/common/http';
import { LanguageModule } from './core/language.module';
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
SharedModule,
CoreModule,
HttpClientModule,
LanguageModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -0,0 +1,47 @@
<h1 *ngIf="!asModal">{{ "ADDEDITSERVER.AddNewServer" | translate }}</h1>
<form class="row g-3" [formGroup]="addServerFormGroup" (ngSubmit)="saveServer()">
<div class="col-12">
<label for="inputName" class="form-label">{{ "ADDEDITSERVER.Name" | translate }}</label>
<input type="text" class="form-control" id="inputName" placeholder="{{ 'ADDEDITSERVER.NamePlaceholder' | translate }}" formControlName="serverNameControl"
[class.is-invalid]="addServerFormGroup.get('serverNameControl')?.invalid && (addServerFormGroup.get('serverNameControl')?.dirty || addServerFormGroup.get('serverNameControl')?.touched)">
<div class="invalid-feedback">
{{ "ADDEDITSERVER.RequiredOrTooLongField" | translate }}
</div>
<div *ngIf="serverAlreadyExists" class="text-danger">
{{ "ADDEDITSERVER.ServerAlreadyExist" | translate }}
</div>
</div>
<div class="col-lg-10">
<label for="inputIp" class="form-label">{{ "ADDEDITSERVER.Ip" | translate }}</label>
<input type="text" class="form-control" id="inputIp" placeholder="{{ 'ADDEDITSERVER.IpPlaceholder' | translate }}" formControlName="serverIpControl"
[class.is-invalid]="addServerFormGroup.get('serverIpControl')?.invalid && (addServerFormGroup.get('serverIpControl')?.dirty || addServerFormGroup.get('serverIpControl')?.touched)">
<div class="invalid-feedback">
{{ "ADDEDITSERVER.RequiredField" | translate }}
</div>
</div>
<div class="col-lg-2">
<label for="inputPort" class="form-label">{{ "ADDEDITSERVER.Port" | translate }}</label>
<input type="number" class="form-control" id="inputPort" formControlName="serverPortControl"
[class.is-invalid]="addServerFormGroup.get('serverPortControl')?.invalid && (addServerFormGroup.get('serverPortControl')?.dirty || addServerFormGroup.get('serverPortControl')?.touched)">
<div class="invalid-feedback">
{{ "ADDEDITSERVER.InvalidPort" | translate }}
</div>
</div>
<div class="col-12">
<label for="inputPassword" class="form-label">{{ "ADDEDITSERVER.Password" | translate }}</label>
<input type="password" class="form-control" id="inputPassword" placeholder="{{ 'ADDEDITSERVER.PasswordPlaceholder' | translate }}" formControlName="serverPasswordControl"
[class.is-invalid]="addServerFormGroup.get('serverPasswordControl')?.invalid && (addServerFormGroup.get('serverPasswordControl')?.dirty || addServerFormGroup.get('serverPasswordControl')?.touched)">
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sslCheck" formControlName="serverSslEnabledControl">
<label class="form-check-label" for="sslCheck">
{{ "ADDEDITSERVER.SslEnabled" | translate }}
</label>
<p *ngIf="isClientOverHttps" class="text-warning">{{ "ADDEDITSERVER.SslEnabledMandatory" | translate }}</p>
</div>
</div>
<div *ngIf="!asModal" class="col-12">
<button type="submit" class="btn btn-primary">{{ "ADDEDITSERVER.Add" | translate }}</button>
</div>
</form>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddServerComponent } from './add-server.component';
describe('AddServerComponent', () => {
let component: AddServerComponent;
let fixture: ComponentFixture<AddServerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AddServerComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddServerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,72 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { StorageService } from 'src/app/_services/storage.service';
@Component({
selector: 'app-add-server',
templateUrl: './add-server.component.html',
styleUrls: ['./add-server.component.scss']
})
export class AddServerComponent implements OnInit {
@Input() asModal: boolean = false; //If component is being rendered inside a modal
//SSL detected
isClientOverHttps: boolean = location.protocol == 'https:';
//Add server FormGroup
addServerFormGroup = new FormGroup({
serverNameControl: new FormControl('', [Validators.required, Validators.maxLength(50)]),
serverIpControl: new FormControl('', [Validators.required]),
serverPortControl: new FormControl(8080, [Validators.required, Validators.min(0), Validators.max(99999)]),
serverPasswordControl: new FormControl(''),
serverSslEnabledControl: new FormControl({ value: location.protocol == 'https:', disabled: location.protocol == 'https:' }),
});
//A server with this name was found during save operation
serverAlreadyExists: boolean = false;
constructor(
private storageService: StorageService,
) { }
ngOnInit(): void {
}
saveServer(modal?: any): void {
//If form is invalid, stop saving operation
if (!this.addServerFormGroup.valid) {
this.addServerFormGroup.markAllAsTouched()
return;
}
//Get form information
const serverName: string = this.addServerFormGroup.get("serverNameControl")?.value.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "").replace(/"/g, "");
const serverIp: string = this.addServerFormGroup.get("serverIpControl")?.value;
const serverPort: string = this.addServerFormGroup.get("serverPortControl")?.value;
const serverPassword: string = this.addServerFormGroup.get("serverPasswordControl")?.value || undefined;
const serverSsl: boolean = this.addServerFormGroup.get("serverSslEnabledControl")?.value;
//If a server with this same name exists, stop saving operation
this.serverAlreadyExists = this.storageService.getAllServers().find(e => e.serverName == serverName) != undefined;
if (this.serverAlreadyExists) {
return;
}
//Build URI
let uri;
if (serverSsl) {
uri = "wss://" + serverIp + ":" + serverPort;
} else {
uri = "ws://" + serverIp + ":" + serverPort;
}
//Save server
this.storageService.saveServer(serverName, uri, serverPassword);
//If component is being shown in a modal, close it
if (this.asModal)
modal?.close('Save server');
}
}

View File

@ -0,0 +1,130 @@
<!-- Server name, insights toggler and badges -->
<div class="row">
<div class="col">
<h1>{{ server.serverName }}</h1>
</div>
<div class="col-md-auto d-flex align-self-center">
<button type="button" class="btn btn-outline-primary" *ngIf="showConsole" (click)="showServerInfo = !showServerInfo">
<fa-icon *ngIf="!showServerInfo" [icon]="icons.faEye"></fa-icon>
<fa-icon *ngIf="showServerInfo" [icon]="icons.faEyeSlash"></fa-icon> {{ "CONSOLE.ToggleServerInfo" | translate }}
</button>
</div>
</div>
<span class="badge bg-success me-1" *ngIf="activeConnection.connectionStatus == 2">{{ "CONSOLE.Connected" | translate }}</span>
<span class="badge bg-danger mb-3" *ngIf="activeConnection.connectionStatus == 3 && showConsole">{{ "CONSOLE.Disconnected" | translate }}</span>
<span class="badge bg-secondary mb-3" *ngIf="activeConnection.connectionStatus == 2 && loggedInUsername">{{ "CONSOLE.LoggedInAs" | translate }} {{ loggedInUsername }} ({{ loggedInAs }})</span>
<!-- Connected collapsable: Only shown when connection is or was stablished -->
<div #collapseGlobal="ngbCollapse" [(ngbCollapse)]="!showConsole">
<!-- Server insights -->
<div class="row" #collapse="ngbCollapse" [(ngbCollapse)]="!showServerInfo">
<div class="col-lg-3">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ "CONSOLE.PlayersOnline" | translate }}</h5>
<p class="card-text">{{ connectedPlayers }} / {{ maxPlayers }}</p>
<p class="card-text">
<ngb-progressbar type="secondary" [value]="connectedPlayers*100/maxPlayers" [striped]="true" [animated]="true"></ngb-progressbar>
</p>
</div>
</div>
</div>
<div class="col-lg-3">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ "CONSOLE.CpuUsage" | translate }}</h5>
<p class="card-text">{{ cpuUsage }}%</p>
<p class="card-text">
<ngb-progressbar [type]="cpuUsage>80 ? 'danger' : 'secondary'" [value]="cpuUsage" [striped]="true" [animated]="true"></ngb-progressbar>
</p>
</div>
</div>
</div>
<div class="col-lg-3">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ "CONSOLE.RamUsage" | translate }}</h5>
<p class="card-text">{{ ramUsed }} MB / {{ ramMax }} MB</p>
<p class="card-text">
<ngb-progressbar [type]="ramUsed*100/ramMax>80 ? 'danger' : 'secondary'" [value]="ramUsed*100/ramMax" [striped]="true" [animated]="true"></ngb-progressbar>
</p>
</div>
</div>
</div>
<div class="col-lg-3">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ "CONSOLE.Tps" | translate }}</h5>
<p class="card-text">{{ tps }} / 20</p>
<p class="card-text">
<ngb-progressbar [type]="tps*100/20>80 ? 'success' : 'secondary'" [value]="tps*100/20" [striped]="true" [animated]="true"></ngb-progressbar>
</p>
</div>
</div>
</div>
</div>
<!-- Log in button -->
<div class="row" *ngIf="!activeConnection.isLoggedIn">
<div class="col-12">
<button type="button" class="btn btn-warning w-100" (click)="requestPassword()">
<fa-icon [icon]="icons.faLock"></fa-icon> {{ "CONSOLE.ClickToLogin" | translate }}
</button>
</div>
</div>
<!-- Console -->
<div class="card mt-3 mb-3">
<div #consoleDiv class="card-body overflow-auto text-light bg-dark console" [innerHTML]="consoleHtml | sanitize" [scrollTop]="keepScrollDown ? consoleDiv.scrollHeight : consoleDiv.scrollTop"></div>
</div>
<!-- Command Input -->
<div class="input-group mb-3">
<input #commandInput type="text" class="form-control" aria-label="Command" aria-describedby="button-command" (keyup)="onKeyUpCommandInput($event)" [disabled]="activeConnection.connectionStatus != 2">
<button class="btn btn-secondary" type="button" id="button-command" (click)="sendCommand()" [disabled]="activeConnection.connectionStatus != 2">{{ "CONSOLE.Send" | translate }}</button>
</div>
</div>
<!-- Connecting spinner -->
<div class="d-flex flex-column min-vh-100 align-items-center" *ngIf="activeConnection.connectionStatus == 1">
<div class="spinner-border" role="status">
<span class="visually-hidden">{{ "GENERAL.Loading" | translate }}</span>
</div>
<p>{{ "CONSOLE.Connecting" | translate }}</p>
</div>
<!-- Disconnected -->
<div class="d-flex flex-column min-vh-100 align-items-center" *ngIf="activeConnection.connectionStatus == 3 && !showConsole">
<fa-icon [icon]="icons.faXmark" size="3x"></fa-icon>
<p class="mb-0">{{ "CONSOLE.CannotConnect" | translate }}</p>
<p class="mb-0">{{ "CONSOLE.CannotConnectDescription1" | translate }} <a href="https://www.yougetsignal.com/tools/open-ports/" target="_blank">{{ "CONSOLE.Tool" | translate }}</a> {{ "CONSOLE.CannotConnectDescription2" | translate }}
</p>
</div>
<!-- Password modal -->
<ng-template #setPasswordModal let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{ "CONSOLE.PasswordRequested" | translate }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="modal.dismiss('Cross click')"></button>
</div>
<div class="modal-body">
<form class="row g-3">
<div class="col-12">
<input #passwordInput type="password" class="form-control" (keyup.enter)="setPassword(passwordInput.value, rememberInput.checked);modal.close();">
<small *ngIf="savedPasswordSent" class="form-text text-danger">{{ "CONSOLE.WrongPassword" | translate }}</small>
</div>
<div class="col-12">
<div class="form-check">
<input #rememberInput class="form-check-input" type="checkbox" id="rememberCheck">
<label class="form-check-label" for="rememberCheck">
{{ "CONSOLE.RememberPassword" | translate }}
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="setPassword(passwordInput.value, rememberInput.checked);modal.close();">{{ "CONSOLE.Connect" | translate }}</button>
</div>
</ng-template>

View File

@ -0,0 +1,3 @@
.console {
height: 480px;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ConsoleComponent } from './console.component';
describe('ConsoleComponent', () => {
let component: ConsoleComponent;
let fixture: ComponentFixture<ConsoleComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ConsoleComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ConsoleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,396 @@
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Subscription } from 'rxjs';
import { Icons } from 'src/app/shared/icons';
import { ActiveConnectionDto } from 'src/app/_dto/ActiveConnectionDto';
import { WebSocketCommand } from 'src/app/_dto/command/WebSocketCommand';
import { WebSocketCommandEnum } from 'src/app/_dto/command/WebSocketCommandEnum';
import { ConnectionStatusEnum } from 'src/app/_dto/ConnectionStatusEnum';
import { ConsoleOutputResponse } from 'src/app/_dto/response/ConsoleOutputResponse';
import { CpuResponse } from 'src/app/_dto/response/CpuResponse';
import { LoggedInResponse } from 'src/app/_dto/response/LoggedInResponse';
import { LoginRequiredResponse } from 'src/app/_dto/response/LoginRequiredResponse';
import { PlayersResponse } from 'src/app/_dto/response/PlayersResponse';
import { RamResponse } from 'src/app/_dto/response/RamResponse';
import { TpsResponse } from 'src/app/_dto/response/TpsResponse';
import { UnknownCommandResponse } from 'src/app/_dto/response/UnknownCommandResponse';
import { WebSocketResponse } from 'src/app/_dto/response/WebSocketResponse';
import { ServerDto } from 'src/app/_dto/ServerDto';
import { SettingsEnum, StorageService } from 'src/app/_services/storage.service';
import { WebconsoleService } from 'src/app/_services/webconsole.service';
@Component({
selector: 'app-console',
templateUrl: './console.component.html',
styleUrls: ['./console.component.scss']
})
export class ConsoleComponent implements OnInit, AfterViewInit, OnDestroy {
//General stuff
icons = Icons;
server!: ServerDto; //Server info
activeConnection!: ActiveConnectionDto; //Active connection object (which stores messages received, messages sent, subject, etc.)
subscription!: Subscription; //Current subscription by this component
//Content of the console
@ViewChild("consoleDiv", { static: false }) consoleDiv!: ElementRef;
consoleHtml: string = "";
//Password modal
@ViewChild("setPasswordModal", { static: false }) passwordModal!: ElementRef;
//Command input
@ViewChild("commandInput", { static: false }) commandInput!: ElementRef;
//Server Insights
connectedPlayers: number = 0;
maxPlayers: number = 0;
cpuUsage: number = 0;
ramFree: number = 0;
ramUsed: number = 0;
ramMax: number = 0;
tps: number = 0;
//Helper properties
keepScrollDown: boolean = true;
showServerInfo: boolean = true;
showConsole: boolean = false;
loggedInUsername: string = "";
loggedInAs: string = "";
savedPasswordSent: boolean = false;
browsingCommandHistoryIndex: number = -1;
insightsInverval!: any;
constructor(
private route: ActivatedRoute,
private router: Router,
private storageService: StorageService,
private webConsoleService: WebconsoleService,
private modalService: NgbModal,
) { }
/**
* On component initialization, connect to WebSocket server and subscribe to subjects (where WebSocket messages are received)
*/
ngOnInit(): void {
console.log("Init console component");
//Get server name from params
const routeParams: ParamMap = this.route.snapshot.paramMap;
const serverName = routeParams.get('serverName');
//If server name not provided, throw error and redirect to homepage
if (!serverName) {
this.router.navigate(['']);
throw Error("Server name not provided");
}
//Get server from its name. If not found, redirect to homepage
const serverObject = this.storageService.getServer(serverName);
if (!serverObject) {
this.router.navigate(['']);
throw Error("Server name invalid");
}
//Save server object and connect
this.server = serverObject;
//Connect to server
this.activeConnection = this.webConsoleService.connect(this.server.serverName);
this.showConsole = this.activeConnection.connectionStatus == ConnectionStatusEnum.Connected;
//Process old messages (In case we are resuming a session)
this.activeConnection.receivedMessages.forEach(e => this.processMessage(e));
//If not created, create the Players, CPU, RAM and TPS interval
if (!this.insightsInverval) {
this.insightsInverval = setInterval(() => {
this.requestServerInsights();
}, 2500);
}
//Subscribe to Subject to process received messages
this.subscription = this.activeConnection.subject$.subscribe({
next: (msg: WebSocketResponse) => {
this.showConsole = true;
this.processMessage(msg);
},
complete: () => {
//Disconnected from server
this.showServerInfo = false;
}
});
}
ngAfterViewInit(): void {
//Scroll down console
setTimeout(() => this.consoleDiv.nativeElement.scrollTop = this.consoleDiv?.nativeElement.scrollHeight)
}
/**
* On component destruction, unsubscribe to subject
*/
ngOnDestroy(): void {
//Stop insights
clearInterval(this.insightsInverval);
//Remove subscription as this component is going mayday
this.subscription.unsubscribe();
}
/**
* Process a new message from WebSockets
* @param response WebSocket message
*/
processMessage(response: WebSocketResponse): void {
// console.log(`Received message from WebSocket (${this.server.serverName}): `, msg);
let r;
switch (response.status) {
case 10:
//Console output
r = response as ConsoleOutputResponse;
this.writeToWebConsole(r.message, r.time);
break;
case 200:
//LoggedIn
r = response as LoggedInResponse;
this.loggedInUsername = r.username;
this.loggedInAs = r.as;
break;
case 400:
//Unknown
r = response as UnknownCommandResponse;
console.log("400 Unknown Comamnd", r);
break;
case 401:
//Login Required
r = response as LoginRequiredResponse;
if (!this.activeConnection.isLoggedIn) {
if (this.server.serverPassword && !this.savedPasswordSent) {
this.savedPasswordSent = true;
this.webConsoleService.sendMessage(this.server.serverName, WebSocketCommandEnum.Login, this.server.serverPassword);
} else {
this.requestPassword();
}
}
break;
case 1000:
//Players
r = response as PlayersResponse;
this.connectedPlayers = r.connectedPlayers;
this.maxPlayers = r.maxPlayers;
break;
case 1001:
//CPU Usage
r = response as CpuResponse;
this.cpuUsage = r.usage;
break;
case 1002:
//RAM usage
r = response as RamResponse;
this.ramFree = r.free;
this.ramUsed = r.used;
this.ramMax = r.max;
break;
case 1003:
//TPS
r = response as TpsResponse;
this.tps = r.tps;
break;
default:
//Not recognised response
console.error("Unrecognised response:", response);
break;
}
}
/**
* Sanitize and print message to console
* @param msg Message to print
* @param time Time, if applicable
*/
private writeToWebConsole(msg: string, time: string) {
this.keepScrollDown = this.consoleDiv?.nativeElement.scrollHeight - this.consoleDiv?.nativeElement.scrollTop === this.consoleDiv?.nativeElement.clientHeight;
//Write to div, replacing < to &lt; (to avoid XSS) and replacing new line to br.
msg = msg.replace(/</g, "&lt;");
msg = msg.replace(/(?:\r\n|\r|\n)/g, "<br>");
//Color filter for Windows (thanks to SuperPykkon)
msg = msg.replace(/\[0;30;22m/g, "<span style='color: #000000;'>"); //&0
msg = msg.replace(/\[0;34;22m/g, "<span style='color: #0000AA;'>"); //&1
msg = msg.replace(/\[0;32;22m/g, "<span style='color: #00AA00;'>"); //&2
msg = msg.replace(/\[0;36;22m/g, "<span style='color: #00AAAA;'>"); //&3
msg = msg.replace(/\[0;31;22m/g, "<span style='color: #AA0000;'>"); //&4
msg = msg.replace(/\[0;35;22m/g, "<span style='color: #AA00AA;'>"); //&5
msg = msg.replace(/\[0;33;22m/g, "<span style='color: #FFAA00;'>"); //&6
msg = msg.replace(/\[0;37;22m/g, "<span style='color: #AAAAAA;'>"); //&7
msg = msg.replace(/\[0;30;1m/g, "<span style='color: #555555;'>"); //&8
msg = msg.replace(/\[0;34;1m/g, "<span style='color: #5555FF;'>"); //&9
msg = msg.replace(/\[0;32;1m/g, "<span style='color: #55FF55;'>"); //&a
msg = msg.replace(/\[0;36;1m/g, "<span style='color: #55FFFF;'>"); //&b
msg = msg.replace(/\[0;31;1m/g, "<span style='color: #FF5555;'>"); //&c
msg = msg.replace(/\[0;35;1m/g, "<span style='color: #FF55FF;'>"); //&d
msg = msg.replace(/\[0;33;1m/g, "<span style='color: #FFFF55;'>"); //&e
msg = msg.replace(/\[0;37;1m/g, "<span style='color: #FFFFFF;'>"); //&f
msg = msg.replace(/\[m/g, "</span>"); //&f
//Color filter for UNIX (This is easier!)
msg = msg.replace(/§0/g, "<span style='color: #000000;'>"); //&0
msg = msg.replace(/§1/g, "<span style='color: #0000AA;'>"); //&1
msg = msg.replace(/§2/g, "<span style='color: #00AA00;'>"); //&2
msg = msg.replace(/§3/g, "<span style='color: #00AAAA;'>"); //&3
msg = msg.replace(/§4/g, "<span style='color: #AA0000;'>"); //&4
msg = msg.replace(/§5/g, "<span style='color: #AA00AA;'>"); //&5
msg = msg.replace(/§6/g, "<span style='color: #FFAA00;'>"); //&6
msg = msg.replace(/§7/g, "<span style='color: #AAAAAA;'>"); //&7
msg = msg.replace(/§8/g, "<span style='color: #555555;'>"); //&8
msg = msg.replace(/§9/g, "<span style='color: #5555FF;'>"); //&9
msg = msg.replace(/§a/g, "<span style='color: #55FF55;'>"); //&a
msg = msg.replace(/§b/g, "<span style='color: #55FFFF;'>"); //&b
msg = msg.replace(/§c/g, "<span style='color: #FF5555;'>"); //&c
msg = msg.replace(/§d/g, "<span style='color: #FF55FF;'>"); //&d
msg = msg.replace(/§e/g, "<span style='color: #FFFF55;'>"); //&e
msg = msg.replace(/§f/g, "<span style='color: #FFFFFF;'>"); //&f
msg = msg.replace(/§l/g, "<span style='font-weight:bold;'>"); //&l
msg = msg.replace(/§m/g, "<span style='text-decoration: line-through;'>"); //&m
msg = msg.replace(/§n/g, "<span style='text-decoration: underline;'>"); //&n
msg = msg.replace(/§o/g, "<span style='font-style: italic;'>"); //&o
msg = msg.replace(/§r/g, "</span>"); //&r
//Color filter for MC 1.18 (Also easy :D)
msg = msg.replace(/0/g, "<span style='color: #000000;'>"); //&0
msg = msg.replace(/1/g, "<span style='color: #0000AA;'>"); //&1
msg = msg.replace(/2/g, "<span style='color: #00AA00;'>"); //&2
msg = msg.replace(/3/g, "<span style='color: #00AAAA;'>"); //&3
msg = msg.replace(/4/g, "<span style='color: #AA0000;'>"); //&4
msg = msg.replace(/5/g, "<span style='color: #AA00AA;'>"); //&5
msg = msg.replace(/6/g, "<span style='color: #FFAA00;'>"); //&6
msg = msg.replace(/7/g, "<span style='color: #AAAAAA;'>"); //&7
msg = msg.replace(/8/g, "<span style='color: #555555;'>"); //&8
msg = msg.replace(/9/g, "<span style='color: #5555FF;'>"); //&9
msg = msg.replace(/a/g, "<span style='color: #55FF55;'>"); //&a
msg = msg.replace(/b/g, "<span style='color: #55FFFF;'>"); //&b
msg = msg.replace(/c/g, "<span style='color: #FF5555;'>"); //&c
msg = msg.replace(/d/g, "<span style='color: #FF55FF;'>"); //&d
msg = msg.replace(/e/g, "<span style='color: #FFFF55;'>"); //&e
msg = msg.replace(/f/g, "<span style='color: #FFFFFF;'>"); //&f
msg = msg.replace(/l/g, "<span style='font-weight:bold;'>"); //&l
msg = msg.replace(/m/g, "<span style='text-decoration: line-through;'>"); //&m
msg = msg.replace(/n/g, "<span style='text-decoration: underline;'>"); //&n
msg = msg.replace(/o/g, "<span style='font-style: italic;'>"); //&o
msg = msg.replace(/r/g, "</span>"); //&r
//Append datetime if enabled
if (this.storageService.getSetting(SettingsEnum.DateTimePrefix)) {
if (typeof time !== 'undefined' && time !== null) //if time is present and not null
msg = "[" + time + "] " + msg;
else if (typeof time !== 'undefined' && time === null) //if time is present and null
null; //no time (is already printed)
else
msg = "[" + new Date().toLocaleTimeString() + "] " + msg;
}
//Append HTML
const spanCount = (msg.match(/<span /g) || []).length; //Number of times a color is applied
const spanCloseCount = (msg.match(/<\/span> /g) || []).length; //Number of already existing </span>
const numberOfUnclosedSpans: number = spanCount - spanCloseCount; //Number of </span> pending to be closed
this.consoleHtml += msg + ("</span>".repeat(numberOfUnclosedSpans)) + "<br>"; //Append to console the message, plus the required </span>'s, plus a line break
}
/**
* Open password request modal
*/
requestPassword(): void {
this.modalService.open(this.passwordModal, { size: 'md' });
}
/**
* Try to login against server
* @param password Password to send
* @param rememberPassword If true, save password in localStorage
*/
setPassword(password: string, rememberPassword: boolean): void {
//Edit server if remember password checkbox is checked
if (rememberPassword)
this.storageService.saveServer(this.server.serverName, this.server.serverURI, password);
setTimeout(() => this.savedPasswordSent = true, 200)
//Send login message
this.webConsoleService.sendMessage(this.server.serverName, WebSocketCommandEnum.Login, password);
}
/**
* Send command typed in the command input
*/
sendCommand(): void {
const cmd: string = this.commandInput.nativeElement.value;
if (!cmd)
return;
//Clear input
this.commandInput.nativeElement.value = "";
this.browsingCommandHistoryIndex = -1;
this.webConsoleService.sendMessage(this.server.serverName, WebSocketCommandEnum.Exec, cmd);
}
/**
* Called when a key is pressed in the command input
* @param e KeyboardEvent
*/
onKeyUpCommandInput(e: KeyboardEvent): void {
if (e.code === 'Enter') { //Detect enter key
this.sendCommand();
} else if (e.code === "ArrowUp") { //Replace with older command
//Get list of sent commands
const sentCommands: WebSocketCommand[] = this.activeConnection.sentCommands.filter(e => e.command === WebSocketCommandEnum.Exec);
//If no command was sent yet, return
if (sentCommands.length == 0)
return;
//If this is the first time arrow up is pressed, start browsing history
if (this.browsingCommandHistoryIndex <= 0)
this.browsingCommandHistoryIndex = sentCommands.length;
//Set command in our input component
this.commandInput.nativeElement.value = sentCommands[this.browsingCommandHistoryIndex - 1]?.params;
this.browsingCommandHistoryIndex = this.browsingCommandHistoryIndex - 1;
} else if (e.code === "ArrowDown") { //Replace with newer command
//Get list of sent commands
const sentCommands: WebSocketCommand[] = this.activeConnection.sentCommands.filter(e => e.command === WebSocketCommandEnum.Exec);
//If not browsing history, do nothing
if (this.browsingCommandHistoryIndex !== -1) {
//Go back to index 0 if overflow
if (this.browsingCommandHistoryIndex >= sentCommands.length - 1)
this.browsingCommandHistoryIndex = -1;
//Set command in our input component
this.commandInput.nativeElement.value = sentCommands[this.browsingCommandHistoryIndex + 1]?.params;
this.browsingCommandHistoryIndex = this.browsingCommandHistoryIndex + 1;
}
} else if (e.code == "tab") { //Detect tab key
//Suggest user from connected Players
//TODO tab not being detected :(
}
}
/**
* Request server insights
*/
requestServerInsights(): void {
if (this.showServerInfo && this.showConsole && this.activeConnection.connectionStatus == ConnectionStatusEnum.Connected && this.activeConnection.isLoggedIn) {
this.webConsoleService.sendMessage(this.server.serverName, WebSocketCommandEnum.Players);
this.webConsoleService.sendMessage(this.server.serverName, WebSocketCommandEnum.CpuUsage);
this.webConsoleService.sendMessage(this.server.serverName, WebSocketCommandEnum.RamUsage);
this.webConsoleService.sendMessage(this.server.serverName, WebSocketCommandEnum.Tps);
}
}
}
//TODO falta por hacer:
//1. Listado de activeConnections para desktop a modo pestañas
//2. Listado de activeConnections en responsive

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ConsoleComponent } from './console/console.component';
import { IndexComponent } from './index/index.component';
import { SettingsComponent } from './settings/settings.component';
const routes: Routes = [
{ path: 'console/:serverName', component: ConsoleComponent },
{ path: 'settings', component: SettingsComponent },
{ path: '', component: IndexComponent },
{ path: '**', component: IndexComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ContentRoutingModule { }

View File

@ -0,0 +1,25 @@
import { NgModule } from '@angular/core';
import { ContentRoutingModule } from './content-routing.module';
import { IndexComponent } from './index/index.component';
import { SettingsComponent } from './settings/settings.component';
import { SharedModule } from '../shared/shared.module';
import { AddServerComponent } from './add-server/add-server.component';
import { EditServerComponent } from './edit-server/edit-server.component';
import { ConsoleComponent } from './console/console.component';
@NgModule({
declarations: [
IndexComponent,
SettingsComponent,
AddServerComponent,
EditServerComponent,
ConsoleComponent
],
imports: [
ContentRoutingModule,
SharedModule
]
})
export class ContentModule { }

View File

@ -0,0 +1,49 @@
<h1 *ngIf="!asModal">{{ "ADDEDITSERVER.EditServer" | translate }}</h1>
<form class="row g-3" [formGroup]="editServerFormGroup" (ngSubmit)="saveServer()">
<div class="col-12">
<label for="inputName" class="form-label">{{ "ADDEDITSERVER.Name" | translate }}</label>
<input type="text" class="form-control" id="inputName" placeholder="{{ 'ADDEDITSERVER.NamePlaceholder' | translate }}" formControlName="serverNameControl">
<small class="form-text text-warning">{{ "ADDEDITSERVER.NameNotEditable" | translate }}</small>
</div>
<div class="col-lg-10">
<label for="inputIp" class="form-label">{{ "ADDEDITSERVER.Ip" | translate }}</label>
<input type="text" class="form-control" id="inputIp" placeholder="{{ 'ADDEDITSERVER.IpPlaceholder' | translate }}" formControlName="serverIpControl"
[class.is-invalid]="editServerFormGroup.get('serverIpControl')?.invalid && (editServerFormGroup.get('serverIpControl')?.dirty || editServerFormGroup.get('serverIpControl')?.touched)">
<div class="invalid-feedback">
{{ "ADDEDITSERVER.RequiredField" | translate }}
</div>
</div>
<div class="col-lg-2">
<label for="inputPort" class="form-label">{{ "ADDEDITSERVER.Port" | translate }}</label>
<input type="number" class="form-control" id="inputPort" formControlName="serverPortControl"
[class.is-invalid]="editServerFormGroup.get('serverPortControl')?.invalid && (editServerFormGroup.get('serverPortControl')?.dirty || editServerFormGroup.get('serverPortControl')?.touched)">
<div class="invalid-feedback">
{{ "ADDEDITSERVER.InvalidPort" | translate }}
</div>
</div>
<div class="col-12">
<label for="inputPassword" class="form-label">{{ "ADDEDITSERVER.Password" | translate }}</label>
<input type="password" class="form-control" id="inputPassword" placeholder="{{ 'ADDEDITSERVER.PasswordPlaceholder' | translate }}" formControlName="serverPasswordControl"
[class.is-invalid]="editServerFormGroup.get('serverPasswordControl')?.invalid && (editServerFormGroup.get('serverPasswordControl')?.dirty || editServerFormGroup.get('serverPasswordControl')?.touched)">
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="updatePasswordCheck" formControlName="keepServerPasswordControl" (change)="onUpdatePasswordCheckboxChange()">
<label class="form-check-label" for="updatePasswordCheck">
{{ "ADDEDITSERVER.KeepPasswordUnchanged" | translate }}
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sslCheck" formControlName="serverSslEnabledControl">
<label class="form-check-label" for="sslCheck">
{{ "ADDEDITSERVER.SslEnabled" | translate }}
</label>
<p *ngIf="isClientOverHttps" class="text-warning">{{ "ADDEDITSERVER.SslEnabledMandatory" | translate }}</p>
</div>
</div>
<div *ngIf="!asModal" class="col-12">
<button type="submit" class="btn btn-primary">{{ "ADDEDITSERVER.EditServer" | translate }}</button>
</div>
</form>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditServerComponent } from './edit-server.component';
describe('EditServerComponent', () => {
let component: EditServerComponent;
let fixture: ComponentFixture<EditServerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ EditServerComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(EditServerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,92 @@
import { AfterViewChecked, Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ServerDto } from 'src/app/_dto/ServerDto';
import { StorageService } from 'src/app/_services/storage.service';
@Component({
selector: 'app-edit-server',
templateUrl: './edit-server.component.html',
styleUrls: ['./edit-server.component.scss']
})
export class EditServerComponent implements OnInit {
@Input() asModal: boolean = false; //If component is being rendered inside a modal
//Name of the server to edit
@Input() serverNameBeingEdited!: string;
//SSL detected
isClientOverHttps: boolean = location.protocol == 'https:';
//Add server FormGroup
editServerFormGroup = new FormGroup({
serverNameControl: new FormControl({ value: '', disabled: true }, [Validators.required, Validators.maxLength(50)]),
serverIpControl: new FormControl('', [Validators.required]),
serverPortControl: new FormControl(8080, [Validators.required, Validators.min(0), Validators.max(99999)]),
serverPasswordControl: new FormControl({ value: '', disabled: true }),
serverSslEnabledControl: new FormControl({ value: location.protocol == 'https:', disabled: location.protocol == 'https:' }),
keepServerPasswordControl: new FormControl(true),
});
constructor(
private storageService: StorageService,
) { }
ngOnInit(): void {
//Load server info
const savedServer: ServerDto | undefined = this.storageService.getServer(this.serverNameBeingEdited);
if (savedServer) {
//Parse address and port
const addressWithPort = savedServer.serverURI.replace("wss://", "").replace("ws://", "");
const address = addressWithPort.slice(0, addressWithPort.lastIndexOf(":"));
const port = addressWithPort.slice(addressWithPort.lastIndexOf(":") + 1);
//Set values
this.editServerFormGroup.get("serverNameControl")?.setValue(savedServer.serverName);
this.editServerFormGroup.get("serverIpControl")?.setValue(address);
this.editServerFormGroup.get("serverPortControl")?.setValue(port);
savedServer.serverURI.startsWith("wss://")
? this.editServerFormGroup.get("serverSslEnabledControl")?.setValue(true)
: this.editServerFormGroup.get("serverSslEnabledControl")?.setValue(false)
}
}
onUpdatePasswordCheckboxChange(): void {
if (this.editServerFormGroup.get("keepServerPasswordControl")?.value){
this.editServerFormGroup.get("serverPasswordControl")?.disable();
this.editServerFormGroup.get("serverPasswordControl")?.setValue('');
}
else
this.editServerFormGroup.get("serverPasswordControl")?.enable();
}
saveServer(modal?: any): void {
//If form is invalid, stop edit operation
if (!this.editServerFormGroup.valid) {
this.editServerFormGroup.markAllAsTouched()
return;
}
//Get form information
const serverName: string = this.editServerFormGroup.get("serverNameControl")?.value.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "").replace(/"/g, "");
const serverIp: string = this.editServerFormGroup.get("serverIpControl")?.value;
const serverPort: string = this.editServerFormGroup.get("serverPortControl")?.value;
const serverPassword: string = this.editServerFormGroup.get("serverPasswordControl")?.value || null;
const serverSsl: boolean = this.editServerFormGroup.get("serverSslEnabledControl")?.value;
//Build URI
let uri;
if (serverSsl) {
uri = "wss://" + serverIp + ":" + serverPort;
} else {
uri = "ws://" + serverIp + ":" + serverPort;
}
//Save server
this.storageService.saveServer(serverName, uri, serverPassword);
//If component is being shown in a modal, close it
if (this.asModal)
modal?.close('Save server');
}
}

View File

@ -0,0 +1,109 @@
<div class="row mb-3">
<div class="col">
<h1>{{ "HOME.YourServers" | translate }}</h1>
</div>
<div class="col-md-auto d-flex align-self-center">
<button class="btn btn-primary" (click)="openModal(addModalContent)">
<fa-icon [icon]="icons.faAdd"></fa-icon> {{ "ADDEDITSERVER.AddNewServer" | translate }}
</button>
</div>
</div>
<p *ngIf="servers.length == 0">{{ "HOME.NoServersAdded" | translate }}</p>
<div class="table-responsive" *ngIf="servers.length > 0">
<table class="table table-striped table-hover text-center">
<thead>
<tr>
<th scope="col">{{ "GENERAL.Server" | translate }}</th>
<th scope="col" class="d-none d-lg-table-cell">{{ "HOME.ServerUri" | translate }}</th>
<th scope="col">{{ "HOME.Actions" | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let server of servers; let i = index">
<td>{{ server.serverName }}</td>
<td class="d-none d-lg-table-cell" [class.blurry-text]="blurryUris">{{ server.serverURI }}</td>
<td class="d-none d-lg-table-cell w-25">
<button type="button" class="btn btn-primary me-1" [ngbTooltip]="'HOME.Connect' | translate" (click)="connectServer(server.serverName)" [ngClass]="isConnectedTo(server.serverName) ? 'btn-success' : 'btn-primary'">
<fa-icon [icon]="icons.faTerminal"></fa-icon>
</button>
<button type="button" class="btn btn-primary me-1" [ngbTooltip]="'HOME.MoveUp' | translate" (click)="moveServerUp(server.serverName)">
<fa-icon [icon]="icons.faArrowUp"></fa-icon>
</button>
<button type="button" class="btn btn-primary me-1" [ngbTooltip]="'HOME.MoveDown' | translate" (click)="moveServerDown(server.serverName)">
<fa-icon [icon]="icons.faArrowDown"></fa-icon>
</button>
<button type="button" class="btn btn-primary me-1" [ngbTooltip]="'HOME.Edit' | translate" (click)="serverClicked = server; openModal(editModalContent)" [disabled]="isConnectedTo(server.serverName)">
<fa-icon [icon]="icons.faEdit"></fa-icon>
</button>
<button type="button" class="btn btn-danger" [ngbTooltip]="'HOME.Delete' | translate" (click)="deleteServer(server.serverName)">
<fa-icon [icon]="icons.faTrashCan"></fa-icon>
</button>
</td>
<td class="d-lg-none w-25">
<button type="button" class="btn btn-primary" (click)="serverClicked = server;openOffcanvas(serverDetailsOffcanvas)">
<fa-icon [icon]="icons.faAnglesRight"></fa-icon>
</button>
</td>
<!-- td para móvil con un boton que saque offcanvas. El offcanvas movil sera el mismo de navegacion, mostrara menu y conexiones activas ¿y luego otro offcanvas para las acciones? -->
</tr>
</tbody>
</table>
</div>
<!-- Modal template to add new server -->
<ng-template #addModalContent let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{ "ADDEDITSERVER.AddNewServer" | translate }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="modal.dismiss('Cross click')"></button>
</div>
<div class="modal-body">
<app-add-server #addServer [asModal]="true"></app-add-server>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="addServer.saveServer(modal);">{{ "ADDEDITSERVER.Add" | translate }}</button>
</div>
</ng-template>
<!-- Modal template to edit server -->
<ng-template #editModalContent let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{ "ADDEDITSERVER.EditServer" | translate }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="modal.dismiss('Cross click')"></button>
</div>
<div class="modal-body">
<app-edit-server #editServer [asModal]="true" [serverNameBeingEdited]="serverClicked.serverName"></app-edit-server>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="editServer.saveServer(modal);">{{ "ADDEDITSERVER.EditServer" | translate }}</button>
</div>
</ng-template>
<!-- Offcanvas template to show server details -->
<ng-template #serverDetailsOffcanvas let-offcanvas>
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{ "HOME.ServerDetails" | translate }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss('Cross click')"></button>
</div>
<div class="offcanvas-body">
<p><strong>{{ "GENERAL.Server" | translate }}:</strong> {{ serverClicked.serverName }}</p>
<p><strong>{{ "HOME.ServerUri" | translate }}:</strong> {{ serverClicked.serverURI }}</p>
<p><strong>{{ "HOME.Actions" | translate }}</strong></p>
<p>
<button type="button" class="btn btn-primary me-1" (click)="offcanvas.dismiss('Action'); connectServer(serverClicked.serverName)" [ngClass]="isConnectedTo(serverClicked.serverName) ? 'btn-success' : 'btn-primary'">
<fa-icon [icon]="icons.faTerminal"></fa-icon>
</button>
<button type="button" class="btn btn-primary me-1" (click)="offcanvas.dismiss('Action'); moveServerUp(serverClicked.serverName)">
<fa-icon [icon]="icons.faArrowUp"></fa-icon>
</button>
<button type="button" class="btn btn-primary me-1" (click)="offcanvas.dismiss('Action'); moveServerDown(serverClicked.serverName)">
<fa-icon [icon]="icons.faArrowDown"></fa-icon>
</button>
<button type="button" class="btn btn-primary me-1" (click)="offcanvas.dismiss('Action'); openModal(editModalContent)" [disabled]="isConnectedTo(serverClicked.serverName)">
<fa-icon [icon]="icons.faEdit"></fa-icon>
</button>
<button type="button" class="btn btn-danger" (click)="offcanvas.dismiss('Action'); deleteServer(serverClicked.serverName)">
<fa-icon [icon]="icons.faTrashCan"></fa-icon>
</button>
</p>
</div>
</ng-template>

View File

@ -0,0 +1,4 @@
.blurry-text {
text-shadow: 0 0 8px black;
color: transparent !important;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IndexComponent } from './index.component';
describe('IndexComponent', () => {
let component: IndexComponent;
let fixture: ComponentFixture<IndexComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ IndexComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(IndexComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,91 @@
import { Component, OnInit, TemplateRef } from '@angular/core';
import { ServerDto } from 'src/app/_dto/ServerDto';
import { SettingsEnum, StorageService } from 'src/app/_services/storage.service';
import { Icons } from 'src/app/shared/icons';
import { NgbModal, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
import { Router } from '@angular/router';
import { WebconsoleService } from 'src/app/_services/webconsole.service';
@Component({
selector: 'app-index',
templateUrl: './index.component.html',
styleUrls: ['./index.component.scss']
})
export class IndexComponent implements OnInit {
icons = Icons;
blurryUris: boolean = true;
servers: ServerDto[] = []; //List of servers
currentlyConnectedServers: string[] = [];
//Helper properties
serverClicked!: ServerDto; //When edit or details button is clicked, server is stored here to be able to send it to the modal or the offcasnvas
constructor(
private storageService: StorageService,
private webConsoleService: WebconsoleService,
private modalService: NgbModal,
private offcanvasService: NgbOffcanvas,
private router: Router,
) { }
ngOnInit(): void {
this.blurryUris = this.storageService.getSetting(SettingsEnum.BlurryUri);
this.refreshServerList();
this.currentlyConnectedServers = this.webConsoleService.getCurrentConnectedServers();
this.webConsoleService.getActiveConnectionsChangedSubject().subscribe({
next: () => this.currentlyConnectedServers = this.webConsoleService.getCurrentConnectedServers()
})
}
refreshServerList(): void {
this.servers = this.storageService.getAllServers();
}
isConnectedTo(serverName: string): boolean {
return this.currentlyConnectedServers.includes(serverName);
}
openModal(modalContent: TemplateRef<any>) {
this.modalService.open(modalContent, { size: 'lg' }).result.then((result) => {
//Fullfilled
this.refreshServerList();
}, (reason) => {
//Rejected
this.refreshServerList();
});
}
openOffcanvas(offcanvasContent: TemplateRef<any>) {
this.offcanvasService.open(offcanvasContent, { ariaLabelledBy: 'offcanvas-server-details', position: "end" }).result.then((result) => {
//Closed
}, (reason) => {
//Dismissed
});
}
connectServer(serverName: string): void {
this.router.navigate(['console', serverName]);
}
moveServerUp(serverName: string): void {
const currentIndex: number = this.servers.findIndex(e => e.serverName == serverName);
this.storageService.moveServerToIndex(serverName, currentIndex - 1);
this.refreshServerList();
}
moveServerDown(serverName: string): void {
const currentIndex: number = this.servers.findIndex(e => e.serverName == serverName);
this.storageService.moveServerToIndex(serverName, currentIndex + 1);
this.refreshServerList();
}
deleteServer(serverName: string): void {
this.webConsoleService.closeConnection(serverName);
this.storageService.deleteServer(serverName);
this.refreshServerList();
}
}

View File

@ -0,0 +1,81 @@
<h1>{{ "SETTINGS.WebConsoleClientSettings" | translate }}</h1>
<h2>{{ "SETTINGS.GeneralSettings" | translate }}</h2>
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="showDateSettingsSwitch" [(ngModel)]="isDateSwitchChecked" (change)="onSwitchChanges()">
<label class="form-check-label" for="showDateSettingsSwitch">{{ "SETTINGS.ShowTimeOnConsoleLine" | translate }}</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="readLogFileSwitch" [(ngModel)]="isLogFileSwitchChecked" (change)="onSwitchChanges()">
<label class="form-check-label" for="readLogFileSwitch">{{ "SETTINGS.RetrieveFullLogOnConnect" | translate }}</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="showUriSwitch" [(ngModel)]="isBlurrySwitchChecked" (change)="onSwitchChanges()">
<label class="form-check-label" for="showUriSwitch">{{ "SETTINGS.BlurryUriHomepage" | translate }}</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="widerSwitch" [(ngModel)]="isWiderSwitchChecked" (change)="onSwitchChanges()">
<label class="form-check-label" for="widerSwitch">{{ "SETTINGS.WiderViewport" | translate }}</label>
</div>
</div>
<hr />
<h2>{{ "SETTINGS.MigrateData" | translate }}</h2>
<p>{{ "SETTINGS.MigrateDataDescription" | translate }}</p>
<div class="mb-2">
<p>
<button type="button" class="btn btn-outline-primary" (click)="openExportCollapse()" [attr.aria-expanded]="!exportContainerCollapsed" aria-controls="collapseExport">
{{ "SETTINGS.ExportData" | translate }}
</button>
<button type="button" class="btn btn-outline-primary ms-2" (click)="openImportCollapse();" [attr.aria-expanded]="!exportContainerCollapsed" aria-controls="collapseImport">
{{ "SETTINGS.ImportData" | translate }}
</button>
<button *ngIf="!exportContainerCollapsed || !importContainerCollapsed" type="button" class="btn btn-outline-primary ms-2" (click)="closeMigrateCollapse();">
<fa-icon [icon]="icons.faClose"></fa-icon>
</button>
</p>
<div #collapseExport="ngbCollapse" [(ngbCollapse)]="exportContainerCollapsed">
<div class="card">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">{{ "SETTINGS.CopyString" | translate }}</h6>
<p class="card-text">{{ exportString }}</p>
</div>
</div>
</div>
<div #collapseImport="ngbCollapse" [(ngbCollapse)]="importContainerCollapsed">
<div class="input-group mb-3" [class.is-valid]="errorOccuredImporting === true" [class.is-invalid]="errorOccuredImporting === false">
<input type="text" class="form-control" placeholder="{{ 'SETTINGS.PasteString' | translate }}" aria-label="Paste import string..." aria-describedby="button-import" [(ngModel)]="importString">
<button class="btn btn-outline-primary" type="button" id="button-import" (click)="onImportClick()">{{ "SETTINGS.Import" | translate }}</button>
</div>
<div class="valid-feedback">
{{ "SETTINGS.ImportSuccessful" | translate }}
</div>
<div class="invalid-feedback">
{{ "SETTINGS.ImportFailed" | translate }}
</div>
</div>
</div>
<hr />
<h2>{{ "SETTINGS.Language" | translate }}</h2>
<p>{{ "SETTINGS.SelectLanguage" | translate }}</p>
<div class="mb-2">
<select [(ngModel)]="savedLanguage" (change)="onLanguageChanged()" class="form-select" aria-label="Language selector">
<option value="en">English</option>
<option value="es">Español</option>
<option value="zh">中文</option>
<option value="ko">한국어</option>
<option value="cs">Czech</option>
<option value="de">Deutsche</option>
<option value="nl">Dutch</option>
<option value="fr">Français</option>
<option value="it">Italiano</option>
<option value="pt">Português</option>
<option value="pl">Polskie</option>
<option value="ru">русский</option>
<option value="tr">Türk</option>
<option value="ja">日本語</option>
</select>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SettingsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,79 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { SettingsEnum, StorageService } from 'src/app/_services/storage.service';
import { Icons } from 'src/app/shared/icons';
import { LanguageService } from 'src/app/_services/language.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent implements OnInit {
icons = Icons;
//Switch values
isDateSwitchChecked!: boolean;
isLogFileSwitchChecked!: boolean;
isBlurrySwitchChecked!: boolean;
isWiderSwitchChecked!: boolean;
//Export data
exportContainerCollapsed: boolean = true;
exportString: string = "";
//Import data
importContainerCollapsed: boolean = true;
importString: string = "";
errorOccuredImporting: boolean | null = null;
//Language
savedLanguage!: string;
constructor(
private storageService: StorageService,
private languageService: LanguageService,
) { }
ngOnInit(): void {
//Initialize switches
this.isDateSwitchChecked = this.storageService.getSetting(SettingsEnum.DateTimePrefix);
this.isLogFileSwitchChecked = this.storageService.getSetting(SettingsEnum.RetrieveLogFile);
this.isBlurrySwitchChecked = this.storageService.getSetting(SettingsEnum.BlurryUri);
this.isWiderSwitchChecked = this.storageService.getSetting(SettingsEnum.WiderViewport);
//Initialize language selector
this.savedLanguage = this.storageService.getLanguage();
}
onSwitchChanges(): void {
this.storageService.setSetting(SettingsEnum.DateTimePrefix, this.isDateSwitchChecked);
this.storageService.setSetting(SettingsEnum.RetrieveLogFile, this.isLogFileSwitchChecked);
this.storageService.setSetting(SettingsEnum.BlurryUri, this.isBlurrySwitchChecked);
this.storageService.setSetting(SettingsEnum.WiderViewport, this.isWiderSwitchChecked);
}
openExportCollapse(): void {
this.exportString = this.storageService.getExportString();
this.exportContainerCollapsed = false;
this.importContainerCollapsed = true;
}
openImportCollapse(): void {
this.exportContainerCollapsed = true;
this.importContainerCollapsed = false;
}
closeMigrateCollapse(): void {
this.exportContainerCollapsed = true;
this.importContainerCollapsed = true;
}
onImportClick(): void {
this.errorOccuredImporting = this.storageService.importSettings(this.importString);
}
onLanguageChanged(): void {
console.log(`Change language to ${this.savedLanguage}`)
this.languageService.setLanguage(this.savedLanguage);
}
}

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { LayoutComponent } from './layout/layout.component';
import { ServerToolbarComponent } from './server-toolbar/server-toolbar.component';
@NgModule({
declarations: [
LayoutComponent,
ServerToolbarComponent,
],
imports: [
SharedModule
],
exports: [
LayoutComponent,
ServerToolbarComponent,
]
})
export class CoreModule { }

View File

@ -0,0 +1,45 @@
import { HttpClient } from "@angular/common/http";
import { NgModule } from "@angular/core";
import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { LanguageService } from "../_services/language.service";
export const HttpLoaderFactory = (http: HttpClient): TranslateHttpLoader => {
return new TranslateHttpLoader(http, "/assets/i18n/")
}
@NgModule({
declarations: [],
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})],
exports: []
})
export class LanguageModule {
readonly VALID_LANGUAGES = ["en", "es", "zh", "ko", "cs", "de", "nl", "fr", "it", "pt", "pl", "ru", "tr", "ja"];
constructor(private languageService: LanguageService) {
this.setup();
}
private setup(): void {
//If a language is set in persistence, and it is supported by the app, use it
const persistenceLanguage = this.languageService.getLanguage();
if (this.VALID_LANGUAGES.includes(persistenceLanguage)) {
this.languageService.setLanguage(persistenceLanguage);
} else {
//If language is not defined in persistence, check browser default language: If browser uses a supported language, use it. Otherwise, use english.
const browserLang = navigator.language.substring(0, 2);
const defaultLang = this.VALID_LANGUAGES.includes(browserLang) ? browserLang : "en";
this.languageService.setLanguage(defaultLang);
}
}
}

View File

@ -0,0 +1,54 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3">
<div class="container-fluid">
<a class="navbar-brand" routerLink="/">
<img src="assets/iconwhite.png" alt="Logo" width="30" height="30" class="d-inline-block align-text-top"> <span class="ms-3">WebConsole Client</span>
</a>
<button class="navbar-toggler" type="button" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation" (click)="openOffcanvas(navbarOffcanvasContent)">
<span class="navbar-toggler-icon" [class.rotated]="ifMobileNavigationOpen"></span>
</button>
<div class="collapse navbar-collapse">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">{{ "LAYOUT.Home" | translate }}</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/settings" routerLinkActive="active">{{ "LAYOUT.Settings" | translate }}</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
<span class="navbar-text">v2.5 - </span>
<a class="nav-link" target="_blank" href="https://github.com/mesacarlos/WebConsole">
<fa-icon [icon]="icons.faArrowUpRightFromSquare"></fa-icon> GitHub
</a>
</ul>
</div>
</div>
</nav>
<div [ngClass]="{ 'container': !isWiderViewportEnabled, 'container-fluid': isWiderViewportEnabled }" class="mb-5">
<!-- Content here -->
<router-outlet></router-outlet>
</div>
<app-server-toolbar></app-server-toolbar>
<ng-template #navbarOffcanvasContent let-offcanvas>
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{ "LAYOUT.Navigation" | translate }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss('Cross click')"></button>
</div>
<div class="offcanvas-body">
<a class="nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }" (click)="offcanvas.dismiss('Navigate')">{{ "LAYOUT.Home" | translate }}</a>
<a class="nav-link" routerLink="/settings" routerLinkActive="active" (click)="offcanvas.dismiss('Navigate')">{{ "LAYOUT.Settings" | translate }}</a>
<hr />
<h4>{{ "LAYOUT.CurrentlyConnectedTo" | translate }}</h4>
<p *ngIf="currentlyConnectedServers.length == 0">{{ "LAYOUT.NoConnectedToServers" | translate }}</p>
<ul *ngIf="currentlyConnectedServers.length != 0" class="nav flex-column">
<li class="nav-item" *ngFor="let server of currentlyConnectedServers">
<a class="d-inline-block nav-link active" [routerLink]="['/console', server]" (click)="offcanvas.dismiss('Connect')">{{ server }}</a>
<a class="d-inline-block nav-link active" (click)="disconnectServer(server)">({{ "LAYOUT.Disconnect" | translate }})</a>
</li>
</ul>
</div>
</ng-template>

View File

@ -0,0 +1,7 @@
.navbar-toggler-icon {
transition: transform 0.3s linear;
}
.rotated {
transform: rotate(-90deg);
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LayoutComponent } from './layout.component';
describe('LayoutComponent', () => {
let component: LayoutComponent;
let fixture: ComponentFixture<LayoutComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LayoutComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import { Component, OnInit, TemplateRef } from '@angular/core';
import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
import { Icons } from 'src/app/shared/icons';
import { StorageService } from 'src/app/_services/storage.service';
import { WebconsoleService } from 'src/app/_services/webconsole.service';
@Component({
selector: 'app-layout',
templateUrl: './layout.component.html',
styleUrls: ['./layout.component.scss']
})
export class LayoutComponent implements OnInit {
icons = Icons;
currentlyConnectedServers: string[] = [];
ifMobileNavigationOpen: boolean = false;
isWiderViewportEnabled!: boolean;
constructor(
private storageService: StorageService,
private webConsoleService: WebconsoleService,
private offcanvasService: NgbOffcanvas,
) { }
ngOnInit(): void {
this.storageService.widerViewportSubject.subscribe(value => this.isWiderViewportEnabled = value);
this.webConsoleService.getActiveConnectionsChangedSubject().subscribe({
next: () => this.currentlyConnectedServers = this.webConsoleService.getCurrentConnectedServers()
});
}
disconnectServer(serverName: string): void {
this.webConsoleService.closeConnection(serverName);
}
openOffcanvas(offcanvasContent: TemplateRef<any>) {
this.ifMobileNavigationOpen = true;
this.offcanvasService.open(offcanvasContent, { ariaLabelledBy: 'offcanvas-navigation' }).result.then((result) => {
//Closed
this.ifMobileNavigationOpen = false;
}, (reason) => {
//Dismissed
this.ifMobileNavigationOpen = false;
});
}
}

View File

@ -0,0 +1,28 @@
<div class="toolbar d-none d-lg-block">
<div *ngFor="let server of currentlyConnectedServers | slice:0:maxTabsToShow" class="toolbar-item ms-2 me-2 d-inline-flex">
<span class="toolbar-item-description hover ps-3 pe-3 flex-grow-1 d-flex align-items-center" [routerLink]="['/console', server]" routerLinkActive="bg-primary">{{ server }}</span>
<fa-icon class="toolbar-item-close hover d-flex align-items-center justify-content-center" [icon]="icons.faXmark" (click)="disconnectServer(server)"></fa-icon>
</div>
<div *ngIf="currentlyConnectedServers.length > maxTabsToShow" class="toolbar-item-all ms-2 me-2 d-inline-flex">
<fa-icon class="toolbar-item-all-icon hover d-flex align-items-center justify-content-center" [icon]="icons.faAdd" (click)="openOffcanvas(navbarOffcanvasContent)"></fa-icon>
</div>
</div>
<ng-template #navbarOffcanvasContent let-offcanvas>
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{ "LAYOUT.CurrentlyConnectedTo" | translate }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss('Cross click')"></button>
</div>
<div class="offcanvas-body" *ngIf="currentlyConnectedServers.length == 0">
<p>{{ "LAYOUT.NoConnectedToServers" | translate }}</p>
</div>
<div class="offcanvas-body" *ngIf="currentlyConnectedServers.length != 0">
<ul class="nav flex-column">
<li class="nav-item" *ngFor="let server of currentlyConnectedServers">
<a class="d-inline-block nav-link active" [routerLink]="['/console', server]" (click)="offcanvas.dismiss('Connect')">{{ server }}</a>
<a class="d-inline-block nav-link active" (click)="disconnectServer(server)">({{ "LAYOUT.Disconnect" | translate }})</a>
</li>
</ul>
</div>
</ng-template>

View File

@ -0,0 +1,47 @@
.toolbar {
position: fixed;
right: 0px;
bottom: 0px;
}
.toolbar-item {
width: 200px;
background-color: var(--bs-dark);
color: white;
border-radius: 10px 10px 0 0;
}
.toolbar-item-description {
height: 35px;
border-radius: 10px 0 0 0;
border-style: solid;
border-color: white;
border-width: 0 1px 0 0;
}
.toolbar-item-close {
height: 35px;
width: 30px;
border-radius: 0 10px 0 0;
}
.hover:hover {
background-color: var(--bs-secondary);
cursor: pointer;
}
.toolbar-item-all {
background-color: var(--bs-dark);
color: white;
border-radius: 10px 10px 0 0;
}
.toolbar-item-all-icon {
height: 38px;
width: 35px;
border-radius: 10px 10px 0 0;
}
a:hover {
cursor: pointer;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ServerToolbarComponent } from './server-toolbar.component';
describe('ServerToolbarComponent', () => {
let component: ServerToolbarComponent;
let fixture: ComponentFixture<ServerToolbarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ServerToolbarComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ServerToolbarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,44 @@
import { Component, HostListener, OnInit, TemplateRef } from '@angular/core';
import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
import { Icons } from 'src/app/shared/icons';
import { WebconsoleService } from 'src/app/_services/webconsole.service';
@Component({
selector: 'app-server-toolbar',
templateUrl: './server-toolbar.component.html',
styleUrls: ['./server-toolbar.component.scss']
})
export class ServerToolbarComponent implements OnInit {
icons = Icons;
currentlyConnectedServers: string[] = [];
maxTabsToShow: number = 0;
constructor(
private webConsoleService: WebconsoleService,
private offcanvasService: NgbOffcanvas,
) { }
ngOnInit(): void {
this.webConsoleService.getActiveConnectionsChangedSubject().subscribe({
next: () => this.currentlyConnectedServers = this.webConsoleService.getCurrentConnectedServers()
});
this.onResize();
}
disconnectServer(serverName: string): void {
this.webConsoleService.closeConnection(serverName);
}
@HostListener('window:resize', ['$event'])
onResize(event?: any): void {
const windowWidth: number = window.innerWidth;
//Each tab take aprox. 230px, with this info we can calculate how many tabs can we show at once
this.maxTabsToShow = Math.floor(windowWidth / 240);
}
openOffcanvas(offcanvasContent: TemplateRef<any>) {
this.offcanvasService.open(offcanvasContent, { ariaLabelledBy: 'offcanvas-connected-servers', position: "end" });
}
}

View File

@ -0,0 +1,18 @@
import { faAdd, faAnglesRight, faArrowDown, faArrowUp, faArrowUpRightFromSquare, faCircleXmark, faClose, faEdit, faEye, faEyeSlash, faLock, faTerminal, faTrashCan, faXmark, IconDefinition } from "@fortawesome/free-solid-svg-icons";
export const Icons = {
faAdd: faAdd,
faClose: faClose,
faTrashCan: faTrashCan,
faArrowUp: faArrowUp,
faArrowDown: faArrowDown,
faEdit: faEdit,
faTerminal: faTerminal,
faArrowUpRightFromSquare: faArrowUpRightFromSquare,
faAnglesRight: faAnglesRight,
faEye: faEye,
faEyeSlash: faEyeSlash,
faCircleXmark: faCircleXmark,
faXmark: faXmark,
faLock: faLock,
}

View File

@ -0,0 +1,15 @@
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Pipe({
name: 'sanitize'
})
export class SanitizePipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) { }
transform(html: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(html);
}
}

View File

@ -0,0 +1,29 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { RouterModule } from '@angular/router';
import { SanitizePipe } from './sanitize.pipe';
@NgModule({
exports: [
TranslateModule,
FormsModule,
FontAwesomeModule,
NgbModule,
ReactiveFormsModule,
RouterModule,
CommonModule,
SanitizePipe
],
declarations: [
SanitizePipe
],
imports: [
TranslateModule,
CommonModule
]
})
export class SharedModule { }