Brand new Angular client
This commit is contained in:
14
client/src/app/_dto/ActiveConnectionDto.ts
Normal file
14
client/src/app/_dto/ActiveConnectionDto.ts
Normal 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;
|
||||
}
|
5
client/src/app/_dto/ConnectionStatusEnum.ts
Normal file
5
client/src/app/_dto/ConnectionStatusEnum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum ConnectionStatusEnum {
|
||||
Connecting = 1,
|
||||
Connected = 2,
|
||||
Disconnected = 3
|
||||
}
|
5
client/src/app/_dto/ServerDto.ts
Normal file
5
client/src/app/_dto/ServerDto.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ServerDto {
|
||||
serverName: string;
|
||||
serverURI: string;
|
||||
serverPassword?: string;
|
||||
}
|
6
client/src/app/_dto/SettingsDto.ts
Normal file
6
client/src/app/_dto/SettingsDto.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface SettingsDto {
|
||||
dateTimePrefix: boolean;
|
||||
retrieveLogFile: boolean;
|
||||
blurryUri: boolean;
|
||||
widerViewport: boolean;
|
||||
}
|
8
client/src/app/_dto/StoredDataDto.ts
Normal file
8
client/src/app/_dto/StoredDataDto.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ServerDto } from "./ServerDto";
|
||||
import { SettingsDto } from "./SettingsDto";
|
||||
|
||||
export interface StoredDataDto {
|
||||
servers: ServerDto[];
|
||||
language: string;
|
||||
settings: SettingsDto;
|
||||
}
|
5
client/src/app/_dto/command/WebSocketCommand.ts
Normal file
5
client/src/app/_dto/command/WebSocketCommand.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface WebSocketCommand {
|
||||
command: string;
|
||||
params?: string;
|
||||
token?: string;
|
||||
}
|
9
client/src/app/_dto/command/WebSocketCommandEnum.ts
Normal file
9
client/src/app/_dto/command/WebSocketCommandEnum.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum WebSocketCommandEnum {
|
||||
Login = "LOGIN",
|
||||
Exec = "EXEC",
|
||||
Players = "PLAYERS",
|
||||
CpuUsage = "CPUUSAGE",
|
||||
RamUsage = "RAMUSAGE",
|
||||
Tps = "TPS",
|
||||
ReadLogFile = "READLOGFILE"
|
||||
}
|
6
client/src/app/_dto/response/ConsoleOutputResponse.ts
Normal file
6
client/src/app/_dto/response/ConsoleOutputResponse.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { WebSocketResponse } from "./WebSocketResponse";
|
||||
|
||||
export interface ConsoleOutputResponse extends WebSocketResponse{
|
||||
time: string;
|
||||
message: string;
|
||||
}
|
5
client/src/app/_dto/response/CpuResponse.ts
Normal file
5
client/src/app/_dto/response/CpuResponse.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { WebSocketResponse } from "./WebSocketResponse";
|
||||
|
||||
export interface CpuResponse extends WebSocketResponse{
|
||||
usage: number;
|
||||
}
|
8
client/src/app/_dto/response/LoggedInResponse.ts
Normal file
8
client/src/app/_dto/response/LoggedInResponse.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { WebSocketResponse } from "./WebSocketResponse";
|
||||
|
||||
export interface LoggedInResponse extends WebSocketResponse{
|
||||
respondsTo: string;
|
||||
username: string;
|
||||
as: string;
|
||||
token: string;
|
||||
}
|
5
client/src/app/_dto/response/LoginRequiredResponse.ts
Normal file
5
client/src/app/_dto/response/LoginRequiredResponse.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { WebSocketResponse } from "./WebSocketResponse";
|
||||
|
||||
export interface LoginRequiredResponse extends WebSocketResponse{
|
||||
|
||||
}
|
7
client/src/app/_dto/response/PlayersResponse.ts
Normal file
7
client/src/app/_dto/response/PlayersResponse.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { WebSocketResponse } from "./WebSocketResponse";
|
||||
|
||||
export interface PlayersResponse extends WebSocketResponse {
|
||||
connectedPlayers: number;
|
||||
maxPlayers: number;
|
||||
players: string[];
|
||||
}
|
7
client/src/app/_dto/response/RamResponse.ts
Normal file
7
client/src/app/_dto/response/RamResponse.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { WebSocketResponse } from "./WebSocketResponse";
|
||||
|
||||
export interface RamResponse extends WebSocketResponse{
|
||||
free: number;
|
||||
used: number;
|
||||
max: number;
|
||||
}
|
5
client/src/app/_dto/response/TpsResponse.ts
Normal file
5
client/src/app/_dto/response/TpsResponse.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { WebSocketResponse } from "./WebSocketResponse";
|
||||
|
||||
export interface TpsResponse extends WebSocketResponse{
|
||||
tps: number;
|
||||
}
|
5
client/src/app/_dto/response/UnknownCommandResponse.ts
Normal file
5
client/src/app/_dto/response/UnknownCommandResponse.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { WebSocketResponse } from "./WebSocketResponse";
|
||||
|
||||
export interface UnknownCommandResponse extends WebSocketResponse{
|
||||
respondsTo: string;
|
||||
}
|
5
client/src/app/_dto/response/WebSocketResponse.ts
Normal file
5
client/src/app/_dto/response/WebSocketResponse.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface WebSocketResponse {
|
||||
status: number;
|
||||
statusDescription: string;
|
||||
message: string;
|
||||
}
|
23
client/src/app/_services/language.service.ts
Normal file
23
client/src/app/_services/language.service.ts
Normal 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();
|
||||
}
|
||||
}
|
245
client/src/app/_services/storage.service.ts
Normal file
245
client/src/app/_services/storage.service.ts
Normal 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
|
||||
}
|
204
client/src/app/_services/webconsole.service.ts
Normal file
204
client/src/app/_services/webconsole.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
30
client/src/app/app-routing.module.ts
Normal file
30
client/src/app/app-routing.module.ts
Normal 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 { }
|
1
client/src/app/app.component.html
Normal file
1
client/src/app/app.component.html
Normal file
@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
0
client/src/app/app.component.scss
Normal file
0
client/src/app/app.component.scss
Normal file
35
client/src/app/app.component.spec.ts
Normal file
35
client/src/app/app.component.spec.ts
Normal 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!');
|
||||
});
|
||||
});
|
10
client/src/app/app.component.ts
Normal file
10
client/src/app/app.component.ts
Normal 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';
|
||||
}
|
26
client/src/app/app.module.ts
Normal file
26
client/src/app/app.module.ts
Normal 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 { }
|
47
client/src/app/content/add-server/add-server.component.html
Normal file
47
client/src/app/content/add-server/add-server.component.html
Normal 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>
|
@ -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();
|
||||
});
|
||||
});
|
72
client/src/app/content/add-server/add-server.component.ts
Normal file
72
client/src/app/content/add-server/add-server.component.ts
Normal 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, "<").replace(/>/g, ">").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');
|
||||
}
|
||||
|
||||
}
|
130
client/src/app/content/console/console.component.html
Normal file
130
client/src/app/content/console/console.component.html
Normal 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>
|
3
client/src/app/content/console/console.component.scss
Normal file
3
client/src/app/content/console/console.component.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.console {
|
||||
height: 480px;
|
||||
}
|
25
client/src/app/content/console/console.component.spec.ts
Normal file
25
client/src/app/content/console/console.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
396
client/src/app/content/console/console.component.ts
Normal file
396
client/src/app/content/console/console.component.ts
Normal 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 < (to avoid XSS) and replacing new line to br.
|
||||
msg = msg.replace(/</g, "<");
|
||||
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
|
18
client/src/app/content/content-routing.module.ts
Normal file
18
client/src/app/content/content-routing.module.ts
Normal 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 { }
|
25
client/src/app/content/content.module.ts
Normal file
25
client/src/app/content/content.module.ts
Normal 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 { }
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
92
client/src/app/content/edit-server/edit-server.component.ts
Normal file
92
client/src/app/content/edit-server/edit-server.component.ts
Normal 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, "<").replace(/>/g, ">").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');
|
||||
}
|
||||
|
||||
}
|
109
client/src/app/content/index/index.component.html
Normal file
109
client/src/app/content/index/index.component.html
Normal 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>
|
4
client/src/app/content/index/index.component.scss
Normal file
4
client/src/app/content/index/index.component.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.blurry-text {
|
||||
text-shadow: 0 0 8px black;
|
||||
color: transparent !important;
|
||||
}
|
25
client/src/app/content/index/index.component.spec.ts
Normal file
25
client/src/app/content/index/index.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
91
client/src/app/content/index/index.component.ts
Normal file
91
client/src/app/content/index/index.component.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
81
client/src/app/content/settings/settings.component.html
Normal file
81
client/src/app/content/settings/settings.component.html
Normal 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>
|
25
client/src/app/content/settings/settings.component.spec.ts
Normal file
25
client/src/app/content/settings/settings.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
79
client/src/app/content/settings/settings.component.ts
Normal file
79
client/src/app/content/settings/settings.component.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
19
client/src/app/core/core.module.ts
Normal file
19
client/src/app/core/core.module.ts
Normal 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 { }
|
45
client/src/app/core/language.module.ts
Normal file
45
client/src/app/core/language.module.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
54
client/src/app/core/layout/layout.component.html
Normal file
54
client/src/app/core/layout/layout.component.html
Normal 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>
|
7
client/src/app/core/layout/layout.component.scss
Normal file
7
client/src/app/core/layout/layout.component.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.navbar-toggler-icon {
|
||||
transition: transform 0.3s linear;
|
||||
}
|
||||
|
||||
.rotated {
|
||||
transform: rotate(-90deg);
|
||||
}
|
25
client/src/app/core/layout/layout.component.spec.ts
Normal file
25
client/src/app/core/layout/layout.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
47
client/src/app/core/layout/layout.component.ts
Normal file
47
client/src/app/core/layout/layout.component.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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" });
|
||||
}
|
||||
|
||||
}
|
18
client/src/app/shared/icons.ts
Normal file
18
client/src/app/shared/icons.ts
Normal 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,
|
||||
}
|
15
client/src/app/shared/sanitize.pipe.ts
Normal file
15
client/src/app/shared/sanitize.pipe.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
29
client/src/app/shared/shared.module.ts
Normal file
29
client/src/app/shared/shared.module.ts
Normal 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 { }
|
0
client/src/assets/.gitkeep
Normal file
0
client/src/assets/.gitkeep
Normal file
5
client/src/assets/i18n/cs.json
Normal file
5
client/src/assets/i18n/cs.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/de.json
Normal file
5
client/src/assets/i18n/de.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
86
client/src/assets/i18n/en.json
Normal file
86
client/src/assets/i18n/en.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"GENERAL": {
|
||||
"Server": "Server",
|
||||
"Loading": "Loading..."
|
||||
},
|
||||
"LAYOUT": {
|
||||
"Home": "Home",
|
||||
"Settings": "Settings",
|
||||
"Navigation": "Navigation",
|
||||
"CurrentlyConnectedTo": "Currently connected servers",
|
||||
"NoConnectedToServers": "You are currently not connected to any server.",
|
||||
"Disconnect": "Disconnect"
|
||||
},
|
||||
"HOME": {
|
||||
"YourServers": "Your saved servers",
|
||||
"NoServersAdded": "You have no servers added yet. Add your first one using the button on the top right of this page. Also, check out the settings page to finish customizing WebConsole.",
|
||||
"ServerUri": "Server URI",
|
||||
"Actions": "Actions",
|
||||
"Connect": "Connect",
|
||||
"MoveUp": "Move up",
|
||||
"MoveDown": "Move down",
|
||||
"Edit": "Edit",
|
||||
"Delete": "Delete",
|
||||
"ServerDetails": "Server details"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"WebConsoleClientSettings": "WebConsole Client Settings",
|
||||
"GeneralSettings": "General Settings",
|
||||
"ShowTimeOnConsoleLine": "Show time on each console line",
|
||||
"RetrieveFullLogOnConnect": "Fetch full log file after connecting. May affect performance for a few seconds or reach maximum browser memory when connecting to busy servers.",
|
||||
"BlurryUriHomepage": "Blurry Server URI on homepage (Useful when using WebConsole in public spaces)",
|
||||
"WiderViewport": "Wider viewport",
|
||||
"MigrateData": "Migrate data",
|
||||
"MigrateDataDescription": "Here you can export your saved servers and settings and import them in another WebConsole Client.",
|
||||
"ExportData": "Export data",
|
||||
"ImportData": "Import data",
|
||||
"CopyString": "Copy the following string and paste it in your desired client:",
|
||||
"PasteString": "Paste import string...",
|
||||
"Import": "Import",
|
||||
"ImportSuccessful": "Imported successfully!",
|
||||
"ImportFailed": "Error occured importing. Check your exported string and try again.",
|
||||
"Language": "Language",
|
||||
"SelectLanguage": "Select your preferred language to use with WebConsole"
|
||||
},
|
||||
"ADDEDITSERVER": {
|
||||
"AddNewServer": "Add new server",
|
||||
"EditServer": "Edit server",
|
||||
"Name": "Server name",
|
||||
"NamePlaceholder": "My server",
|
||||
"NameNotEditable": "Name not editable. To modify it, delete this server and create it again.",
|
||||
"Ip": "IP or Domain",
|
||||
"IpPlaceholder": "192.168.0.1 or mc.example.com",
|
||||
"Port": "Port",
|
||||
"Password": "Password (Optional)",
|
||||
"PasswordPlaceholder": "Leave blank to ask for password when connecting.",
|
||||
"KeepPasswordUnchanged": "Keep password unchanged",
|
||||
"SslEnabled": "SSL is enabled on the plugin config",
|
||||
"SslEnabledMandatory": "SSL is mandatory when using client over HTTPS connections due to browsers restrictions.",
|
||||
"Add": "Add server",
|
||||
"RequiredField": "This field is required",
|
||||
"RequiredOrTooLongField": "This field is empty or exceeds 50 characters",
|
||||
"InvalidPort": "Invalid port",
|
||||
"ServerAlreadyExist": "A server with this name already exists"
|
||||
},
|
||||
"CONSOLE": {
|
||||
"ToggleServerInfo": "Toggle server info",
|
||||
"Connected": "Connected",
|
||||
"Disconnected": "Disconnected",
|
||||
"LoggedInAs": "Logged in as",
|
||||
"PlayersOnline": "Players online",
|
||||
"CpuUsage": "CPU Usage",
|
||||
"RamUsage": "RAM Usage",
|
||||
"Tps": "TPS",
|
||||
"ClickToLogin": "Login required. Click to login.",
|
||||
"Send": "Send",
|
||||
"Connecting": "Connecting, please wait...",
|
||||
"CannotConnect": "Cannot connect to server.",
|
||||
"CannotConnectDescription1": "Please make sure that server is running, and that WebConsole port is open in both your firewall and router. You can use this",
|
||||
"Tool": "tool",
|
||||
"CannotConnectDescription2": "to verify your port status.",
|
||||
"PasswordRequested": "Password Requested",
|
||||
"WrongPassword": "Wrong password. Try again.",
|
||||
"RememberPassword": "Remember password",
|
||||
"Connect": "Connect"
|
||||
}
|
||||
}
|
86
client/src/assets/i18n/es.json
Normal file
86
client/src/assets/i18n/es.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"GENERAL": {
|
||||
"Server": "Servidor",
|
||||
"Loading": "Cargando..."
|
||||
},
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio",
|
||||
"Settings": "Configuración",
|
||||
"Navigation": "Navegación",
|
||||
"CurrentlyConnectedTo": "Conexiones activas",
|
||||
"NoConnectedToServers": "Actualmente no estas conectado a ningún servidor.",
|
||||
"Disconnect": "Desconectar"
|
||||
},
|
||||
"HOME": {
|
||||
"YourServers": "Tus servidores guardados",
|
||||
"NoServersAdded": "No has añadido ningún servidor aún. Comienza añadiendo uno utilizando el botón situado en la parte superior derecha de esta oagina. Ademas, puedes terminar de personalizar WebConsole utilizando la sección de configuración que encontrarás en el menú principal.",
|
||||
"ServerUri": "URI del servidor",
|
||||
"Actions": "Acciones",
|
||||
"Connect": "Conectar",
|
||||
"MoveUp": "Subir",
|
||||
"MoveDown": "Bajar",
|
||||
"Edit": "Editar",
|
||||
"Delete": "Borrar",
|
||||
"ServerDetails": "Detalles del servidor"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"WebConsoleClientSettings": "Configuración del cliente WebConsole",
|
||||
"GeneralSettings": "Configuración general",
|
||||
"ShowTimeOnConsoleLine": "Mostrar tiempo en cada linea de consola",
|
||||
"RetrieveFullLogOnConnect": "Obtener log completo tras conectar al servidor. Puede afectar al rendimiento durante unos segundos o sobrepasar la memoria en servidores muy grandes.",
|
||||
"BlurryUriHomepage": "Obfuscar URI de servidor en página de inicio (Útil si usas WebConsole en espacios públicos)",
|
||||
"WiderViewport": "Ventana sin márgenes laterales",
|
||||
"MigrateData": "Migrar datos",
|
||||
"MigrateDataDescription": "A continuación puedes exportar tus servidores guardados e importarlos en otro cliente de WebConsole",
|
||||
"ExportData": "Exportar datos",
|
||||
"ImportData": "Importar datos",
|
||||
"CopyString": "Copia la siguiente cadena y pégala en el cliente WebConsole donde desees importar los datos:",
|
||||
"PasteString": "Pega tu cadena aquí...",
|
||||
"Import": "Importar",
|
||||
"ImportSuccessful": "Datos importados correctamente!",
|
||||
"ImportFailed": "Ocrrió un error al importar. Comprueba que la cadena exportada anteriormente sea correcta e inténtalo de nuevo.",
|
||||
"Language": "Idioma",
|
||||
"SelectLanguage": "Selecciona el idioma en el que mostrar el Cliente de WebConsole"
|
||||
},
|
||||
"ADDEDITSERVER": {
|
||||
"AddNewServer": "Añadir nuevo servidor",
|
||||
"EditServer": "Editar servidor",
|
||||
"Name": "Nombre del servidor",
|
||||
"NamePlaceholder": "Mi server",
|
||||
"NameNotEditable": "Nombre no editable. Si deseas modificarlo, debes eliminar el servidor y crearlo de nuevo.",
|
||||
"Ip": "IP o Dominio",
|
||||
"IpPlaceholder": "192.168.0.1 o mc.example.com",
|
||||
"Port": "Puerto",
|
||||
"Password": "Contraseña (Opcional)",
|
||||
"PasswordPlaceholder": "Deja este campo en blanco para introducir la contraseña al conectar.",
|
||||
"KeepPasswordUnchanged": "No modificar contraseña",
|
||||
"SslEnabled": "SSL activado en la configuración del plugin",
|
||||
"SslEnabledMandatory": "SSL es obligatorio cuando se utiliza el cliente bajo HTTPS debido a restricciones de los navegadores.",
|
||||
"Add": "Añadir servidor",
|
||||
"RequiredField": "Este campo es obligatorio",
|
||||
"RequiredOrTooLongField": "Este campo está vacio o sobrepasa los 50 caracteres",
|
||||
"InvalidPort": "Puerto inválido",
|
||||
"ServerAlreadyExist": "Ya existe un servidor con el mismo nombre"
|
||||
},
|
||||
"CONSOLE": {
|
||||
"ToggleServerInfo": "Mostrar/Ocultar información del servidor",
|
||||
"Connected": "Conectado",
|
||||
"Disconnected": "Desconectado",
|
||||
"LoggedInAs": "Sesión iniciada como",
|
||||
"PlayersOnline": "Jugadores conectados",
|
||||
"CpuUsage": "Uso de CPU",
|
||||
"RamUsage": "Uso de RAM",
|
||||
"Tps": "TPS",
|
||||
"ClickToLogin": "Inicio de sesión requerido. Click para iniciar sesión.",
|
||||
"Send": "Enviar",
|
||||
"Connecting": "Conectando, espere...",
|
||||
"CannotConnect": "No se puede conectar con el servidor.",
|
||||
"CannotConnectDescription1": "Asegúrate que el servidor está en ejecución, y que el puerto de WebConsole está abierto en tu firewall y/o router. Puedes usar esta",
|
||||
"Tool": "herramienta",
|
||||
"CannotConnectDescription2": "para comprobar si tu puerto está abierto.",
|
||||
"PasswordRequested": "Contraseña necesaria",
|
||||
"WrongPassword": "Contraseña incorrecta. Inténtelo de nuevo.",
|
||||
"RememberPassword": "Recordar contraseña",
|
||||
"Connect": "Conectar"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/fr.json
Normal file
5
client/src/assets/i18n/fr.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/it.json
Normal file
5
client/src/assets/i18n/it.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/ja.json
Normal file
5
client/src/assets/i18n/ja.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/ko.json
Normal file
5
client/src/assets/i18n/ko.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/nl.json
Normal file
5
client/src/assets/i18n/nl.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/pl.json
Normal file
5
client/src/assets/i18n/pl.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/pt.json
Normal file
5
client/src/assets/i18n/pt.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/ru.json
Normal file
5
client/src/assets/i18n/ru.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/tr.json
Normal file
5
client/src/assets/i18n/tr.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
5
client/src/assets/i18n/zh.json
Normal file
5
client/src/assets/i18n/zh.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"LAYOUT": {
|
||||
"Home": "Inicio"
|
||||
}
|
||||
}
|
BIN
client/src/assets/icon.png
Normal file
BIN
client/src/assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 885 B |
BIN
client/src/assets/iconwhite.png
Normal file
BIN
client/src/assets/iconwhite.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 898 B |
3
client/src/environments/environment.prod.ts
Normal file
3
client/src/environments/environment.prod.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
16
client/src/environments/environment.ts
Normal file
16
client/src/environments/environment.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
BIN
client/src/favicon.ico
Normal file
BIN
client/src/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
13
client/src/index.html
Normal file
13
client/src/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WebConsole Client</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
12
client/src/main.ts
Normal file
12
client/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
57
client/src/polyfills.ts
Normal file
57
client/src/polyfills.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/***************************************************************************************************
|
||||
* Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
|
||||
*/
|
||||
import '@angular/localize/init';
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
||||
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
11
client/src/styles.scss
Normal file
11
client/src/styles.scss
Normal file
@ -0,0 +1,11 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
/* Importing Bootstrap SCSS file. */
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
|
||||
//Make placeholders less visible
|
||||
.form-control::-webkit-input-placeholder { opacity: 0.3; } /* WebKit, Blink, Edge */
|
||||
.form-control:-moz-placeholder { opacity: 0.3; } /* Mozilla Firefox 4 to 18 */
|
||||
.form-control::-moz-placeholder { opacity: 0.3; } /* Mozilla Firefox 19+ */
|
||||
.form-control:-ms-input-placeholder { opacity: 0.3; } /* Internet Explorer 10-11 */
|
||||
.form-control::-ms-input-placeholder { opacity: 0.3; } /* Microsoft Edge */
|
26
client/src/test.ts
Normal file
26
client/src/test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: {
|
||||
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||
<T>(id: string): T;
|
||||
keys(): string[];
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting(),
|
||||
);
|
||||
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
Reference in New Issue
Block a user