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 { }
|
Reference in New Issue
Block a user