Revert codestyle changes (#40)

* Revert "7f7efbb94bab8574e42d942ad82d414e007b2970"

Code style changes should probably be part of a PR
This commit is contained in:
Eliot Partridge
2019-08-30 10:53:14 -05:00
committed by GitHub
parent eefd9e9a2c
commit caf70a864f
446 changed files with 70667 additions and 70655 deletions
File diff suppressed because it is too large Load Diff
@@ -12,112 +12,112 @@ let currentPage = 0;
let not = false;
@Component({
selector: 'admin-accounts',
templateUrl: 'admin-accounts.pug',
selector: 'admin-accounts',
templateUrl: 'admin-accounts.pug',
})
export class AdminAccounts implements OnInit {
readonly syncIcon = faSync;
readonly filterIcon = faFilter;
readonly commentIcon = faComment;
readonly eraserIcon = faEraser;
readonly spinnerIcon = faSpinner;
readonly filters = [
'all',
'banned',
'timed out',
'with flags',
'notes',
'supporters',
];
totalItems = 99999999;
itemsOnPageIds: string[] = [];
itemsPerPage = 20;
loading = false;
private expanded = new Set<string>();
constructor(public model: AdminModel, private router: Router) {
}
get showOnly() {
return showOnly;
}
set showOnly(value) {
if (showOnly !== value) {
showOnly = value;
this.refresh();
}
}
get not() {
return not;
}
set not(value) {
if (not !== value) {
not = value;
this.refresh();
}
}
get autoRefresh() {
return autoRefresh;
}
set autoRefresh(value) {
autoRefresh = value;
}
get currentPage() {
return currentPage;
}
set currentPage(value) {
if (currentPage !== value) {
currentPage = value;
this.refresh();
}
}
get search() {
return search;
}
set search(value) {
if (search !== value) {
search = value;
this.execSearch();
}
}
private execSearch = debounce(() => this.refresh(), 500);
get duplicateEntries() {
return this.model.duplicateEntries;
}
refreshDuplicates() {
this.model.checkDuplicateEntries(true);
}
limit(account: Account) {
return this.expanded.has(account._id) ? 99999 : 2;
}
expand(account: Account) {
this.expanded.add(account._id);
}
ngOnInit() {
this.model.accountPromise
.then(() => this.refresh());
}
refresh(force = false) {
this.loading = true;
this.model.findAccounts({
search: this.search.trim(),
not: this.not,
showOnly: this.showOnly,
page: this.currentPage - 1,
itemsPerPage: this.itemsPerPage,
force,
}).then(result => {
if (result && result.page === (this.currentPage - 1)) {
this.totalItems = result.totalItems;
this.itemsOnPageIds = result.accounts;
this.loading = false;
}
});
}
createAccount() {
const name = prompt('enter new account name');
readonly syncIcon = faSync;
readonly filterIcon = faFilter;
readonly commentIcon = faComment;
readonly eraserIcon = faEraser;
readonly spinnerIcon = faSpinner;
readonly filters = [
'all',
'banned',
'timed out',
'with flags',
'notes',
'supporters',
];
totalItems = 99999999;
itemsOnPageIds: string[] = [];
itemsPerPage = 20;
loading = false;
private expanded = new Set<string>();
constructor(public model: AdminModel, private router: Router) {
}
get showOnly() {
return showOnly;
}
set showOnly(value) {
if (showOnly !== value) {
showOnly = value;
this.refresh();
}
}
get not() {
return not;
}
set not(value) {
if (not !== value) {
not = value;
this.refresh();
}
}
get autoRefresh() {
return autoRefresh;
}
set autoRefresh(value) {
autoRefresh = value;
}
get currentPage() {
return currentPage;
}
set currentPage(value) {
if (currentPage !== value) {
currentPage = value;
this.refresh();
}
}
get search() {
return search;
}
set search(value) {
if (search !== value) {
search = value;
this.execSearch();
}
}
private execSearch = debounce(() => this.refresh(), 500);
get duplicateEntries() {
return this.model.duplicateEntries;
}
refreshDuplicates() {
this.model.checkDuplicateEntries(true);
}
limit(account: Account) {
return this.expanded.has(account._id) ? 99999 : 2;
}
expand(account: Account) {
this.expanded.add(account._id);
}
ngOnInit() {
this.model.accountPromise
.then(() => this.refresh());
}
refresh(force = false) {
this.loading = true;
this.model.findAccounts({
search: this.search.trim(),
not: this.not,
showOnly: this.showOnly,
page: this.currentPage - 1,
itemsPerPage: this.itemsPerPage,
force,
}).then(result => {
if (result && result.page === (this.currentPage - 1)) {
this.totalItems = result.totalItems;
this.itemsOnPageIds = result.accounts;
this.loading = false;
}
});
}
createAccount() {
const name = prompt('enter new account name');
if (name) {
this.model.createAccount(name)
.then(id => this.router.navigate(['accounts', id]));
}
}
if (name) {
this.model.createAccount(name)
.then(id => this.router.navigate(['accounts', id]));
}
}
}
@@ -5,95 +5,95 @@ import { AdminModel } from '../../services/adminModel';
import { BaseTable, BaseTableState } from '../base-table';
import { AdminChatLog } from '../shared/admin-chat-log/admin-chat-log';
import {
faBell, faSync, faClock, faTrash, faComments, faHdd, faMicrochip, faCertificate, faClone, faPatreon
faBell, faSync, faClock, faTrash, faComments, faHdd, faMicrochip, faCertificate, faClone, faPatreon
} from '../../../client/icons';
let state: BaseTableState;
@Component({
selector: 'admin-events',
templateUrl: 'admin-events.pug',
selector: 'admin-events',
templateUrl: 'admin-events.pug',
})
export class AdminEvents extends BaseTable<Event> implements OnInit, OnDestroy {
readonly bellIcon = faBell;
readonly syncIcon = faSync;
readonly clockIcon = faClock;
readonly trashIcon = faTrash;
readonly commentsIcon = faComments;
readonly hddIcon = faHdd;
readonly ramIcon = faMicrochip;
readonly certificateIcon = faCertificate;
readonly duplicateIcon = faClone;
readonly patreonIcon = faPatreon;
@ViewChild('chatLog', { static: true }) chatLog!: AdminChatLog;
private chatEvent?: Event;
constructor(private model: AdminModel) {
super();
}
get status() {
return this.model.state.status;
}
get isLowDiskSpace() {
return this.model.isLowDiskSpace;
}
get isLowMemory() {
return this.model.isLowMemory;
}
get isOldCertificate() {
return this.model.isOldCertificate;
}
get isOldPatreon() {
return this.model.isOldPatreon;
}
get items() {
return this.model.events;
}
get duplicateEntries() {
return this.model.duplicateEntries;
}
get notifications() {
return this.model.notifications;
}
ngOnInit() {
this.model.updated = () => this.updateItems();
this.setState(state);
this.updateItems();
}
ngOnDestroy() {
this.model.updated = () => { };
}
cleanupDeleted() {
this.model.cleanupDeletedEvents();
}
refreshDuplicates() {
this.model.checkDuplicateEntries(true);
}
removeEvents(olderThan: number) {
const date = fromNow(-olderThan);
const oldEvents = this.items.filter(e => e.updatedAt.getTime() < date.getTime());
return Promise.all(oldEvents.map(e => this.model.removeEvent(e._id).then(() => this.removedEvent(e))));
}
showChat(e?: ChatEvent) {
this.chatEvent = e && e.event;
this.chatLog.show(e && e.account);
}
addChat(e: ChatEvent) {
if (e.account) {
this.chatLog.add(e.account);
}
}
removedEvent(e: Event) {
if (this.chatEvent === e) {
this.chatLog.close();
}
}
toggleNotifications() {
this.model.toggleNotifications();
}
protected onChange() {
state = this.getState();
}
protected updatePage() {
super.updatePage();
}
readonly bellIcon = faBell;
readonly syncIcon = faSync;
readonly clockIcon = faClock;
readonly trashIcon = faTrash;
readonly commentsIcon = faComments;
readonly hddIcon = faHdd;
readonly ramIcon = faMicrochip;
readonly certificateIcon = faCertificate;
readonly duplicateIcon = faClone;
readonly patreonIcon = faPatreon;
@ViewChild('chatLog', { static: true }) chatLog!: AdminChatLog;
private chatEvent?: Event;
constructor(private model: AdminModel) {
super();
}
get status() {
return this.model.state.status;
}
get isLowDiskSpace() {
return this.model.isLowDiskSpace;
}
get isLowMemory() {
return this.model.isLowMemory;
}
get isOldCertificate() {
return this.model.isOldCertificate;
}
get isOldPatreon() {
return this.model.isOldPatreon;
}
get items() {
return this.model.events;
}
get duplicateEntries() {
return this.model.duplicateEntries;
}
get notifications() {
return this.model.notifications;
}
ngOnInit() {
this.model.updated = () => this.updateItems();
this.setState(state);
this.updateItems();
}
ngOnDestroy() {
this.model.updated = () => { };
}
cleanupDeleted() {
this.model.cleanupDeletedEvents();
}
refreshDuplicates() {
this.model.checkDuplicateEntries(true);
}
removeEvents(olderThan: number) {
const date = fromNow(-olderThan);
const oldEvents = this.items.filter(e => e.updatedAt.getTime() < date.getTime());
return Promise.all(oldEvents.map(e => this.model.removeEvent(e._id).then(() => this.removedEvent(e))));
}
showChat(e?: ChatEvent) {
this.chatEvent = e && e.event;
this.chatLog.show(e && e.account);
}
addChat(e: ChatEvent) {
if (e.account) {
this.chatLog.add(e.account);
}
}
removedEvent(e: Event) {
if (this.chatEvent === e) {
this.chatLog.close();
}
}
toggleNotifications() {
this.model.toggleNotifications();
}
protected onChange() {
state = this.getState();
}
protected updatePage() {
super.updatePage();
}
}
@@ -4,33 +4,33 @@ import { Event } from '../../../common/adminInterfaces';
import { AdminModel } from '../../services/adminModel';
@Component({
selector: 'admin-origin-details',
templateUrl: 'admin-origin-details.pug',
selector: 'admin-origin-details',
templateUrl: 'admin-origin-details.pug',
})
export class AdminOriginDetails implements OnInit {
events?: Event[];
accounts: string[] = [];
ip?: string;
constructor(private route: ActivatedRoute, private model: AdminModel) {
}
get whoisHref() {
return `http://whois.urih.com/record/${this.ip}/`;
}
ngOnInit() {
this.route.params.forEach(p => {
this.ip = p['ip'];
this.update();
});
}
private update() {
this.accounts = [];
events?: Event[];
accounts: string[] = [];
ip?: string;
constructor(private route: ActivatedRoute, private model: AdminModel) {
}
get whoisHref() {
return `http://whois.urih.com/record/${this.ip}/`;
}
ngOnInit() {
this.route.params.forEach(p => {
this.ip = p['ip'];
this.update();
});
}
private update() {
this.accounts = [];
if (this.model.connected && this.ip) {
this.model.getAccountsByOrigin(this.ip)
.then(accounts => this.accounts = accounts || []);
this.events = this.model.events
.filter(e => e.origin && e.origin.ip === this.ip)
.slice(0, 20);
}
}
if (this.model.connected && this.ip) {
this.model.getAccountsByOrigin(this.ip)
.then(accounts => this.accounts = accounts || []);
this.events = this.model.events
.filter(e => e.origin && e.origin.ip === this.ip)
.slice(0, 20);
}
}
}
@@ -4,50 +4,50 @@ import { faSync, faEraser, faClock, faUser, faChevronDown, faSpinner } from '../
import { AdminModel } from '../../services/adminModel';
@Component({
selector: 'admin-origins',
templateUrl: 'admin-origins.pug',
selector: 'admin-origins',
templateUrl: 'admin-origins.pug',
})
export class AdminOrigins implements OnInit {
readonly syncIcon = faSync;
readonly eraserIcon = faEraser;
readonly clockIcon = faClock;
readonly userIcon = faUser;
readonly chevronDownIcon = faChevronDown;
readonly spinnerIcon = faSpinner;
stats?: OriginStats;
other?: OtherStats;
requestStats: RequestStats[] = [];
pending = false;
constructor(public model: AdminModel) {
}
ngOnInit() {
if (this.model.connected) {
this.update();
}
}
update() {
this.model.getOriginStats().then(stats => this.stats = stats);
this.model.getOtherStats().then(stats => this.other = stats);
}
clear(count: number, andHigher = false) {
this.clearAll(count, andHigher, true, true, false);
}
clearOld(count: number) {
this.clearAll(count, true, true, false, false);
}
clearSingles(count: number) {
this.clearAll(count, true, false, true, false);
}
clearTo10(count: number) {
this.clearAll(count, true, false, true, true);
}
clearAll(count: number, andHigher: boolean, old: boolean, singles: boolean, trim: boolean) {
this.pending = true;
readonly syncIcon = faSync;
readonly eraserIcon = faEraser;
readonly clockIcon = faClock;
readonly userIcon = faUser;
readonly chevronDownIcon = faChevronDown;
readonly spinnerIcon = faSpinner;
stats?: OriginStats;
other?: OtherStats;
requestStats: RequestStats[] = [];
pending = false;
constructor(public model: AdminModel) {
}
ngOnInit() {
if (this.model.connected) {
this.update();
}
}
update() {
this.model.getOriginStats().then(stats => this.stats = stats);
this.model.getOtherStats().then(stats => this.other = stats);
}
clear(count: number, andHigher = false) {
this.clearAll(count, andHigher, true, true, false);
}
clearOld(count: number) {
this.clearAll(count, true, true, false, false);
}
clearSingles(count: number) {
this.clearAll(count, true, false, true, false);
}
clearTo10(count: number) {
this.clearAll(count, true, false, true, true);
}
clearAll(count: number, andHigher: boolean, old: boolean, singles: boolean, trim: boolean) {
this.pending = true;
return this.model.clearOrigins(count, andHigher, { old, singles, trim })
.finally(() => {
this.pending = false;
this.update();
});
}
return this.model.clearOrigins(count, andHigher, { old, singles, trim })
.finally(() => {
this.pending = false;
this.update();
});
}
}
@@ -6,85 +6,85 @@ import { Subscription } from '../../../common/interfaces';
import { showTextInNewTab } from '../../../client/htmlUtils';
interface Field {
key: keyof GeneralSettings;
title: string;
value: string | undefined;
key: keyof GeneralSettings;
title: string;
value: string | undefined;
}
@Component({
selector: 'admin-other',
templateUrl: 'admin-other.pug',
selector: 'admin-other',
templateUrl: 'admin-other.pug',
})
export class AdminOther implements OnInit, OnDestroy {
fields: Field[] = [
{ key: 'suspiciousNames', title: 'Suspicious pony names & emails to report', value: undefined },
{ key: 'suspiciousAuths', title: 'Suspicious auths to report', value: undefined },
{ key: 'suspiciousMessages', title: 'Suspicious messages to report (instant)', value: undefined },
{ key: 'suspiciousSafeMessages', title: 'Suspicious messages to report (safe only) (5+)', value: undefined },
{ key: 'suspiciousSafeWholeMessages', title: 'Suspicious messages to report (safe only) (5+) (whole words)', value: undefined },
{ key: 'suspiciousSafeInstantMessages', title: 'Suspicious messages to report (safe only) (instant)', value: undefined },
{
key: 'suspiciousSafeInstantWholeMessages',
title: 'Suspicious messages to report (safe only) (whole words) (instant)',
value: undefined
},
];
max = 100;
value = 0;
error?: string;
succeeded = false;
suspiciousPonies?: string;
suspiciousPoniesError?: string;
ignoreErrors?: string;
account: Account | undefined;
private subscription?: Subscription;
constructor(public model: AdminModel) {
}
ngOnInit() {
this.model.accountPromise.then(() => {
this.resetFields();
this.resetSuspiciousPonies();
});
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
saveFields() {
let settings: any = {};
this.fields.forEach(field => settings[field.key] = field.value);
this.model.updateSettings(settings);
}
resetFields() {
this.fields.map(field => {
field.value = this.model.state.loginServers[0][field.key] as string | undefined;
});
}
saveSuspiciousPonies() {
this.suspiciousPoniesError = undefined;
fields: Field[] = [
{ key: 'suspiciousNames', title: 'Suspicious pony names & emails to report', value: undefined },
{ key: 'suspiciousAuths', title: 'Suspicious auths to report', value: undefined },
{ key: 'suspiciousMessages', title: 'Suspicious messages to report (instant)', value: undefined },
{ key: 'suspiciousSafeMessages', title: 'Suspicious messages to report (safe only) (5+)', value: undefined },
{ key: 'suspiciousSafeWholeMessages', title: 'Suspicious messages to report (safe only) (5+) (whole words)', value: undefined },
{ key: 'suspiciousSafeInstantMessages', title: 'Suspicious messages to report (safe only) (instant)', value: undefined },
{
key: 'suspiciousSafeInstantWholeMessages',
title: 'Suspicious messages to report (safe only) (whole words) (instant)',
value: undefined
},
];
max = 100;
value = 0;
error?: string;
succeeded = false;
suspiciousPonies?: string;
suspiciousPoniesError?: string;
ignoreErrors?: string;
account: Account | undefined;
private subscription?: Subscription;
constructor(public model: AdminModel) {
}
ngOnInit() {
this.model.accountPromise.then(() => {
this.resetFields();
this.resetSuspiciousPonies();
});
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
saveFields() {
let settings: any = {};
this.fields.forEach(field => settings[field.key] = field.value);
this.model.updateSettings(settings);
}
resetFields() {
this.fields.map(field => {
field.value = this.model.state.loginServers[0][field.key] as string | undefined;
});
}
saveSuspiciousPonies() {
this.suspiciousPoniesError = undefined;
try {
compact(this.suspiciousPonies!.split(/\n/g).map(x => x.trim())).map(x => JSON.parse(x));
this.model.updateSettings({ suspiciousPonies: this.suspiciousPonies });
} catch (e) {
this.suspiciousPoniesError = e.message;
}
}
resetSuspiciousPonies() {
this.suspiciousPonies = this.model.state.loginServers[0].suspiciousPonies;
}
updatePatreon() {
this.model.server.updatePatreon();
}
updatePatreonToken(patreonToken: string) {
this.model.updateSettings({ patreonToken });
}
getLastPatreonData() {
this.model.getLastPatreonData()
.then(data => {
showTextInNewTab(JSON.stringify(data, null, 2));
});
}
updatePastSupporters() {
this.model.updatePastSupporters();
}
try {
compact(this.suspiciousPonies!.split(/\n/g).map(x => x.trim())).map(x => JSON.parse(x));
this.model.updateSettings({ suspiciousPonies: this.suspiciousPonies });
} catch (e) {
this.suspiciousPoniesError = e.message;
}
}
resetSuspiciousPonies() {
this.suspiciousPonies = this.model.state.loginServers[0].suspiciousPonies;
}
updatePatreon() {
this.model.server.updatePatreon();
}
updatePatreonToken(patreonToken: string) {
this.model.updateSettings({ patreonToken });
}
getLastPatreonData() {
this.model.getLastPatreonData()
.then(data => {
showTextInNewTab(JSON.stringify(data, null, 2));
});
}
updatePastSupporters() {
this.model.updatePastSupporters();
}
}
@@ -9,89 +9,89 @@ let currentPage = 1;
let query: FindPonyQuery = {};
@Component({
selector: 'admin-ponies',
templateUrl: 'admin-ponies.pug',
styleUrls: ['admin-ponies.scss'],
selector: 'admin-ponies',
templateUrl: 'admin-ponies.pug',
styleUrls: ['admin-ponies.scss'],
})
export class AdminPonies implements OnInit {
readonly syncIcon = faSync;
readonly filterIcon = faFilter;
readonly spinnerIcon = faSpinner;
readonly trashIcon = faTrash;
readonly commentIcon = faComment;
items?: string[];
itemsPerPage = 20;
query: FindPonyQuery = {};
loading = false;
private lastQuery: FindPonyQuery = {};
private totalCount?: number;
private execSearch = debounce(() => this.fetchPonies(false), 1000);
constructor(private model: AdminModel) {
}
get totalItems() {
return this.totalCount === undefined ? this.model.counts.characters : this.totalCount;
}
get search() {
return query.search;
}
set search(value: string | undefined) {
if (query.search !== value) {
query.search = value;
this.execSearch();
}
}
get orderBy() {
return query.orderBy;
}
set orderBy(value: string | undefined) {
if (query.orderBy !== value) {
query.orderBy = value;
this.execSearch();
}
}
get currentPage() {
return currentPage;
}
set currentPage(value) {
if (currentPage !== value) {
currentPage = value;
this.fetchPonies(isEqual(this.lastQuery, query));
}
}
ngOnInit() {
if (this.model.connected) {
this.fetchPonies(false);
}
}
refresh() {
this.fetchPonies(false);
}
remove(pony: Character) {
if (confirm('Are you sure?')) {
this.model.removePony(pony._id)
.then(() => delay(500))
.then(() => this.refresh());
}
}
private fetchPonies(skipTotalCount: boolean) {
const thisQuery = cloneDeep(query);
this.lastQuery = cloneDeep(query);
this.loading = true;
readonly syncIcon = faSync;
readonly filterIcon = faFilter;
readonly spinnerIcon = faSpinner;
readonly trashIcon = faTrash;
readonly commentIcon = faComment;
items?: string[];
itemsPerPage = 20;
query: FindPonyQuery = {};
loading = false;
private lastQuery: FindPonyQuery = {};
private totalCount?: number;
private execSearch = debounce(() => this.fetchPonies(false), 1000);
constructor(private model: AdminModel) {
}
get totalItems() {
return this.totalCount === undefined ? this.model.counts.characters : this.totalCount;
}
get search() {
return query.search;
}
set search(value: string | undefined) {
if (query.search !== value) {
query.search = value;
this.execSearch();
}
}
get orderBy() {
return query.orderBy;
}
set orderBy(value: string | undefined) {
if (query.orderBy !== value) {
query.orderBy = value;
this.execSearch();
}
}
get currentPage() {
return currentPage;
}
set currentPage(value) {
if (currentPage !== value) {
currentPage = value;
this.fetchPonies(isEqual(this.lastQuery, query));
}
}
ngOnInit() {
if (this.model.connected) {
this.fetchPonies(false);
}
}
refresh() {
this.fetchPonies(false);
}
remove(pony: Character) {
if (confirm('Are you sure?')) {
this.model.removePony(pony._id)
.then(() => delay(500))
.then(() => this.refresh());
}
}
private fetchPonies(skipTotalCount: boolean) {
const thisQuery = cloneDeep(query);
this.lastQuery = cloneDeep(query);
this.loading = true;
thisQuery.search = thisQuery.search && thisQuery.search.trim();
thisQuery.search = thisQuery.search && thisQuery.search.trim();
this.model.findPonies(thisQuery, this.currentPage - 1, skipTotalCount)
.then(result => {
if (result) {
if (!skipTotalCount) {
this.totalCount = result.totalCount;
}
this.model.findPonies(thisQuery, this.currentPage - 1, skipTotalCount)
.then(result => {
if (result) {
if (!skipTotalCount) {
this.totalCount = result.totalCount;
}
this.items = result.items;
} else {
this.items = [];
}
})
.finally(() => this.loading = false);
}
this.items = result.items;
} else {
this.items = [];
}
})
.finally(() => this.loading = false);
}
}
@@ -6,286 +6,286 @@ import { SERVER_FPS } from '../../../../common/constants';
import { AgDragEvent } from '../../../shared/directives/agDrag';
interface Tooltip {
x: number;
y: number;
w: number;
h: number;
text: string;
x: number;
y: number;
w: number;
h: number;
text: string;
}
interface ListingEntry {
name: string;
count: number;
selfTime: number;
totalTime: number;
selfPercent: number;
totalPercent: number;
name: string;
count: number;
selfTime: number;
totalTime: number;
selfPercent: number;
totalPercent: number;
}
const frameTime = 1000 / SERVER_FPS;
const timePadding = 10; // ms
@Component({
selector: 'admin-reports-perf',
templateUrl: 'admin-reports-perf.pug',
selector: 'admin-reports-perf',
templateUrl: 'admin-reports-perf.pug',
})
export class AdminReportsPerf {
@ViewChild('container', { static: true }) container!: ElementRef;
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
@ViewChild('tooltip', { static: true }) tooltip!: ElementRef;
loaded = false;
server = '';
startTime = 0;
endTime = 0;
listing: ListingEntry[] = [];
timings: TimingEntry[] = [];
private tooltips: Tooltip[] = [];
private startTimeFrom = 0;
private endTimeFrom = 0;
private frame: any = 0;
private lastZoom = 0;
constructor(private model: AdminModel) {
if (DEVELOPMENT) {
const interval = setInterval(() => {
if (findById(this.model.state.gameServers, 'dev')) {
clearInterval(interval);
this.load('dev');
}
}, 100);
}
}
get servers() {
return this.model.state.gameServers.map(s => s.id);
}
async load(server: string) {
this.server = server;
this.timings = [];
this.loaded = false;
const result = await this.model.getTimings(server);
@ViewChild('container', { static: true }) container!: ElementRef;
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
@ViewChild('tooltip', { static: true }) tooltip!: ElementRef;
loaded = false;
server = '';
startTime = 0;
endTime = 0;
listing: ListingEntry[] = [];
timings: TimingEntry[] = [];
private tooltips: Tooltip[] = [];
private startTimeFrom = 0;
private endTimeFrom = 0;
private frame: any = 0;
private lastZoom = 0;
constructor(private model: AdminModel) {
if (DEVELOPMENT) {
const interval = setInterval(() => {
if (findById(this.model.state.gameServers, 'dev')) {
clearInterval(interval);
this.load('dev');
}
}, 100);
}
}
get servers() {
return this.model.state.gameServers.map(s => s.id);
}
async load(server: string) {
this.server = server;
this.timings = [];
this.loaded = false;
const result = await this.model.getTimings(server);
if (result) {
this.loaded = true;
this.timings = result as TimingEntry[];
this.setupZoom();
this.recalcListing();
}
if (result) {
this.loaded = true;
this.timings = result as TimingEntry[];
this.setupZoom();
this.recalcListing();
}
this.redraw();
}
mouseMove(e: MouseEvent) {
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
const x = e.pageX - rect.left;
const y = e.pageY - rect.top;
this.redraw();
}
mouseMove(e: MouseEvent) {
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
const x = e.pageX - rect.left;
const y = e.pageY - rect.top;
const tooltip = this.tooltips.find(t => pointInRect(x, y, t));
const element = this.tooltip.nativeElement as HTMLElement;
const tooltip = this.tooltips.find(t => pointInRect(x, y, t));
const element = this.tooltip.nativeElement as HTMLElement;
if (tooltip) {
element.style.display = 'block';
element.style.left = `${x + 10}px`;
element.style.top = `${y + 10}px`;
element.innerText = tooltip.text;
} else {
element.style.display = 'none';
}
}
wheel(e: WheelEvent) {
e.preventDefault();
if (tooltip) {
element.style.display = 'block';
element.style.left = `${x + 10}px`;
element.style.top = `${y + 10}px`;
element.innerText = tooltip.text;
} else {
element.style.display = 'none';
}
}
wheel(e: WheelEvent) {
e.preventDefault();
const deltaY = clamp(e.deltaY, -1, 1);
const change = ((this.endTime - this.startTime) * 0.2 * deltaY);
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
const ratioFromLeft = (e.pageX - rect.left) / rect.width;
const deltaY = clamp(e.deltaY, -1, 1);
const change = ((this.endTime - this.startTime) * 0.2 * deltaY);
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
const ratioFromLeft = (e.pageX - rect.left) / rect.width;
this.startTime = this.startTime - change * ratioFromLeft;
this.endTime = this.endTime + change * (1 - ratioFromLeft);
this.startTime = this.startTime - change * ratioFromLeft;
this.endTime = this.endTime + change * (1 - ratioFromLeft);
this.redraw();
}
setupZoom() {
switch (this.lastZoom) {
case 0:
default:
this.resetZoom();
break;
case 1:
this.fitZoom();
break;
case 2:
this.fullFrameZoom();
break;
}
}
resetZoom() {
const firstTime = this.timings[0].time;
const lastTime = this.timings[this.timings.length - 1].time;
this.startTime = firstTime - timePadding;
this.endTime = lastTime + timePadding;
this.redraw();
this.lastZoom = 0;
}
fitZoom() {
const firstTime = this.timings[0].time;
const lastTime = this.timings[this.timings.length - 1].time;
const totalTime = lastTime - firstTime;
this.startTime = firstTime - totalTime * 0.05;
this.endTime = lastTime + totalTime * 0.05;
this.redraw();
this.lastZoom = 1;
}
fullFrameZoom() {
const firstTime = this.timings[0].time;
const lastTime = firstTime + frameTime;
this.startTime = firstTime - 2;
this.endTime = lastTime + 2;
this.redraw();
this.lastZoom = 2;
}
drag(e: AgDragEvent) {
if (e.type === 'start') {
this.startTimeFrom = this.startTime;
this.endTimeFrom = this.endTime;
}
this.redraw();
}
setupZoom() {
switch (this.lastZoom) {
case 0:
default:
this.resetZoom();
break;
case 1:
this.fitZoom();
break;
case 2:
this.fullFrameZoom();
break;
}
}
resetZoom() {
const firstTime = this.timings[0].time;
const lastTime = this.timings[this.timings.length - 1].time;
this.startTime = firstTime - timePadding;
this.endTime = lastTime + timePadding;
this.redraw();
this.lastZoom = 0;
}
fitZoom() {
const firstTime = this.timings[0].time;
const lastTime = this.timings[this.timings.length - 1].time;
const totalTime = lastTime - firstTime;
this.startTime = firstTime - totalTime * 0.05;
this.endTime = lastTime + totalTime * 0.05;
this.redraw();
this.lastZoom = 1;
}
fullFrameZoom() {
const firstTime = this.timings[0].time;
const lastTime = firstTime + frameTime;
this.startTime = firstTime - 2;
this.endTime = lastTime + 2;
this.redraw();
this.lastZoom = 2;
}
drag(e: AgDragEvent) {
if (e.type === 'start') {
this.startTimeFrom = this.startTime;
this.endTimeFrom = this.endTime;
}
const scale = (this.endTime - this.startTime) / this.canvas.nativeElement.width;
this.startTime = this.startTimeFrom - e.dx * scale;
this.endTime = this.endTimeFrom - e.dx * scale;
this.redraw();
}
redraw() {
this.frame = this.frame || requestAnimationFrame(() => this.draw());
}
draw() {
this.frame = 0;
const scale = (this.endTime - this.startTime) / this.canvas.nativeElement.width;
this.startTime = this.startTimeFrom - e.dx * scale;
this.endTime = this.endTimeFrom - e.dx * scale;
this.redraw();
}
redraw() {
this.frame = this.frame || requestAnimationFrame(() => this.draw());
}
draw() {
this.frame = 0;
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
canvas.width = rect.width;
canvas.height = 400;
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
canvas.width = rect.width;
canvas.height = 400;
const context = canvas.getContext('2d')!;
context.fillStyle = '#222';
context.fillRect(0, 0, canvas.width, canvas.height);
const context = canvas.getContext('2d')!;
context.fillStyle = '#222';
context.fillRect(0, 0, canvas.width, canvas.height);
this.tooltips.length = 0;
this.tooltips.length = 0;
if (!this.timings.length)
return;
if (!this.timings.length)
return;
const firstTime = this.timings[0].time;
const startTime = this.startTime;
const endTime = this.endTime;
const totalTime = endTime - startTime;
const timeScale = canvas.width / totalTime;
const firstTime = this.timings[0].time;
const startTime = this.startTime;
const endTime = this.endTime;
const totalTime = endTime - startTime;
const timeScale = canvas.width / totalTime;
function timeToX(time: number) {
return (time - startTime) * timeScale;
}
function timeToX(time: number) {
return (time - startTime) * timeScale;
}
function timeToXAligned(time: number) {
return Math.floor(timeToX(time)) + 0.5;
}
function timeToXAligned(time: number) {
return Math.floor(timeToX(time)) + 0.5;
}
context.textBaseline = 'middle';
context.font = 'Arial 14px normal';
context.textBaseline = 'middle';
context.font = 'Arial 14px normal';
// scale
const scaleHeight = 20;
// scale
const scaleHeight = 20;
context.strokeStyle = '#666';
context.beginPath();
context.strokeStyle = '#666';
context.beginPath();
for (let time = firstTime; time < endTime; time += frameTime) {
context.moveTo(timeToXAligned(time), 0);
context.lineTo(timeToXAligned(time), canvas.height);
}
for (let time = firstTime; time < endTime; time += frameTime) {
context.moveTo(timeToXAligned(time), 0);
context.lineTo(timeToXAligned(time), canvas.height);
}
context.stroke();
context.stroke();
context.strokeStyle = '#ddd';
context.beginPath();
context.moveTo(0, scaleHeight + 0.5);
context.lineTo(canvas.width, scaleHeight + 0.5);
context.stroke();
context.strokeStyle = '#ddd';
context.beginPath();
context.moveTo(0, scaleHeight + 0.5);
context.lineTo(canvas.width, scaleHeight + 0.5);
context.stroke();
// entries
const rowHeight = 20;
const startStack: TimingEntry[] = [];
// entries
const rowHeight = 20;
const startStack: TimingEntry[] = [];
for (const entry of this.timings) {
if (entry.type === TimingEntryType.Start) {
startStack.push(entry);
} else {
const start = startStack.pop()!;
const name = start.name!;
const startX = timeToX(start.time);
const endX = timeToX(entry.time);
const y = scaleHeight + 2 + startStack.length * rowHeight;
const w = endX - startX;
let text = name;
const time = entry.time - start.time;
for (const entry of this.timings) {
if (entry.type === TimingEntryType.Start) {
startStack.push(entry);
} else {
const start = startStack.pop()!;
const name = start.name!;
const startX = timeToX(start.time);
const endX = timeToX(entry.time);
const y = scaleHeight + 2 + startStack.length * rowHeight;
const w = endX - startX;
let text = name;
const time = entry.time - start.time;
if (startStack.length === 0) {
context.fillStyle = '#efc457';
text = `${text} (${time.toFixed(2)} ms) ${(100 * time / frameTime).toFixed(0)}%`;
} else if (/\(\)$/.test(name)) {
context.fillStyle = '#d4ecc6';
} else {
context.fillStyle = '#c6dcec';
}
if (startStack.length === 0) {
context.fillStyle = '#efc457';
text = `${text} (${time.toFixed(2)} ms) ${(100 * time / frameTime).toFixed(0)}%`;
} else if (/\(\)$/.test(name)) {
context.fillStyle = '#d4ecc6';
} else {
context.fillStyle = '#c6dcec';
}
context.fillRect(startX, y, w, rowHeight - 1);
this.tooltips.push({ x: startX, y, w, h: rowHeight, text: `${name}\n${time.toFixed(2)} ms` });
context.fillRect(startX, y, w, rowHeight - 1);
this.tooltips.push({ x: startX, y, w, h: rowHeight, text: `${name}\n${time.toFixed(2)} ms` });
if (w > 4) {
context.fillStyle = '#222';
context.fillText(text, startX + 4, y + rowHeight / 2);
}
}
}
}
recalcListing() {
interface Entry extends TimingEntry {
excludedTime: number;
}
if (w > 4) {
context.fillStyle = '#222';
context.fillText(text, startX + 4, y + rowHeight / 2);
}
}
}
}
recalcListing() {
interface Entry extends TimingEntry {
excludedTime: number;
}
this.listing = [];
const startStack: Entry[] = [];
this.listing = [];
const startStack: Entry[] = [];
for (const entry of this.timings) {
if (entry.type === TimingEntryType.Start) {
startStack.push({ ...entry, excludedTime: 0 });
} else {
const start = startStack.pop()!;
const name = start.name!;
const time = entry.time - start.time;
for (const entry of this.timings) {
if (entry.type === TimingEntryType.Start) {
startStack.push({ ...entry, excludedTime: 0 });
} else {
const start = startStack.pop()!;
const name = start.name!;
const time = entry.time - start.time;
let listing = this.listing.find(l => l.name === name);
let listing = this.listing.find(l => l.name === name);
if (!listing) {
listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 };
this.listing.push(listing);
}
if (!listing) {
listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 };
this.listing.push(listing);
}
listing.count++;
listing.selfTime += (time - start.excludedTime);
listing.totalTime += time;
listing.count++;
listing.selfTime += (time - start.excludedTime);
listing.totalTime += time;
if (startStack.length) {
startStack[startStack.length - 1].excludedTime += time;
}
}
}
if (startStack.length) {
startStack[startStack.length - 1].excludedTime += time;
}
}
}
const firstTime = this.timings[0].time;
const lastTime = this.timings[this.timings.length - 1].time;
const totalTime = lastTime - firstTime;
const firstTime = this.timings[0].time;
const lastTime = this.timings[this.timings.length - 1].time;
const totalTime = lastTime - firstTime;
for (const listing of this.listing) {
listing.selfPercent = 100 * listing.selfTime / totalTime;
listing.totalPercent = 100 * listing.totalTime / totalTime;
}
for (const listing of this.listing) {
listing.selfPercent = 100 * listing.selfTime / totalTime;
listing.totalPercent = 100 * listing.totalTime / totalTime;
}
this.listing.sort((a, b) => b.selfTime - a.selfTime);
}
this.listing.sort((a, b) => b.selfTime - a.selfTime);
}
}
@@ -1,8 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'admin-reports',
templateUrl: 'admin-reports.pug',
selector: 'admin-reports',
templateUrl: 'admin-reports.pug',
})
export class AdminReports {
}
@@ -1,8 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'admin-sign-in',
templateUrl: 'admin-sign-in.pug',
selector: 'admin-sign-in',
templateUrl: 'admin-sign-in.pug',
})
export class AdminSignIn {
}
+100 -100
View File
@@ -1,111 +1,111 @@
import { Component } from '@angular/core';
import {
GameServerState, SERVER_SETTINGS, LOGIN_SERVER_SETTINGS, ServerStats, RequestStats,
UserCountStats, Stats, StatsTable
GameServerState, SERVER_SETTINGS, LOGIN_SERVER_SETTINGS, ServerStats, RequestStats,
UserCountStats, Stats, StatsTable
} from '../../../common/adminInterfaces';
import { hasRole } from '../../../common/accountUtils';
import { AdminModel } from '../../services/adminModel';
import { faCog, faSlidersH } from '../../../client/icons';
@Component({
selector: 'admin-state',
templateUrl: 'admin-state.pug',
styleUrls: ['admin-state.scss'],
selector: 'admin-state',
templateUrl: 'admin-state.pug',
styleUrls: ['admin-state.scss'],
})
export class AdminState {
readonly cogIcon = faCog;
readonly optionsIcon = faSlidersH;
readonly loginOptions = LOGIN_SERVER_SETTINGS;
readonly options = SERVER_SETTINGS;
private stats = new Map<string, ServerStats>();
private statsTables = new Map<string, StatsTable>();
requestStats?: RequestStats[];
userCounts?: UserCountStats[];
showSettings: any = {};
constructor(private model: AdminModel) {
}
get state() {
return this.model.state;
}
get loginServers() {
return this.model.state.loginServers;
}
get servers() {
return this.model.state.gameServers;
}
get isSuperadmin() {
return hasRole(this.model.account, 'superadmin');
}
kickAll(server: GameServerState) {
return this.model.kickAll(server.id);
}
updateLoginSetting(key: string, value: boolean) {
return this.model.updateSettings({ [key]: value });
}
updateLoginSettings(state: any) {
return this.model.updateSettings(state);
}
updateServerSetting(server: GameServerState, key: string, value: boolean) {
return this.model.updateGameServerSettings(server.id, { [key]: value });
}
updateServerSettings(server: GameServerState, state: any) {
return this.model.updateGameServerSettings(server.id, state);
}
// request stats
fetchRequestStats() {
this.model.getRequestStats()
.then(stats => {
this.requestStats = stats && stats.requests;
this.userCounts = stats && stats.userCounts;
});
}
resetRequestStats() {
this.requestStats = undefined;
this.userCounts = undefined;
}
// socket stats
fetchStats(server: GameServerState) {
return this.model.fetchServerStats(server.id)
.then(stats => stats && this.stats.set(server.id, stats));
}
resetStats(server: GameServerState) {
this.stats.delete(server.id);
}
getStats(server: GameServerState) {
return this.stats.get(server.id);
}
// stats tables
fetchCountryStats(server: GameServerState) {
return this.fetchStatsTable(server, Stats.Country);
}
fetchSupportStats(server: GameServerState) {
return this.fetchStatsTable(server, Stats.Support);
}
fetchMapStats(server: GameServerState) {
return this.fetchStatsTable(server, Stats.Maps);
}
resetStatsTable(server: GameServerState) {
this.statsTables.delete(server.id);
}
getStatsTable(server: GameServerState) {
return this.statsTables.get(server.id);
}
private fetchStatsTable(server: GameServerState, stats: Stats) {
return this.model.fetchServerStatsTable(server.id, stats)
.then(stats => stats && this.statsTables.set(server.id, stats));
}
// updates
notifyOfUpdate(server = '*') {
if (confirm('Are you sure?')) {
this.model.notifyUpdate(server);
}
}
shutdownServers(server = '*') {
if (confirm('Are you sure?')) {
this.model.shutdownServers(server);
}
}
resetUpdating(server = '*') {
this.model.resetUpdating(server);
}
readonly cogIcon = faCog;
readonly optionsIcon = faSlidersH;
readonly loginOptions = LOGIN_SERVER_SETTINGS;
readonly options = SERVER_SETTINGS;
private stats = new Map<string, ServerStats>();
private statsTables = new Map<string, StatsTable>();
requestStats?: RequestStats[];
userCounts?: UserCountStats[];
showSettings: any = {};
constructor(private model: AdminModel) {
}
get state() {
return this.model.state;
}
get loginServers() {
return this.model.state.loginServers;
}
get servers() {
return this.model.state.gameServers;
}
get isSuperadmin() {
return hasRole(this.model.account, 'superadmin');
}
kickAll(server: GameServerState) {
return this.model.kickAll(server.id);
}
updateLoginSetting(key: string, value: boolean) {
return this.model.updateSettings({ [key]: value });
}
updateLoginSettings(state: any) {
return this.model.updateSettings(state);
}
updateServerSetting(server: GameServerState, key: string, value: boolean) {
return this.model.updateGameServerSettings(server.id, { [key]: value });
}
updateServerSettings(server: GameServerState, state: any) {
return this.model.updateGameServerSettings(server.id, state);
}
// request stats
fetchRequestStats() {
this.model.getRequestStats()
.then(stats => {
this.requestStats = stats && stats.requests;
this.userCounts = stats && stats.userCounts;
});
}
resetRequestStats() {
this.requestStats = undefined;
this.userCounts = undefined;
}
// socket stats
fetchStats(server: GameServerState) {
return this.model.fetchServerStats(server.id)
.then(stats => stats && this.stats.set(server.id, stats));
}
resetStats(server: GameServerState) {
this.stats.delete(server.id);
}
getStats(server: GameServerState) {
return this.stats.get(server.id);
}
// stats tables
fetchCountryStats(server: GameServerState) {
return this.fetchStatsTable(server, Stats.Country);
}
fetchSupportStats(server: GameServerState) {
return this.fetchStatsTable(server, Stats.Support);
}
fetchMapStats(server: GameServerState) {
return this.fetchStatsTable(server, Stats.Maps);
}
resetStatsTable(server: GameServerState) {
this.statsTables.delete(server.id);
}
getStatsTable(server: GameServerState) {
return this.statsTables.get(server.id);
}
private fetchStatsTable(server: GameServerState, stats: Stats) {
return this.model.fetchServerStatsTable(server.id, stats)
.then(stats => stats && this.statsTables.set(server.id, stats));
}
// updates
notifyOfUpdate(server = '*') {
if (confirm('Are you sure?')) {
this.model.notifyUpdate(server);
}
}
shutdownServers(server = '*') {
if (confirm('Are you sure?')) {
this.model.shutdownServers(server);
}
}
resetUpdating(server = '*') {
this.model.resetUpdating(server);
}
}
+75 -75
View File
@@ -56,84 +56,84 @@ import { AdminApp } from './admin';
import { ErrorReporter } from '../services/errorReporter';
export const routes: Routes = [
{ path: '', component: AdminState },
{ path: 'sign-in', component: AdminSignIn },
{ path: 'events', component: AdminEvents },
{ path: 'accounts', component: AdminAccounts },
{ path: 'accounts/:id', component: AdminAccountDetails },
{ path: 'ponies', component: AdminPonies },
{ path: 'origins', component: AdminOrigins },
{ path: 'origins/:ip/:country', component: AdminOriginDetails },
{
path: 'reports',
component: AdminReports,
children: [
{ path: '', redirectTo: 'perf', pathMatch: 'full' },
{ path: 'perf', component: AdminReportsPerf },
],
},
{ path: 'other', component: AdminOther },
{ path: '', component: AdminState },
{ path: 'sign-in', component: AdminSignIn },
{ path: 'events', component: AdminEvents },
{ path: 'accounts', component: AdminAccounts },
{ path: 'accounts/:id', component: AdminAccountDetails },
{ path: 'ponies', component: AdminPonies },
{ path: 'origins', component: AdminOrigins },
{ path: 'origins/:ip/:country', component: AdminOriginDetails },
{
path: 'reports',
component: AdminReports,
children: [
{ path: '', redirectTo: 'perf', pathMatch: 'full' },
{ path: 'perf', component: AdminReportsPerf },
],
},
{ path: 'other', component: AdminOther },
];
@NgModule({
imports: [
BrowserModule,
RouterModule,
FormsModule,
HttpClientModule,
PopoverModule.forRoot(),
PaginationModule.forRoot(),
ButtonsModule.forRoot(),
ModalModule,
TooltipModule,
SharedModule,
RouterModule.forRoot(routes),
FontAwesomeModule,
],
declarations: [
KeysPipe,
OrderByPipe,
TranslitPipe,
EventsTable,
AccountInfo,
AccountInfoRemote,
AccountStatus,
AccountTooltip,
OriginInfo,
OriginInfoRemote,
OriginListRemote,
PonyInfo,
PonyInfoRemote,
PonyListRemote,
AuthInfo,
AuthInfoRemote,
AuthInfoEdit,
AuthList,
AuthListRemote,
OnOffSwitch,
AdminChatLog,
BanIcon,
EmailList,
FromNow,
TimeField,
UAInfo,
AdminState,
AdminEvents,
AdminOther,
AdminAccounts,
AdminAccountDetails,
AdminPonies,
AdminReports,
AdminReportsPerf,
AdminOrigins,
AdminOriginDetails,
AdminSignIn,
AdminApp,
],
providers: [
ErrorReporter,
],
bootstrap: [AdminApp],
imports: [
BrowserModule,
RouterModule,
FormsModule,
HttpClientModule,
PopoverModule.forRoot(),
PaginationModule.forRoot(),
ButtonsModule.forRoot(),
ModalModule,
TooltipModule,
SharedModule,
RouterModule.forRoot(routes),
FontAwesomeModule,
],
declarations: [
KeysPipe,
OrderByPipe,
TranslitPipe,
EventsTable,
AccountInfo,
AccountInfoRemote,
AccountStatus,
AccountTooltip,
OriginInfo,
OriginInfoRemote,
OriginListRemote,
PonyInfo,
PonyInfoRemote,
PonyListRemote,
AuthInfo,
AuthInfoRemote,
AuthInfoEdit,
AuthList,
AuthListRemote,
OnOffSwitch,
AdminChatLog,
BanIcon,
EmailList,
FromNow,
TimeField,
UAInfo,
AdminState,
AdminEvents,
AdminOther,
AdminAccounts,
AdminAccountDetails,
AdminPonies,
AdminReports,
AdminReportsPerf,
AdminOrigins,
AdminOriginDetails,
AdminSignIn,
AdminApp,
],
providers: [
ErrorReporter,
],
bootstrap: [AdminApp],
})
export class AdminAppModule {
}
+56 -56
View File
@@ -5,71 +5,71 @@ import { PopoverConfig } from 'ngx-bootstrap/popover';
import { hasRole } from '../../common/accountUtils';
import { AdminModel } from '../services/adminModel';
import {
faSpinner, faSlidersH, faExclamationCircle, faUsers, faHorseHead, faMapMarkerAlt, faChartPie, faCog
faSpinner, faSlidersH, faExclamationCircle, faUsers, faHorseHead, faMapMarkerAlt, faChartPie, faCog
} from '../../client/icons';
export function tooltipConfig() {
return Object.assign(new TooltipConfig(), { container: 'body' });
return Object.assign(new TooltipConfig(), { container: 'body' });
}
export function popoverConfig() {
return Object.assign(new PopoverConfig(), { container: 'body' });
return Object.assign(new PopoverConfig(), { container: 'body' });
}
@Component({
selector: 'pony-town-app',
templateUrl: 'admin.pug',
styleUrls: ['admin.scss'],
providers: [
{ provide: TooltipConfig, useFactory: tooltipConfig },
{ provide: PopoverConfig, useFactory: popoverConfig },
]
selector: 'pony-town-app',
templateUrl: 'admin.pug',
styleUrls: ['admin.scss'],
providers: [
{ provide: TooltipConfig, useFactory: tooltipConfig },
{ provide: PopoverConfig, useFactory: popoverConfig },
]
})
export class AdminApp {
readonly spinnerIcon = faSpinner;
readonly stateIcon = faSlidersH;
readonly eventsIcon = faExclamationCircle;
readonly accountsIcon = faUsers;
readonly poniesIcon = faHorseHead;
readonly originsIcon = faMapMarkerAlt;
readonly reportsIcon = faChartPie;
readonly otherIcon = faCog;
constructor(public model: AdminModel, private router: Router) {
}
get loading() {
if (!this.model.initialized) {
return 'Initializing';
} else if (!this.model.connected) {
return 'Connecting';
} else if (!this.model.loaded) {
return 'Loading';
} else {
return '';
}
}
get clients() {
return this.model.state.gameServers.reduce((sum, s) => sum + s.online, 0);
}
get events() {
return this.model.events.length;
}
get accounts() {
return this.model.counts.accounts;
}
get ponies() {
return this.model.counts.characters;
}
get origins() {
return this.model.counts.origins;
}
get isSuperadmin() {
return hasRole(this.model.account, 'superadmin');
}
@HostListener('window:go-to-account', ['$event'])
goToAccount({ detail }: CustomEvent) {
this.router.navigate(['/accounts', detail]);
}
signOut() {
window.location.href = '/';
}
readonly spinnerIcon = faSpinner;
readonly stateIcon = faSlidersH;
readonly eventsIcon = faExclamationCircle;
readonly accountsIcon = faUsers;
readonly poniesIcon = faHorseHead;
readonly originsIcon = faMapMarkerAlt;
readonly reportsIcon = faChartPie;
readonly otherIcon = faCog;
constructor(public model: AdminModel, private router: Router) {
}
get loading() {
if (!this.model.initialized) {
return 'Initializing';
} else if (!this.model.connected) {
return 'Connecting';
} else if (!this.model.loaded) {
return 'Loading';
} else {
return '';
}
}
get clients() {
return this.model.state.gameServers.reduce((sum, s) => sum + s.online, 0);
}
get events() {
return this.model.events.length;
}
get accounts() {
return this.model.counts.accounts;
}
get ponies() {
return this.model.counts.characters;
}
get origins() {
return this.model.counts.origins;
}
get isSuperadmin() {
return hasRole(this.model.account, 'superadmin');
}
@HostListener('window:go-to-account', ['$event'])
goToAccount({ detail }: CustomEvent) {
this.router.navigate(['/accounts', detail]);
}
signOut() {
window.location.href = '/';
}
}
+92 -92
View File
@@ -1,100 +1,100 @@
import { debounce } from 'lodash';
export interface BaseTableState {
search: string;
currentPage: number;
sortedBy: string;
sortedAsc: boolean;
search: string;
currentPage: number;
sortedBy: string;
sortedAsc: boolean;
}
export abstract class BaseTable<T> {
itemsPerPage = 20;
sorted: T[] = [];
filtered: T[] = [];
filteredOnPage: T[] = [];
sortedBy = 'createdAt';
sortedAsc = true;
private _search = '';
private _currentPage = 1;
private execSearch = debounce(() => {
this.updateFiltered();
this.onChange();
}, 500);
get itemsFrom() {
return (this.currentPage - 1) * this.itemsPerPage;
}
get items(): T[] {
return [];
}
get search() {
return this._search;
}
set search(value: string) {
if (this._search !== value) {
this._search = value;
this.execSearch();
}
}
get currentPage() {
return this._currentPage;
}
set currentPage(value: number) {
if (this._currentPage !== value) {
this._currentPage = value;
this.updatePage();
this.onChange();
}
}
sortBy(field: string) {
if (this.sortedBy === field) {
this.sortedAsc = !this.sortedAsc;
} else {
this.sortedBy = field;
this.sortedAsc = true;
}
itemsPerPage = 20;
sorted: T[] = [];
filtered: T[] = [];
filteredOnPage: T[] = [];
sortedBy = 'createdAt';
sortedAsc = true;
private _search = '';
private _currentPage = 1;
private execSearch = debounce(() => {
this.updateFiltered();
this.onChange();
}, 500);
get itemsFrom() {
return (this.currentPage - 1) * this.itemsPerPage;
}
get items(): T[] {
return [];
}
get search() {
return this._search;
}
set search(value: string) {
if (this._search !== value) {
this._search = value;
this.execSearch();
}
}
get currentPage() {
return this._currentPage;
}
set currentPage(value: number) {
if (this._currentPage !== value) {
this._currentPage = value;
this.updatePage();
this.onChange();
}
}
sortBy(field: string) {
if (this.sortedBy === field) {
this.sortedAsc = !this.sortedAsc;
} else {
this.sortedBy = field;
this.sortedAsc = true;
}
this.updateSorted();
this.onChange();
}
sortedClass(field: string) {
return this.sortedBy === field ? (this.sortedAsc ? 'sorted-asc' : 'sorted-desc') : undefined;
}
protected updateItems() {
this.updateSorted();
}
protected updateSorted() {
this.sorted = this.sortItems(this.items, this.sortedBy, this.sortedAsc);
this.updateFiltered();
}
protected updateFiltered() {
this.filtered = this.filterItems(this.sorted, this.search);
this.updatePage();
}
protected updatePage() {
this.filteredOnPage = this.filtered.slice(this.itemsFrom, this.itemsFrom + this.itemsPerPage);
}
protected sortItems(items: T[], _by: string, _asc: boolean) {
return items;
}
protected filterItems(items: T[], _search: string) {
return items;
}
protected onChange() {
}
protected getState(): BaseTableState {
return {
search: this.search,
currentPage: this.currentPage,
sortedBy: this.sortedBy,
sortedAsc: this.sortedAsc,
};
}
protected setState(state: BaseTableState | undefined) {
if (state) {
this._search = state.search;
this._currentPage = state.currentPage || 1;
this.sortedBy = state.sortedBy;
this.sortedAsc = state.sortedAsc;
}
}
this.updateSorted();
this.onChange();
}
sortedClass(field: string) {
return this.sortedBy === field ? (this.sortedAsc ? 'sorted-asc' : 'sorted-desc') : undefined;
}
protected updateItems() {
this.updateSorted();
}
protected updateSorted() {
this.sorted = this.sortItems(this.items, this.sortedBy, this.sortedAsc);
this.updateFiltered();
}
protected updateFiltered() {
this.filtered = this.filterItems(this.sorted, this.search);
this.updatePage();
}
protected updatePage() {
this.filteredOnPage = this.filtered.slice(this.itemsFrom, this.itemsFrom + this.itemsPerPage);
}
protected sortItems(items: T[], _by: string, _asc: boolean) {
return items;
}
protected filterItems(items: T[], _search: string) {
return items;
}
protected onChange() {
}
protected getState(): BaseTableState {
return {
search: this.search,
currentPage: this.currentPage,
sortedBy: this.sortedBy,
sortedAsc: this.sortedAsc,
};
}
protected setState(state: BaseTableState | undefined) {
if (state) {
this._search = state.search;
this._currentPage = state.currentPage || 1;
this.sortedBy = state.sortedBy;
this.sortedAsc = state.sortedAsc;
}
}
}
+4 -4
View File
@@ -1,10 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'keys',
name: 'keys',
})
export class KeysPipe implements PipeTransform {
transform(value: any) {
return value ? Object.keys(value) : value;
}
transform(value: any) {
return value ? Object.keys(value) : value;
}
}
+4 -4
View File
@@ -1,10 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'orderBy',
name: 'orderBy',
})
export class OrderByPipe implements PipeTransform {
transform(value: any[] | undefined, compare?: any) {
return value && value.slice().sort(compare);
}
transform(value: any[] | undefined, compare?: any) {
return value && value.slice().sort(compare);
}
}
@@ -2,14 +2,14 @@ import { Pipe, PipeTransform } from '@angular/core';
import { transliterate } from 'transliteration';
@Pipe({
name: 'translit',
name: 'translit',
})
export class TranslitPipe implements PipeTransform {
transform(value: string) {
if (!value || /^[a-z0-9-_.,\[\]!@#$%^&*{}|\/\\ ]+$/i.test(value))
return undefined;
transform(value: string) {
if (!value || /^[a-z0-9-_.,\[\]!@#$%^&*{}|\/\\ ]+$/i.test(value))
return undefined;
const translit = transliterate(value);
return translit !== value ? translit : undefined;
}
const translit = transliterate(value);
return translit !== value ? translit : undefined;
}
}
@@ -4,41 +4,41 @@ import { Subscription } from '../../../../common/interfaces';
import { AdminModel } from '../../../services/adminModel';
@Component({
selector: 'account-info-remote',
templateUrl: 'account-info-remote.pug',
selector: 'account-info-remote',
templateUrl: 'account-info-remote.pug',
})
export class AccountInfoRemote implements OnDestroy {
@Input() extendedAuths = false;
@Input() popoverPlacement?: string;
@Input() showDuplicates = false;
@Input() basic = false;
account?: Account;
private _accountId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
get accountId() {
return this._accountId;
}
@Input() set accountId(value) {
if (this._accountId !== value) {
this._accountId = value;
this.account = undefined;
this.updateSubscription();
}
}
private updateSubscription() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = undefined;
}
@Input() extendedAuths = false;
@Input() popoverPlacement?: string;
@Input() showDuplicates = false;
@Input() basic = false;
account?: Account;
private _accountId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
get accountId() {
return this._accountId;
}
@Input() set accountId(value) {
if (this._accountId !== value) {
this._accountId = value;
this.account = undefined;
this.updateSubscription();
}
}
private updateSubscription() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = undefined;
}
if (this.accountId) {
this.subscription = this.model.accounts
.subscribe(this.accountId, account => this.account = account);
}
}
if (this.accountId) {
this.subscription = this.model.accounts
.subscribe(this.accountId, account => this.account = account);
}
}
}
@@ -4,14 +4,14 @@ import { compact } from 'lodash';
import { DAY, MINUTE, HOUR } from '../../../../common/constants';
import { hasFlag, fromNow, toInt, setFlag } from '../../../../common/utils';
import {
Account, AccountFlags, accountCounters, accountFlags, SupporterFlags, supporterFlags, BannedMuted, DuplicatesInfo
Account, AccountFlags, accountCounters, accountFlags, SupporterFlags, supporterFlags, BannedMuted, DuplicatesInfo
} from '../../../../common/adminInterfaces';
import { supporterLevel, patreonSupporterLevel, getAge, isPastSupporter } from '../../../../common/adminUtils';
import { AdminModel } from '../../../services/adminModel';
import { AccountCounters } from '../../../../common/interfaces';
import {
faPatreon, faCog, faMinus, faPlus, faCheck, faFlag, faStickyNote, faCertificate, faIdBadge, faEnvelope, faFont,
faClock, faBan, faMapMarkerAlt
faPatreon, faCog, faMinus, faPlus, faCheck, faFlag, faStickyNote, faCertificate, faIdBadge, faEnvelope, faFont,
faClock, faBan, faMapMarkerAlt
} from '../../../../client/icons';
const EMPTY_ROLES: string[] = [];
@@ -22,271 +22,271 @@ const accountDuplicatesIntervalTime = 10 * MINUTE;
const accountDuplicates = new Map<string, DuplicatesInfo>();
const predefinedAlerts = [
{
name: 'erp:flagged',
message: `Your account has been flagged for inappropriate bahavior on PG rated server. `
+ `Continuing that behavior may result in permanent ban.`
},
{
name: 'erp:timeout',
message: `Your account has been timed out for inappropriate language and bahavior on PG rated server. `
+ `Continuing that behavior may result in permanent ban.`
},
{
name: 'dups',
message: `Your account has been flagged for making multiple accounts. `
+ `Continuing that behavior may result in permanent ban.`
},
{
name: 'under',
message: `Your account has been reported for being underage, please do NOT play on 18+ server. `
+ `Continuing that may result in permanent ban.`
},
{
name: 'erp:flagged',
message: `Your account has been flagged for inappropriate bahavior on PG rated server. `
+ `Continuing that behavior may result in permanent ban.`
},
{
name: 'erp:timeout',
message: `Your account has been timed out for inappropriate language and bahavior on PG rated server. `
+ `Continuing that behavior may result in permanent ban.`
},
{
name: 'dups',
message: `Your account has been flagged for making multiple accounts. `
+ `Continuing that behavior may result in permanent ban.`
},
{
name: 'under',
message: `Your account has been reported for being underage, please do NOT play on 18+ server. `
+ `Continuing that may result in permanent ban.`
},
];
const alertExpires = [
{ name: '1h', length: HOUR },
{ name: '5h', length: 5 * HOUR },
{ name: '12h', length: 12 * HOUR },
{ name: '1d', length: DAY },
{ name: '2d', length: 2 * DAY },
{ name: '5d', length: 5 * DAY },
{ name: '7d', length: 7 * DAY },
{ name: '2w', length: 14 * DAY },
{ name: '1h', length: HOUR },
{ name: '5h', length: 5 * HOUR },
{ name: '12h', length: 12 * HOUR },
{ name: '1d', length: DAY },
{ name: '2d', length: 2 * DAY },
{ name: '5d', length: 5 * DAY },
{ name: '7d', length: 7 * DAY },
{ name: '2w', length: 14 * DAY },
];
@Component({
selector: 'account-info',
templateUrl: 'account-info.pug',
styleUrls: ['account-info.scss'],
selector: 'account-info',
templateUrl: 'account-info.pug',
styleUrls: ['account-info.scss'],
})
export class AccountInfo implements OnInit, OnChanges {
readonly counters = accountCounters;
readonly flags = accountFlags;
readonly supporterFlags = supporterFlags;
readonly cogIcon = faCog;
readonly minusIcon = faMinus;
readonly plusIcon = faPlus;
readonly checkIcon = faCheck;
readonly flagIcon = faFlag;
readonly noteIcon = faStickyNote;
readonly newIcon = faCertificate;
readonly duplicateBrowserIdIcon = faIdBadge;
readonly duplicateEmailIcon = faEnvelope;
readonly duplicateNameIcon = faFont;
readonly duplicatePermaIcon = faBan;
readonly teleportIcon = faMapMarkerAlt;
predefinedAlerts = predefinedAlerts;
alertExpires = alertExpires;
alertExpire = alertExpires[3];
alertMessage = '';
@Input() account!: Account;
@Input() extendedAuths = false;
@Input() popoverPlacement?: string;
@Input() showDuplicates = false;
@ViewChild('alertModal', { static: true }) alertModal!: TemplateRef<any>;
note?: string;
duplicates?: DuplicatesInfo;
private alertModalRef?: BsModalRef;
private _isNoteOpen = false;
constructor(private model: AdminModel, private modalService: BsModalService) {
}
ngOnInit() {
this.updateDuplicates();
}
ngOnChanges(changes: SimpleChanges) {
if (changes.account) {
this.updateDuplicates();
}
}
get age() {
return this.account.birthdate ? getAge(this.account.birthdate) : '-';
}
get alert() {
const alert = this.account.alert;
return (alert && alert.expires.getTime() > Date.now()) ? alert : undefined;
}
get isNew() {
return this.account.createdAt && this.account.createdAt.getTime() > newTime;
}
get isNoteOpen() {
return this._isNoteOpen;
}
set isNoteOpen(value: boolean) {
if (this._isNoteOpen !== value) {
this._isNoteOpen = value;
readonly counters = accountCounters;
readonly flags = accountFlags;
readonly supporterFlags = supporterFlags;
readonly cogIcon = faCog;
readonly minusIcon = faMinus;
readonly plusIcon = faPlus;
readonly checkIcon = faCheck;
readonly flagIcon = faFlag;
readonly noteIcon = faStickyNote;
readonly newIcon = faCertificate;
readonly duplicateBrowserIdIcon = faIdBadge;
readonly duplicateEmailIcon = faEnvelope;
readonly duplicateNameIcon = faFont;
readonly duplicatePermaIcon = faBan;
readonly teleportIcon = faMapMarkerAlt;
predefinedAlerts = predefinedAlerts;
alertExpires = alertExpires;
alertExpire = alertExpires[3];
alertMessage = '';
@Input() account!: Account;
@Input() extendedAuths = false;
@Input() popoverPlacement?: string;
@Input() showDuplicates = false;
@ViewChild('alertModal', { static: true }) alertModal!: TemplateRef<any>;
note?: string;
duplicates?: DuplicatesInfo;
private alertModalRef?: BsModalRef;
private _isNoteOpen = false;
constructor(private model: AdminModel, private modalService: BsModalService) {
}
ngOnInit() {
this.updateDuplicates();
}
ngOnChanges(changes: SimpleChanges) {
if (changes.account) {
this.updateDuplicates();
}
}
get age() {
return this.account.birthdate ? getAge(this.account.birthdate) : '-';
}
get alert() {
const alert = this.account.alert;
return (alert && alert.expires.getTime() > Date.now()) ? alert : undefined;
}
get isNew() {
return this.account.createdAt && this.account.createdAt.getTime() > newTime;
}
get isNoteOpen() {
return this._isNoteOpen;
}
set isNoteOpen(value: boolean) {
if (this._isNoteOpen !== value) {
this._isNoteOpen = value;
if (value) {
this.note = this.account.note;
}
}
}
get isInactive() {
return this.account && this.account.lastVisit && this.account.lastVisit.getTime() < oldTime;
}
get roles() {
return this.account.roles ? this.account.roles.filter(r => r !== 'superadmin') : EMPTY_ROLES;
}
get flagClass() {
const counters = this.account.counters;
if (value) {
this.note = this.account.note;
}
}
}
get isInactive() {
return this.account && this.account.lastVisit && this.account.lastVisit.getTime() < oldTime;
}
get roles() {
return this.account.roles ? this.account.roles.filter(r => r !== 'superadmin') : EMPTY_ROLES;
}
get flagClass() {
const counters = this.account.counters;
if (this.account.flags) {
return 'text-banned';
} else if (!counters && !this.account.supporter) {
return 'text-muted';
} else if (counters && ((counters.spam || 0) > 100 || (counters.swears || 0) > 10)) {
return 'text-alert';
} else {
return 'text-present';
}
}
get hasDuplicateNote() {
return this.account.note && /duplicate/i.test(this.account.note);
}
hasFlag(value: AccountFlags) {
return hasFlag(this.account.flags, value);
}
toggleFlag(value: AccountFlags) {
this.model.setAccountFlags(this.account._id, this.account.flags ^ value);
}
toggleBan(field: keyof BannedMuted, value: number) {
this.model.setAccountBanField(this.account._id, field, value);
}
kick() {
this.model.kick(this.account._id);
}
report() {
this.model.report(this.account._id);
}
blur() {
this.model.setNote(this.account._id, this.note || '');
this.isNoteOpen = false;
}
decrementCounter(name: keyof AccountCounters) {
if (this.getCounter(name) > 0) {
this.setCounter(name, this.getCounter(name) - 1);
}
}
incrementCounter(name: keyof AccountCounters) {
this.setCounter(name, this.getCounter(name) + 1);
}
getCounter(name: keyof AccountCounters) {
const counters = this.account.counters;
return toInt(counters && counters[name]);
}
setCounter(name: keyof AccountCounters, value: number) {
const counters = this.account.counters || (this.account.counters = {});
counters[name] = value;
this.model.setAccountCounter(this.account._id, name, value);
}
removeAlert() {
this.model.setAlert(this.account._id, '', 0);
}
setAlert() {
this.alertMessage = this.alert ? this.alert.message : '';
this.alertExpire = this.alertExpires[2];
this.alertModalRef = this.modalService.show(this.alertModal, { ignoreBackdropClick: true });
}
cancelAlert() {
this.alertModalRef && this.alertModalRef.hide();
this.alertModalRef = undefined;
}
confirmAlert() {
this.model.setAlert(this.account._id, this.alertMessage, this.alertExpire.length);
this.cancelAlert();
}
private updateDuplicates() {
const account = this.account;
if (this.account.flags) {
return 'text-banned';
} else if (!counters && !this.account.supporter) {
return 'text-muted';
} else if (counters && ((counters.spam || 0) > 100 || (counters.swears || 0) > 10)) {
return 'text-alert';
} else {
return 'text-present';
}
}
get hasDuplicateNote() {
return this.account.note && /duplicate/i.test(this.account.note);
}
hasFlag(value: AccountFlags) {
return hasFlag(this.account.flags, value);
}
toggleFlag(value: AccountFlags) {
this.model.setAccountFlags(this.account._id, this.account.flags ^ value);
}
toggleBan(field: keyof BannedMuted, value: number) {
this.model.setAccountBanField(this.account._id, field, value);
}
kick() {
this.model.kick(this.account._id);
}
report() {
this.model.report(this.account._id);
}
blur() {
this.model.setNote(this.account._id, this.note || '');
this.isNoteOpen = false;
}
decrementCounter(name: keyof AccountCounters) {
if (this.getCounter(name) > 0) {
this.setCounter(name, this.getCounter(name) - 1);
}
}
incrementCounter(name: keyof AccountCounters) {
this.setCounter(name, this.getCounter(name) + 1);
}
getCounter(name: keyof AccountCounters) {
const counters = this.account.counters;
return toInt(counters && counters[name]);
}
setCounter(name: keyof AccountCounters, value: number) {
const counters = this.account.counters || (this.account.counters = {});
counters[name] = value;
this.model.setAccountCounter(this.account._id, name, value);
}
removeAlert() {
this.model.setAlert(this.account._id, '', 0);
}
setAlert() {
this.alertMessage = this.alert ? this.alert.message : '';
this.alertExpire = this.alertExpires[2];
this.alertModalRef = this.modalService.show(this.alertModal, { ignoreBackdropClick: true });
}
cancelAlert() {
this.alertModalRef && this.alertModalRef.hide();
this.alertModalRef = undefined;
}
confirmAlert() {
this.model.setAlert(this.account._id, this.alertMessage, this.alertExpire.length);
this.cancelAlert();
}
private updateDuplicates() {
const account = this.account;
if (this.showDuplicates && account) {
const cached = accountDuplicates.get(account._id);
const threshold = fromNow(-accountDuplicatesIntervalTime);
if (this.showDuplicates && account) {
const cached = accountDuplicates.get(account._id);
const threshold = fromNow(-accountDuplicatesIntervalTime);
if (cached && cached.generatedAt > threshold.getTime()) {
this.duplicates = cached;
} else {
this.model.getAllDuplicatesQuickInfo(account._id)
.then(duplicates => {
if (duplicates) {
accountDuplicates.set(account._id, duplicates);
this.duplicates = duplicates;
}
});
}
}
}
teleportTo() {
this.model.teleportTo(this.account._id);
}
// supporters
get isPatreonOrSupporter() {
return !!(this.account.patreon || this.account.supporter || this.account.supporterDeclinedSince);
}
get supporterClass() {
return supporterLevel(this.account) ? 'badge-success' : 'badge-warning';
}
get supporterTitle() {
const supporter = this.account.supporter!;
const flagSupporter = (supporter & SupporterFlags.SupporterMask) !== 0;
const patreonSupporter = patreonSupporterLevel(this.account);
const ignorePatreon = hasFlag(supporter, SupporterFlags.IgnorePatreon);
const pastSupporter = hasFlag(supporter, SupporterFlags.PastSupporter);
if (cached && cached.generatedAt > threshold.getTime()) {
this.duplicates = cached;
} else {
this.model.getAllDuplicatesQuickInfo(account._id)
.then(duplicates => {
if (duplicates) {
accountDuplicates.set(account._id, duplicates);
this.duplicates = duplicates;
}
});
}
}
}
teleportTo() {
this.model.teleportTo(this.account._id);
}
// supporters
get isPatreonOrSupporter() {
return !!(this.account.patreon || this.account.supporter || this.account.supporterDeclinedSince);
}
get supporterClass() {
return supporterLevel(this.account) ? 'badge-success' : 'badge-warning';
}
get supporterTitle() {
const supporter = this.account.supporter!;
const flagSupporter = (supporter & SupporterFlags.SupporterMask) !== 0;
const patreonSupporter = patreonSupporterLevel(this.account);
const ignorePatreon = hasFlag(supporter, SupporterFlags.IgnorePatreon);
const pastSupporter = hasFlag(supporter, SupporterFlags.PastSupporter);
return compact([
flagSupporter && 'flags',
patreonSupporter && `patreon`,
ignorePatreon && 'ignore',
!patreonSupporter && this.account.supporterDeclinedSince && 'declined',
pastSupporter && 'past',
]).join(', ');
}
get supporterIcon() {
const hasPatreon = patreonSupporterLevel(this.account);
const hasIgnoreFlag = hasFlag(this.account.supporter, SupporterFlags.IgnorePatreon);
const hasDeclined = !!this.account.supporterDeclinedSince;
return hasPatreon ? faPatreon : ((hasIgnoreFlag || !hasDeclined) ? faFlag : faClock);
}
get hasAnySupporter() {
return (this.account.supporter! & SupporterFlags.SupporterMask) !== 0;
}
get hasPastSupporter() {
return hasFlag(this.account.supporter, SupporterFlags.PastSupporter);
}
get supporterLevel() {
return supporterLevel(this.account);
}
get supporterLevelString() {
const level = supporterLevel(this.account);
return level ? level : (isPastSupporter(this.account) ? 'P' : '');
}
isSupporter(level: number) {
return (this.account.supporter! & SupporterFlags.SupporterMask) === level;
}
setSupporter(level: number) {
const supporter = (this.account.supporter! & ~SupporterFlags.SupporterMask) | level;
this.model.setSupporterFlags(this.account._id, supporter);
}
hasSupporterFlag(value: SupporterFlags) {
return hasFlag(this.account.supporter, value);
}
toggleSupporterFlag(value: SupporterFlags) {
this.model.setSupporterFlags(this.account._id, this.account.supporter! ^ value);
}
// past supporter
get isForcePastSupporter() {
return hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter);
}
get isIgnorePastSupporter() {
return hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter);
}
toggleForcePastSupporter() {
const has = hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter);
const supporter = setFlag(this.account.supporter, SupporterFlags.ForcePastSupporter, !has);
this.model.setSupporterFlags(this.account._id, supporter);
}
toggleIgnorePastSupporter() {
const has = hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter);
const supporter = setFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter, !has);
this.model.setSupporterFlags(this.account._id, supporter);
}
return compact([
flagSupporter && 'flags',
patreonSupporter && `patreon`,
ignorePatreon && 'ignore',
!patreonSupporter && this.account.supporterDeclinedSince && 'declined',
pastSupporter && 'past',
]).join(', ');
}
get supporterIcon() {
const hasPatreon = patreonSupporterLevel(this.account);
const hasIgnoreFlag = hasFlag(this.account.supporter, SupporterFlags.IgnorePatreon);
const hasDeclined = !!this.account.supporterDeclinedSince;
return hasPatreon ? faPatreon : ((hasIgnoreFlag || !hasDeclined) ? faFlag : faClock);
}
get hasAnySupporter() {
return (this.account.supporter! & SupporterFlags.SupporterMask) !== 0;
}
get hasPastSupporter() {
return hasFlag(this.account.supporter, SupporterFlags.PastSupporter);
}
get supporterLevel() {
return supporterLevel(this.account);
}
get supporterLevelString() {
const level = supporterLevel(this.account);
return level ? level : (isPastSupporter(this.account) ? 'P' : '');
}
isSupporter(level: number) {
return (this.account.supporter! & SupporterFlags.SupporterMask) === level;
}
setSupporter(level: number) {
const supporter = (this.account.supporter! & ~SupporterFlags.SupporterMask) | level;
this.model.setSupporterFlags(this.account._id, supporter);
}
hasSupporterFlag(value: SupporterFlags) {
return hasFlag(this.account.supporter, value);
}
toggleSupporterFlag(value: SupporterFlags) {
this.model.setSupporterFlags(this.account._id, this.account.supporter! ^ value);
}
// past supporter
get isForcePastSupporter() {
return hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter);
}
get isIgnorePastSupporter() {
return hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter);
}
toggleForcePastSupporter() {
const has = hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter);
const supporter = setFlag(this.account.supporter, SupporterFlags.ForcePastSupporter, !has);
this.model.setSupporterFlags(this.account._id, supporter);
}
toggleIgnorePastSupporter() {
const has = hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter);
const supporter = setFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter, !has);
this.model.setSupporterFlags(this.account._id, supporter);
}
}
@@ -4,25 +4,25 @@ import { Account, AccountStatus as IAccountStatus } from '../../../../common/adm
import { faUserSecret } from '../../../../client/icons';
@Component({
selector: 'account-status',
templateUrl: 'account-status.pug',
selector: 'account-status',
templateUrl: 'account-status.pug',
})
export class AccountStatus implements OnInit, OnChanges {
readonly incognitoIcon = faUserSecret;
@Input() account!: Account;
@Input() verbose = false;
status: IAccountStatus[] | undefined = undefined;
constructor(private model: AdminModel) {
}
ngOnInit() {
this.refresh();
}
ngOnChanges() {
this.status = undefined;
this.refresh();
}
refresh() {
this.model.getAccountStatus(this.account._id)
.then(status => this.status = status);
}
readonly incognitoIcon = faUserSecret;
@Input() account!: Account;
@Input() verbose = false;
status: IAccountStatus[] | undefined = undefined;
constructor(private model: AdminModel) {
}
ngOnInit() {
this.refresh();
}
ngOnChanges() {
this.status = undefined;
this.refresh();
}
refresh() {
this.model.getAccountStatus(this.account._id)
.then(status => this.status = status);
}
}
@@ -5,16 +5,16 @@ import { getAge } from '../../../../common/adminUtils';
const year = (new Date()).getFullYear();
@Component({
selector: 'account-tooltip',
templateUrl: 'account-tooltip.pug',
selector: 'account-tooltip',
templateUrl: 'account-tooltip.pug',
})
export class AccountTooltip {
@Input() account!: Account;
@Input() extendedAuths = false;
get age() {
return this.account.birthdate ? getAge(this.account.birthdate) : '-';
}
get forceAge() {
return this.account.birthyear ? (year - this.account.birthyear) : '';
}
@Input() account!: Account;
@Input() extendedAuths = false;
get age() {
return this.account.birthdate ? getAge(this.account.birthdate) : '-';
}
get forceAge() {
return this.account.birthyear ? (year - this.account.birthyear) : '';
}
}
@@ -8,175 +8,175 @@ import { removeAllNodes, appendAllNodes, showTextInNewTab } from '../../../../cl
import { includes } from '../../../../common/utils';
@Component({
selector: 'admin-chat-log',
templateUrl: 'admin-chat-log.pug',
styleUrls: ['admin-chat-log.scss'],
selector: 'admin-chat-log',
templateUrl: 'admin-chat-log.pug',
styleUrls: ['admin-chat-log.scss'],
})
export class AdminChatLog implements OnDestroy {
readonly searchIcon = faSearch;
readonly spinnerIcon = faSpinner;
readonly syncIcon = faSync;
readonly fileIcon = faFileAlt;
readonly closeIcon = faTimes;
readonly chevronLeftIcon = faChevronLeft;
readonly chevronRightIcon = faChevronRight;
@Input() canClose = true;
accounts: Account[] = [];
search?: string;
open = false;
today: ChatDate = createChatDate(moment());
dates: ChatDate[] = [/*{ value: 'all', label: 'All' },*/ ...createDateRange(new Date(), 14)];
date?: ChatDate;
chatRaw?: string;
loading = false;
private _account?: Account;
private refreshInterval?: any;
constructor(private model: AdminModel, private element: ElementRef) {
}
get autoRefresh() {
return !!this.refreshInterval;
}
set autoRefresh(value: boolean) {
if (value) {
this.refreshInterval = this.refreshInterval || setInterval(() => this.refresh(), 10 * 1000);
} else {
this.stopInterval();
}
}
get title() {
return this.search || (this.account && this.account.name) || 'Chat';
}
get account() {
return this._account;
}
@Input() set account(value) {
const theSame = this._account === value || (value && this._account && value._id === this._account._id);
this._account = value;
readonly searchIcon = faSearch;
readonly spinnerIcon = faSpinner;
readonly syncIcon = faSync;
readonly fileIcon = faFileAlt;
readonly closeIcon = faTimes;
readonly chevronLeftIcon = faChevronLeft;
readonly chevronRightIcon = faChevronRight;
@Input() canClose = true;
accounts: Account[] = [];
search?: string;
open = false;
today: ChatDate = createChatDate(moment());
dates: ChatDate[] = [/*{ value: 'all', label: 'All' },*/ ...createDateRange(new Date(), 14)];
date?: ChatDate;
chatRaw?: string;
loading = false;
private _account?: Account;
private refreshInterval?: any;
constructor(private model: AdminModel, private element: ElementRef) {
}
get autoRefresh() {
return !!this.refreshInterval;
}
set autoRefresh(value: boolean) {
if (value) {
this.refreshInterval = this.refreshInterval || setInterval(() => this.refresh(), 10 * 1000);
} else {
this.stopInterval();
}
}
get title() {
return this.search || (this.account && this.account.name) || 'Chat';
}
get account() {
return this._account;
}
@Input() set account(value) {
const theSame = this._account === value || (value && this._account && value._id === this._account._id);
this._account = value;
if (!theSame) {
this.date = undefined;
this.setChatlogElements([]);
}
}
ngOnDestroy() {
this.close();
}
show(account?: Account, date?: ChatDate) {
this.search = undefined;
this.account = account;
this.date = date || this.today;
this.open = true;
this.accounts = [];
this.refresh();
}
add(account: Account) {
if (!this.account) {
this.show(account);
} else if (account !== this.account && !includes(this.accounts, account)) {
this.date = this.date || this.today;
this.accounts.push(account);
this.refresh();
}
}
removeAccount(index: number) {
this.accounts.splice(index, 1);
this.refresh();
}
showDate(date: ChatDate) {
this.date = date;
this.refresh();
}
prev() {
this.switchDate(-1);
}
next() {
this.switchDate(1);
}
all() {
this.date = this.dates[0];
this.refresh();
}
close() {
this.account = undefined;
this.date = undefined;
this.open = false;
this.setChatlogElements([]);
this.stopInterval();
}
searchChat(search: string | undefined) {
this.search = search;
this.refresh();
}
refresh() {
const date = this.date && this.date.value;
if (!theSame) {
this.date = undefined;
this.setChatlogElements([]);
}
}
ngOnDestroy() {
this.close();
}
show(account?: Account, date?: ChatDate) {
this.search = undefined;
this.account = account;
this.date = date || this.today;
this.open = true;
this.accounts = [];
this.refresh();
}
add(account: Account) {
if (!this.account) {
this.show(account);
} else if (account !== this.account && !includes(this.accounts, account)) {
this.date = this.date || this.today;
this.accounts.push(account);
this.refresh();
}
}
removeAccount(index: number) {
this.accounts.splice(index, 1);
this.refresh();
}
showDate(date: ChatDate) {
this.date = date;
this.refresh();
}
prev() {
this.switchDate(-1);
}
next() {
this.switchDate(1);
}
all() {
this.date = this.dates[0];
this.refresh();
}
close() {
this.account = undefined;
this.date = undefined;
this.open = false;
this.setChatlogElements([]);
this.stopInterval();
}
searchChat(search: string | undefined) {
this.search = search;
this.refresh();
}
refresh() {
const date = this.date && this.date.value;
if (this.account) {
const accounts = [this.account._id, ...this.accounts.map(a => a._id)];
this.handleChat(this.model.accountsFormattedChat(accounts, date));
} else if (this.search) {
this.handleChat(this.model.searchFormattedChat(this.search, date));
}
}
openLog() {
showTextInNewTab(`${this.date ? this.date.label : 'none'}\n\n${(this.chatRaw || '').replace(/\t/g, ' ')}`);
}
private handleChat(promise: Promise<{ raw: string; html: HTMLElement[]; }>) {
this.loading = true;
if (this.account) {
const accounts = [this.account._id, ...this.accounts.map(a => a._id)];
this.handleChat(this.model.accountsFormattedChat(accounts, date));
} else if (this.search) {
this.handleChat(this.model.searchFormattedChat(this.search, date));
}
}
openLog() {
showTextInNewTab(`${this.date ? this.date.label : 'none'}\n\n${(this.chatRaw || '').replace(/\t/g, ' ')}`);
}
private handleChat(promise: Promise<{ raw: string; html: HTMLElement[]; }>) {
this.loading = true;
promise
.then(({ raw, html }) => {
this.chatRaw = raw;
this.setChatlogElements(html);
})
.finally(() => this.loading = false);
}
private switchDate(days: number) {
const validDate = this.date && this.date.value !== 'all';
this.date = validDate ? createChatDate(moment(this.date!.value).add(days, 'days')) : this.today;
this.refresh();
}
private stopInterval() {
clearInterval(this.refreshInterval);
this.refreshInterval = undefined;
}
private getChatlogElement() {
return (this.element.nativeElement as HTMLElement).querySelector('.chatlog');
}
private setChatlogElements(elements: HTMLElement[]) {
const element = this.getChatlogElement();
promise
.then(({ raw, html }) => {
this.chatRaw = raw;
this.setChatlogElements(html);
})
.finally(() => this.loading = false);
}
private switchDate(days: number) {
const validDate = this.date && this.date.value !== 'all';
this.date = validDate ? createChatDate(moment(this.date!.value).add(days, 'days')) : this.today;
this.refresh();
}
private stopInterval() {
clearInterval(this.refreshInterval);
this.refreshInterval = undefined;
}
private getChatlogElement() {
return (this.element.nativeElement as HTMLElement).querySelector('.chatlog');
}
private setChatlogElements(elements: HTMLElement[]) {
const element = this.getChatlogElement();
if (element) {
removeAllNodes(element);
appendAllNodes(element, elements);
this.nodesToProcess = [
...Array.from(element.getElementsByClassName('name')),
...Array.from(element.getElementsByClassName('message')),
] as HTMLElement[];
this.atNode = 0;
this.processNodes();
}
}
private processNodes() {
const processStep = 50;
const nodes = this.nodesToProcess;
if (element) {
removeAllNodes(element);
appendAllNodes(element, elements);
this.nodesToProcess = [
...Array.from(element.getElementsByClassName('name')),
...Array.from(element.getElementsByClassName('message')),
] as HTMLElement[];
this.atNode = 0;
this.processNodes();
}
}
private processNodes() {
const processStep = 50;
const nodes = this.nodesToProcess;
cancelIdleCallback(this.processIdle);
cancelIdleCallback(this.processIdle);
if (nodes && this.atNode < nodes.length) {
let i = 0;
if (nodes && this.atNode < nodes.length) {
let i = 0;
while (i < processStep && (i + this.atNode) < nodes.length) {
replaceSwears(nodes[i + this.atNode]);
i++;
}
while (i < processStep && (i + this.atNode) < nodes.length) {
replaceSwears(nodes[i + this.atNode]);
i++;
}
this.atNode += i;
this.processIdle = requestIdleCallback(() => this.processNodes());
} else {
this.nodesToProcess = undefined;
}
}
private nodesToProcess?: HTMLElement[];
private atNode = 0;
private processIdle = 0;
this.atNode += i;
this.processIdle = requestIdleCallback(() => this.processNodes());
} else {
this.nodesToProcess = undefined;
}
}
private nodesToProcess?: HTMLElement[];
private atNode = 0;
private processIdle = 0;
}
@@ -5,60 +5,60 @@ import { AdminModel } from '../../../services/adminModel';
import { faInfo, faLock, faTrash, faEyeSlash, faArrowRight } from '../../../../client/icons';
@Component({
selector: 'auth-info-edit',
templateUrl: 'auth-info-edit.pug',
selector: 'auth-info-edit',
templateUrl: 'auth-info-edit.pug',
})
export class AuthInfoEdit implements OnDestroy {
readonly assignIcon = faArrowRight;
readonly infoIcon = faInfo;
readonly lockIcon = faLock;
readonly trashIcon = faTrash;
readonly eyeSlashIcon = faEyeSlash;
@Input() duplicates?: DuplicateResult[];
@Input() showName = false;
auth?: Auth;
private _authId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get authId() {
return this._authId;
}
@Input() set authId(value: string | undefined) {
if (this.authId !== value) {
this._authId = value;
this.auth = undefined;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
removeAuth(auth: Auth | undefined) {
if (auth && confirm('Are you sure?')) {
this.model.removeAuth(auth._id);
}
}
showAuthData(auth: Auth | undefined) {
if (auth) {
this.model.getAuth(auth._id)
.then(x => console.log(x));
}
}
toggleAuthDisabled(auth: Auth | undefined) {
if (auth) {
this.model.updateAuth(auth._id, { disabled: !auth.disabled });
}
}
toggleAuthBanned(auth: Auth | undefined) {
if (auth) {
this.model.updateAuth(auth._id, { banned: !auth.banned });
}
}
assignTo(accountId: string) {
if (this.authId) {
this.model.assignAuth(this.authId, accountId);
}
}
readonly assignIcon = faArrowRight;
readonly infoIcon = faInfo;
readonly lockIcon = faLock;
readonly trashIcon = faTrash;
readonly eyeSlashIcon = faEyeSlash;
@Input() duplicates?: DuplicateResult[];
@Input() showName = false;
auth?: Auth;
private _authId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get authId() {
return this._authId;
}
@Input() set authId(value: string | undefined) {
if (this.authId !== value) {
this._authId = value;
this.auth = undefined;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
removeAuth(auth: Auth | undefined) {
if (auth && confirm('Are you sure?')) {
this.model.removeAuth(auth._id);
}
}
showAuthData(auth: Auth | undefined) {
if (auth) {
this.model.getAuth(auth._id)
.then(x => console.log(x));
}
}
toggleAuthDisabled(auth: Auth | undefined) {
if (auth) {
this.model.updateAuth(auth._id, { disabled: !auth.disabled });
}
}
toggleAuthBanned(auth: Auth | undefined) {
if (auth) {
this.model.updateAuth(auth._id, { banned: !auth.banned });
}
}
assignTo(accountId: string) {
if (this.authId) {
this.model.assignAuth(this.authId, accountId);
}
}
}
@@ -4,28 +4,28 @@ import { AdminModel } from '../../../services/adminModel';
import { Auth } from '../../../../common/adminInterfaces';
@Component({
selector: 'auth-info-remote',
templateUrl: 'auth-info-remote.pug',
selector: 'auth-info-remote',
templateUrl: 'auth-info-remote.pug',
})
export class AuthInfoRemote implements OnDestroy {
@Input() showName = false;
auth?: Auth;
private _authId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get authId() {
return this._authId;
}
@Input() set authId(value: string | undefined) {
if (this.authId !== value) {
this._authId = value;
this.auth = undefined;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
@Input() showName = false;
auth?: Auth;
private _authId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get authId() {
return this._authId;
}
@Input() set authId(value: string | undefined) {
if (this.authId !== value) {
this._authId = value;
this.auth = undefined;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
}
@@ -3,26 +3,26 @@ import { Auth } from '../../../../common/adminInterfaces';
import { oauthIcons, faGlobe } from '../../../../client/icons';
@Component({
selector: 'auth-info',
templateUrl: 'auth-info.pug',
styleUrls: ['auth-info.scss'],
host: {
'[class.deleted]': 'deleted',
},
selector: 'auth-info',
templateUrl: 'auth-info.pug',
styleUrls: ['auth-info.scss'],
host: {
'[class.deleted]': 'deleted',
},
})
export class AuthInfo {
@Input() auth?: Auth;
@Input() showName = false;
get deleted(): boolean {
return !!(this.auth && (this.auth.disabled || this.auth.banned));
}
get name(): string {
return this.auth && this.auth.name || '';
}
get icon() {
return oauthIcons[this.auth && this.auth.provider || ''] || faGlobe;
}
get pledged() {
return (this.auth && this.auth.pledged || 0) / 100;
}
@Input() auth?: Auth;
@Input() showName = false;
get deleted(): boolean {
return !!(this.auth && (this.auth.disabled || this.auth.banned));
}
get name(): string {
return this.auth && this.auth.name || '';
}
get icon() {
return oauthIcons[this.auth && this.auth.provider || ''] || faGlobe;
}
get pledged() {
return (this.auth && this.auth.pledged || 0) / 100;
}
}
@@ -3,35 +3,35 @@ import { Subscription } from '../../../../common/interfaces';
import { AdminModel } from '../../../services/adminModel';
@Component({
selector: 'auth-list-remote',
templateUrl: 'auth-list-remote.pug',
styleUrls: ['auth-list-remote.scss'],
selector: 'auth-list-remote',
templateUrl: 'auth-list-remote.pug',
styleUrls: ['auth-list-remote.scss'],
})
export class AuthListRemote implements OnDestroy {
@Input() limit = 6;
@Input() extended = false;
auths: string[] = [];
loading = false;
private _accountId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get accountId() {
return this._accountId;
}
@Input() set accountId(value: string | undefined) {
if (this.accountId !== value) {
this._accountId = value;
this.auths = [];
this.loading = true;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.accountAuths.subscribe(value, auths => {
this.auths = auths || [];
this.loading = false;
}) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
@Input() limit = 6;
@Input() extended = false;
auths: string[] = [];
loading = false;
private _accountId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get accountId() {
return this._accountId;
}
@Input() set accountId(value: string | undefined) {
if (this.accountId !== value) {
this._accountId = value;
this.auths = [];
this.loading = true;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.accountAuths.subscribe(value, auths => {
this.auths = auths || [];
this.loading = false;
}) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
}
@@ -2,18 +2,18 @@ import { Component, Input } from '@angular/core';
import { Auth } from '../../../../common/adminInterfaces';
@Component({
selector: 'auth-list',
templateUrl: 'auth-list.pug',
styleUrls: ['auth-list.scss'],
host: {
'[class.extended]': 'extended',
},
selector: 'auth-list',
templateUrl: 'auth-list.pug',
styleUrls: ['auth-list.scss'],
host: {
'[class.extended]': 'extended',
},
})
export class AuthList {
@Input() limit = 6;
@Input() extended = false;
@Input() auths?: Auth[];
get fixedAuths() {
return this.auths || [];
}
@Input() limit = 6;
@Input() extended = false;
@Input() auths?: Auth[];
get fixedAuths() {
return this.auths || [];
}
}
@@ -1,5 +1,5 @@
import {
Component, Input, EventEmitter, Output, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, NgZone
Component, Input, EventEmitter, Output, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, NgZone
} from '@angular/core';
import { TIMEOUTS } from '../../../../common/constants';
import { BannedMuted } from '../../../../common/adminInterfaces';
@@ -7,71 +7,71 @@ import { IntervalUpdateService } from '../../../services/intervalUpdateService';
import { faClock, faMicrophoneSlash, faEyeSlash, faBan } from '../../../../client/icons';
const ICONS = {
mute: faMicrophoneSlash,
shadow: faEyeSlash,
ban: faBan,
mute: faMicrophoneSlash,
shadow: faEyeSlash,
ban: faBan,
};
@Component({
selector: 'ban-icon',
templateUrl: 'ban-icon.pug',
styleUrls: ['ban-icon.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'ban-icon',
templateUrl: 'ban-icon.pug',
styleUrls: ['ban-icon.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BanIcon implements OnInit, OnDestroy, OnChanges {
readonly timeouts = TIMEOUTS;
readonly clockIcon = faClock;
@Input() type: keyof BannedMuted = 'ban';
@Input() value = 0;
@Output() toggle = new EventEmitter<number>();
get icon() {
return ICONS[this.type] || ICONS.ban;
}
get isPerma() {
return this.value === -1;
}
get isTimedOut() {
return this.value > Date.now();
}
get className() {
if (this.isPerma) {
return 'text-banned';
} else if (this.isTimedOut) {
return 'text-alert';
} else {
return 'text-muted';
}
}
private timedOut = false;
private toggleUpdate: (on: boolean) => void;
constructor(zone: NgZone, updateService: IntervalUpdateService) {
this.toggleUpdate = updateService.toggle(() => {
if (this.timedOut !== this.isTimedOut) {
zone.run(() => this.timedOut = this.isTimedOut);
this.toggleUpdate(this.isTimedOut);
}
});
}
ngOnInit() {
this.toggleUpdate(this.isTimedOut);
}
ngOnChanges() {
this.toggleUpdate(this.isTimedOut);
}
ngOnDestroy() {
this.toggleUpdate(false);
}
clear() {
this.setValue(0);
}
perma() {
this.setValue(-1);
}
timeout(value: number) {
this.setValue(Date.now() + value);
}
private setValue(value: number) {
this.value = value;
this.toggle.emit(value);
}
readonly timeouts = TIMEOUTS;
readonly clockIcon = faClock;
@Input() type: keyof BannedMuted = 'ban';
@Input() value = 0;
@Output() toggle = new EventEmitter<number>();
get icon() {
return ICONS[this.type] || ICONS.ban;
}
get isPerma() {
return this.value === -1;
}
get isTimedOut() {
return this.value > Date.now();
}
get className() {
if (this.isPerma) {
return 'text-banned';
} else if (this.isTimedOut) {
return 'text-alert';
} else {
return 'text-muted';
}
}
private timedOut = false;
private toggleUpdate: (on: boolean) => void;
constructor(zone: NgZone, updateService: IntervalUpdateService) {
this.toggleUpdate = updateService.toggle(() => {
if (this.timedOut !== this.isTimedOut) {
zone.run(() => this.timedOut = this.isTimedOut);
this.toggleUpdate(this.isTimedOut);
}
});
}
ngOnInit() {
this.toggleUpdate(this.isTimedOut);
}
ngOnChanges() {
this.toggleUpdate(this.isTimedOut);
}
ngOnDestroy() {
this.toggleUpdate(false);
}
clear() {
this.setValue(0);
}
perma() {
this.setValue(-1);
}
timeout(value: number) {
this.setValue(Date.now() + value);
}
private setValue(value: number) {
this.value = value;
this.toggle.emit(value);
}
}
@@ -1,16 +1,16 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'email-list',
templateUrl: 'email-list.pug',
selector: 'email-list',
templateUrl: 'email-list.pug',
})
export class EmailList {
@Input() emails?: string[];
limit = 3;
get hasMore() {
return this.emails && this.emails.length > this.limit;
}
showMore() {
this.limit = 9999;
}
@Input() emails?: string[];
limit = 3;
get hasMore() {
return this.emails && this.emails.length > this.limit;
}
showMore() {
this.limit = 9999;
}
}
@@ -5,52 +5,52 @@ import { faLanguage, faTrash, faComment, faClipboard } from '../../../../client/
import { getTranslationUrl } from '../../../../common/adminUtils';
@Component({
selector: 'events-table',
templateUrl: 'events-table.pug',
styleUrls: ['events-table.scss'],
selector: 'events-table',
templateUrl: 'events-table.pug',
styleUrls: ['events-table.scss'],
})
export class EventsTable {
readonly clipboardIcon = faClipboard;
readonly langIcon = faLanguage;
readonly trashIcon = faTrash;
readonly commentIcon = faComment;
@Input() events!: Event[];
@Output() showChat = new EventEmitter<ChatEvent>();
@Output() addChat = new EventEmitter<ChatEvent>();
@Output() removedEvent = new EventEmitter<Event>();
constructor(private model: AdminModel) {
}
serverLabel(e: Event) {
return SERVER_LABELS[e.server] || 'badge-none';
}
remove(e: Event) {
this.model.removeEvent(e._id);
this.removedEvent.emit(e);
}
removeAll(e: Event) {
this.model.events
.filter(x => x.message === e.message)
.forEach(x => this.model.removeEvent(x._id));
}
copyToNotes(e: Event, account: Account | undefined) {
if (account) {
const desc = e.desc ? `: ${e.desc}` : '';
const count = e.count > 1 ? `[${e.count}] ` : '';
const note = `${(account.note || '')}\r\n[${e.server}]${count}${e.message}${desc}`;
this.model.setNote(account._id, note.trim());
}
}
translateUrl(e: Event) {
return getTranslationUrl(e.desc);
}
onShowChat(e: MouseEvent, event: Event, account: Account | undefined) {
if (e.shiftKey) {
this.addChat.emit({ event, account });
} else {
this.showChat.emit({ event, account });
}
}
onAddChat(event: Event, account: Account | undefined) {
this.addChat.emit({ event, account });
}
readonly clipboardIcon = faClipboard;
readonly langIcon = faLanguage;
readonly trashIcon = faTrash;
readonly commentIcon = faComment;
@Input() events!: Event[];
@Output() showChat = new EventEmitter<ChatEvent>();
@Output() addChat = new EventEmitter<ChatEvent>();
@Output() removedEvent = new EventEmitter<Event>();
constructor(private model: AdminModel) {
}
serverLabel(e: Event) {
return SERVER_LABELS[e.server] || 'badge-none';
}
remove(e: Event) {
this.model.removeEvent(e._id);
this.removedEvent.emit(e);
}
removeAll(e: Event) {
this.model.events
.filter(x => x.message === e.message)
.forEach(x => this.model.removeEvent(x._id));
}
copyToNotes(e: Event, account: Account | undefined) {
if (account) {
const desc = e.desc ? `: ${e.desc}` : '';
const count = e.count > 1 ? `[${e.count}] ` : '';
const note = `${(account.note || '')}\r\n[${e.server}]${count}${e.message}${desc}`;
this.model.setNote(account._id, note.trim());
}
}
translateUrl(e: Event) {
return getTranslationUrl(e.desc);
}
onShowChat(e: MouseEvent, event: Event, account: Account | undefined) {
if (e.shiftKey) {
this.addChat.emit({ event, account });
} else {
this.showChat.emit({ event, account });
}
}
onAddChat(event: Event, account: Account | undefined) {
this.addChat.emit({ event, account });
}
}
+27 -27
View File
@@ -3,34 +3,34 @@ import * as moment from 'moment';
import { IntervalUpdateService } from '../../services/intervalUpdateService';
@Component({
selector: 'from-now',
template: '<span></span>',
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'from-now',
template: '<span></span>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FromNow implements OnInit, OnDestroy, OnChanges {
@Input() time?: any;
private moment?: moment.Moment;
private text?: string;
private unsubscribe?: () => void;
constructor(private element: ElementRef, private updateService: IntervalUpdateService) {
}
ngOnChanges() {
this.moment = this.time ? moment(this.time) : undefined;
this.update();
}
ngOnInit() {
this.unsubscribe = this.updateService.subscribe(() => this.update());
this.update();
}
ngOnDestroy() {
this.unsubscribe && this.unsubscribe();
}
private update() {
const text = this.moment ? this.moment.fromNow(true).replace('seconds', 'secs') : '';
@Input() time?: any;
private moment?: moment.Moment;
private text?: string;
private unsubscribe?: () => void;
constructor(private element: ElementRef, private updateService: IntervalUpdateService) {
}
ngOnChanges() {
this.moment = this.time ? moment(this.time) : undefined;
this.update();
}
ngOnInit() {
this.unsubscribe = this.updateService.subscribe(() => this.update());
this.update();
}
ngOnDestroy() {
this.unsubscribe && this.unsubscribe();
}
private update() {
const text = this.moment ? this.moment.fromNow(true).replace('seconds', 'secs') : '';
if (this.text !== text) {
this.text = text;
(this.element.nativeElement as HTMLElement).children[0].textContent = text;
}
}
if (this.text !== text) {
this.text = text;
(this.element.nativeElement as HTMLElement).children[0].textContent = text;
}
}
}
@@ -1,20 +1,20 @@
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'on-off-switch',
templateUrl: 'on-off-switch.pug',
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'on-off-switch',
templateUrl: 'on-off-switch.pug',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OnOffSwitch {
@Input() on = false;
@Input() disabled = false;
@Input() onText = 'ON';
@Input() offText = 'OFF';
@Input() label = '';
@Output() toggle = new EventEmitter<boolean>();
onToggle(value: boolean) {
if (value !== this.on) {
this.toggle.emit(value);
}
}
@Input() on = false;
@Input() disabled = false;
@Input() onText = 'ON';
@Input() offText = 'OFF';
@Input() label = '';
@Output() toggle = new EventEmitter<boolean>();
onToggle(value: boolean) {
if (value !== this.on) {
this.toggle.emit(value);
}
}
}
@@ -4,28 +4,28 @@ import { AdminModel } from '../../../services/adminModel';
import { Origin } from '../../../../common/adminInterfaces';
@Component({
selector: 'origin-info-remote',
templateUrl: 'origin-info-remote.pug',
selector: 'origin-info-remote',
templateUrl: 'origin-info-remote.pug',
})
export class OriginInfoRemote implements OnDestroy {
@Input() showName = false;
origin?: Origin;
private _originIP?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get originIP() {
return this._originIP;
}
@Input() set originIP(value: string | undefined) {
if (this.originIP !== value) {
this._originIP = value;
this.origin = undefined;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.origins.subscribe(value, origin => this.origin = origin) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
@Input() showName = false;
origin?: Origin;
private _originIP?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get originIP() {
return this._originIP;
}
@Input() set originIP(value: string | undefined) {
if (this.originIP !== value) {
this._originIP = value;
this.origin = undefined;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.origins.subscribe(value, origin => this.origin = origin) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
}
@@ -4,20 +4,20 @@ import { AdminModel } from '../../../services/adminModel';
import { countryCodeToName } from '../../../../common/countries';
@Component({
selector: 'origin-info',
templateUrl: 'origin-info.pug',
styleUrls: ['origin-info.scss'],
selector: 'origin-info',
templateUrl: 'origin-info.pug',
styleUrls: ['origin-info.scss'],
})
export class OriginInfo {
@Input() origin?: Origin;
constructor(private model: AdminModel) {
}
get countryName() {
return countryCodeToName[this.origin && this.origin.country || '??'] || 'Unknown';
}
toggleBan(field: keyof BannedMuted, value: number) {
if (this.origin) {
this.model.updateOrigin({ ip: this.origin.ip, country: this.origin.country, [field]: value });
}
}
@Input() origin?: Origin;
constructor(private model: AdminModel) {
}
get countryName() {
return countryCodeToName[this.origin && this.origin.country || '??'] || 'Unknown';
}
toggleBan(field: keyof BannedMuted, value: number) {
if (this.origin) {
this.model.updateOrigin({ ip: this.origin.ip, country: this.origin.country, [field]: value });
}
}
}
@@ -4,29 +4,29 @@ import { AdminModel } from '../../../services/adminModel';
import { OriginInfoBase } from '../../../../common/adminInterfaces';
@Component({
selector: 'origin-list-remote',
templateUrl: 'origin-list-remote.pug',
selector: 'origin-list-remote',
templateUrl: 'origin-list-remote.pug',
})
export class OriginListRemote implements OnDestroy {
@Input() limit = 2;
@Input() extended = false;
origins: OriginInfoBase[] = [];
private _accountId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get accountId() {
return this._accountId;
}
@Input() set accountId(value: string | undefined) {
if (this.accountId !== value) {
this._accountId = value;
this.origins = [];
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.accountOrigins.subscribe(value, x => this.origins = x || []) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
@Input() limit = 2;
@Input() extended = false;
origins: OriginInfoBase[] = [];
private _accountId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get accountId() {
return this._accountId;
}
@Input() set accountId(value: string | undefined) {
if (this.accountId !== value) {
this._accountId = value;
this.origins = [];
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.accountOrigins.subscribe(value, x => this.origins = x || []) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
}
@@ -4,29 +4,29 @@ import { AdminModel } from '../../../services/adminModel';
import { Character } from '../../../../common/adminInterfaces';
@Component({
selector: 'pony-info-remote',
templateUrl: 'pony-info-remote.pug',
selector: 'pony-info-remote',
templateUrl: 'pony-info-remote.pug',
})
export class PonyInfoRemote implements OnDestroy {
@Input() highlight = false;
@Input() showName = false;
pony?: Character;
private _ponyId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get ponyId() {
return this._ponyId;
}
@Input() set ponyId(value: string | undefined) {
if (this.ponyId !== value) {
this._ponyId = value;
this.pony = undefined;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.ponies.subscribe(value, pony => this.pony = pony) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
@Input() highlight = false;
@Input() showName = false;
pony?: Character;
private _ponyId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get ponyId() {
return this._ponyId;
}
@Input() set ponyId(value: string | undefined) {
if (this.ponyId !== value) {
this._ponyId = value;
this.pony = undefined;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.ponies.subscribe(value, pony => this.pony = pony) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
}
@@ -5,38 +5,38 @@ import { isForbiddenName } from '../../../../common/security';
import { AdminModel } from '../../../services/adminModel';
@Component({
selector: 'pony-info',
templateUrl: 'pony-info.pug',
styleUrls: ['pony-info.scss'],
selector: 'pony-info',
templateUrl: 'pony-info.pug',
styleUrls: ['pony-info.scss'],
})
export class PonyInfo implements OnChanges {
@Input() pony?: Character;
@Input() highlight = false;
labelClass = 'badge-none';
private promise?: Promise<void>;
constructor(private model: AdminModel) {
}
get isBadCM() {
return !!this.pony && hasFlag(this.pony.flags, CharacterFlags.BadCM);
}
ngOnChanges() {
if (this.pony) {
if (isForbiddenName(this.pony.name)) {
this.labelClass = 'badge-forbidden';
} else if (hasFlag(this.pony.flags, CharacterFlags.BadCM)) {
this.labelClass = 'badge-danger';
} else {
this.labelClass = 'badge-none';
}
}
}
onShown() {
if (this.pony && !this.pony.ponyInfo && !this.promise) {
this.promise = this.model.getPonyInfo(this.pony)
.finally(() => this.promise = undefined);
}
}
click() {
console.log(this.pony);
}
@Input() pony?: Character;
@Input() highlight = false;
labelClass = 'badge-none';
private promise?: Promise<void>;
constructor(private model: AdminModel) {
}
get isBadCM() {
return !!this.pony && hasFlag(this.pony.flags, CharacterFlags.BadCM);
}
ngOnChanges() {
if (this.pony) {
if (isForbiddenName(this.pony.name)) {
this.labelClass = 'badge-forbidden';
} else if (hasFlag(this.pony.flags, CharacterFlags.BadCM)) {
this.labelClass = 'badge-danger';
} else {
this.labelClass = 'badge-none';
}
}
}
onShown() {
if (this.pony && !this.pony.ponyInfo && !this.promise) {
this.promise = this.model.getPonyInfo(this.pony)
.finally(() => this.promise = undefined);
}
}
click() {
console.log(this.pony);
}
}
@@ -5,71 +5,71 @@ import { faTrash, faArrowRight } from '../../../../client/icons';
import { Character, PonyIdDateName, DuplicateResult } from '../../../../common/adminInterfaces';
@Component({
selector: 'pony-list-remote',
templateUrl: 'pony-list-remote.pug',
styleUrls: ['pony-list-remote.scss'],
selector: 'pony-list-remote',
templateUrl: 'pony-list-remote.pug',
styleUrls: ['pony-list-remote.scss'],
})
export class PonyListRemote implements OnDestroy {
readonly trashIcon = faTrash;
readonly assignIcon = faArrowRight;
@Input() limit = 10;
@Input() expanded = false;
@Input() deletable = false;
@Input() highlight: (pony: Character) => boolean = () => false;
@Input() duplicates?: DuplicateResult[];
full = false;
ponies: string[] = [];
loading = false;
private ponyInfos: PonyIdDateName[] = [];
private _accountId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get limitTo() {
return this.full ? 999999 : this.limit;
}
get accountId() {
return this._accountId;
}
@Input() set accountId(value: string | undefined) {
if (this.accountId !== value) {
this._accountId = value;
this.ponies = [];
this.ponyInfos = [];
this.loading = true;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.accountPonies.subscribe(value, (x = []) => {
this.ponyInfos = x;
this.updatePonies();
this.loading = false;
}) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
remove(characterId: string) {
if (confirm('Are you sure?')) {
this.model.removePony(characterId);
}
}
toggleFull() {
this.full = !this.full;
this.updatePonies();
}
assignTo(pony: string, account: string) {
this.model.assignPony(pony, account);
}
private updatePonies() {
const compare = this.full ? compareNames : compareDates;
this.ponies = this.ponyInfos.sort(compare).map(p => p.id);
}
readonly trashIcon = faTrash;
readonly assignIcon = faArrowRight;
@Input() limit = 10;
@Input() expanded = false;
@Input() deletable = false;
@Input() highlight: (pony: Character) => boolean = () => false;
@Input() duplicates?: DuplicateResult[];
full = false;
ponies: string[] = [];
loading = false;
private ponyInfos: PonyIdDateName[] = [];
private _accountId?: string;
private subscription?: Subscription;
constructor(private model: AdminModel) {
}
get limitTo() {
return this.full ? 999999 : this.limit;
}
get accountId() {
return this._accountId;
}
@Input() set accountId(value: string | undefined) {
if (this.accountId !== value) {
this._accountId = value;
this.ponies = [];
this.ponyInfos = [];
this.loading = true;
this.subscription && this.subscription.unsubscribe();
this.subscription = value ? this.model.accountPonies.subscribe(value, (x = []) => {
this.ponyInfos = x;
this.updatePonies();
this.loading = false;
}) : undefined;
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
remove(characterId: string) {
if (confirm('Are you sure?')) {
this.model.removePony(characterId);
}
}
toggleFull() {
this.full = !this.full;
this.updatePonies();
}
assignTo(pony: string, account: string) {
this.model.assignPony(pony, account);
}
private updatePonies() {
const compare = this.full ? compareNames : compareDates;
this.ponies = this.ponyInfos.sort(compare).map(p => p.id);
}
}
export function compareNames(a: { name: string }, b: { name: string }) {
return a.name.localeCompare(b.name);
return a.name.localeCompare(b.name);
}
export function compareDates(a: { date: number }, b: { date: number }) {
return b.date - a.date;
return b.date - a.date;
}
@@ -1,10 +1,10 @@
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'time-field',
templateUrl: 'time-field.pug',
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'time-field',
templateUrl: 'time-field.pug',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TimeField {
@Input() time: any;
@Input() time: any;
}
@@ -3,43 +3,43 @@ import { UAParser } from 'ua-parser-js';
import { uaIcons, faQuestionCircle, faGlobe, faDesktop } from '../../../../client/icons';
function icon(value: string | undefined, defaultValue: any): any {
return value && uaIcons[value] || defaultValue;
return value && uaIcons[value] || defaultValue;
}
const extensions = {
browser: [
[/(Amigo|YaBrowser)\/([\w\.]+)/i], [UAParser.BROWSER.NAME, UAParser.BROWSER.VERSION]
],
browser: [
[/(Amigo|YaBrowser)\/([\w\.]+)/i], [UAParser.BROWSER.NAME, UAParser.BROWSER.VERSION]
],
};
@Component({
selector: 'ua-info',
templateUrl: 'ua-info.pug',
styles: [`:host { display: inline-block; }`],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'ua-info',
templateUrl: 'ua-info.pug',
styles: [`:host { display: inline-block; }`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UAInfo {
osClass?: string;
osVersion?: string;
browserClass?: string;
browserVersion?: string;
deviceClass?: string;
private _userAgent?: string;
@Input() set userAgent(value: string | undefined) {
if (this._userAgent !== value) {
this._userAgent = value;
osClass?: string;
osVersion?: string;
browserClass?: string;
browserVersion?: string;
deviceClass?: string;
private _userAgent?: string;
@Input() set userAgent(value: string | undefined) {
if (this._userAgent !== value) {
this._userAgent = value;
const parser = new UAParser(value, extensions);
const { os, browser, device } = parser.getResult();
const parser = new UAParser(value, extensions);
const { os, browser, device } = parser.getResult();
this.osVersion = os.version;
this.browserVersion = (browser.version || '').replace(/\..*$/, '');
this.osClass = icon(os.name, faQuestionCircle);
this.browserClass = icon(browser.name, faGlobe);
this.deviceClass = icon(device.type, faDesktop);
}
}
get userAgent() {
return this._userAgent;
}
this.osVersion = os.version;
this.browserVersion = (browser.version || '').replace(/\..*$/, '');
this.osClass = icon(os.name, faQuestionCircle);
this.browserClass = icon(browser.name, faGlobe);
this.deviceClass = icon(device.type, faDesktop);
}
}
get userAgent() {
return this._userAgent;
}
}