Compare commits
23 Commits
b1bac758e3
...
master
Author | SHA1 | Date | |
---|---|---|---|
9338e7e624 | |||
daa500111c | |||
b0f9a2dea8 | |||
931046cbb3 | |||
01c62bc143 | |||
f4511157a5 | |||
f2c5178e82 | |||
7048a7c46c | |||
0a511f1424 | |||
b8a92534d9 | |||
3e9a9f9dc5 | |||
70e0e9bf71 | |||
b465f0a474 | |||
fcf1e9ac03 | |||
1e6690ff4b | |||
5489eb4df6 | |||
e053529d49 | |||
d69fc68ec1 | |||
d011571164 | |||
055885837c | |||
d44ec50a6a | |||
ea19375ad2 | |||
298d351e5d |
@ -47,8 +47,8 @@
|
|||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "1024kB",
|
"maximumWarning": "3MB",
|
||||||
"maximumError": "1MB"
|
"maximumError": "5MB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
@ -91,7 +91,10 @@
|
|||||||
"buildTarget": "hermes-web-angular:build:development"
|
"buildTarget": "hermes-web-angular:build:development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development",
|
||||||
|
"options": {
|
||||||
|
"allowedHosts": ["beta.tomtospeech.com"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||||
|
2993
package-lock.json
generated
2993
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@ -2,29 +2,29 @@
|
|||||||
"name": "hermes-web-angular",
|
"name": "hermes-web-angular",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"start": "ng serve -c development --host 0.0.0.0 --watch false",
|
||||||
"start": "ng serve -c production --host 0.0.0.0 --watch false",
|
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng serve -c development --host 0.0.0.0",
|
"watch": "ng serve -c development --host 0.0.0.0 --disable-host-check",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"serve:ssr:hermes-web-angular": "node dist/hermes-web-angular/server/server.mjs"
|
"serve:ssr:hermes-web-angular": "node dist/hermes-web-angular/server/server.mjs"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^19.0.5",
|
"@angular/animations": "^19.2.4",
|
||||||
"@angular/cdk": "^19.0.4",
|
"@angular/cdk": "^19.2.7",
|
||||||
"@angular/common": "^19.0.5",
|
"@angular/common": "^19.2.4",
|
||||||
"@angular/compiler": "^19.0.5",
|
"@angular/compiler": "^19.2.4",
|
||||||
"@angular/core": "^19.0.5",
|
"@angular/core": "^19.2.4",
|
||||||
"@angular/forms": "^19.0.5",
|
"@angular/forms": "^19.2.4",
|
||||||
"@angular/material": "^19.0.4",
|
"@angular/material": "^19.2.7",
|
||||||
"@angular/platform-browser": "^19.0.5",
|
"@angular/platform-browser": "^19.2.4",
|
||||||
"@angular/platform-browser-dynamic": "^19.0.5",
|
"@angular/platform-browser-dynamic": "^19.2.4",
|
||||||
"@angular/platform-server": "^19.0.5",
|
"@angular/platform-server": "^19.2.4",
|
||||||
"@angular/router": "^19.0.5",
|
"@angular/router": "^19.2.4",
|
||||||
"@angular/ssr": "^19.0.6",
|
"@angular/ssr": "^19.2.5",
|
||||||
"angular-oauth2-oidc": "^17.0.2",
|
"angular-oauth2-oidc": "^17.0.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"ngx-socket-io": "^4.7.0",
|
"ngx-socket-io": "^4.7.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"rxjs-websockets": "^9.0.0",
|
"rxjs-websockets": "^9.0.0",
|
||||||
@ -33,9 +33,9 @@
|
|||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^19.0.6",
|
"@angular-devkit/build-angular": "^19.2.5",
|
||||||
"@angular/cli": "^19.0.6",
|
"@angular/cli": "^19.2.5",
|
||||||
"@angular/compiler-cli": "^19.0.5",
|
"@angular/compiler-cli": "^19.2.4",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@types/node": "^18.18.0",
|
"@types/node": "^18.18.0",
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { Component, EventEmitter, inject, input, Input, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -21,20 +20,9 @@ export class ActionDropdownComponent implements OnInit {
|
|||||||
@Input() action: string | undefined;
|
@Input() action: string | undefined;
|
||||||
@Output() readonly actionChange = new EventEmitter<string>();
|
@Output() readonly actionChange = new EventEmitter<string>();
|
||||||
|
|
||||||
private readonly route = inject(ActivatedRoute);
|
|
||||||
|
|
||||||
errorMessageKeys: string[] = []
|
errorMessageKeys: string[] = []
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.route.data.subscribe(data => {
|
|
||||||
if (!data['redeemableActions'])
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.actions = data['redeemableActions'];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.errorMessageKeys = Object.keys(this.errorMessages);
|
this.errorMessageKeys = Object.keys(this.errorMessages);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<body>
|
<content>
|
||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title-group>
|
<mat-card-title-group>
|
||||||
@ -108,18 +108,26 @@
|
|||||||
}
|
}
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
|
||||||
<mat-card-actions class="actions"
|
<mat-card-actions class="actions">
|
||||||
align="end">
|
|
||||||
@if (!isNew) {
|
@if (!isNew) {
|
||||||
<button mat-raised-button
|
<button mat-button
|
||||||
class="delete"
|
class="danger"
|
||||||
(click)="deleteAction(action)">Delete</button>
|
(click)="deleteAction(action)">
|
||||||
|
<mat-icon>delete</mat-icon>Delete
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
<button mat-raised-button
|
<button mat-button
|
||||||
(click)="dialogRef.close()">Cancel</button>
|
class="neutral"
|
||||||
<button mat-raised-button
|
disabled="{{waitForResponse}}"
|
||||||
|
(click)="dialogRef.close()">
|
||||||
|
<mat-icon>cancel</mat-icon>Cancel
|
||||||
|
</button>
|
||||||
|
<button mat-button
|
||||||
|
class="confirm"
|
||||||
disabled="{{!formsDirty || !formsValidity || waitForResponse}}"
|
disabled="{{!formsDirty || !formsValidity || waitForResponse}}"
|
||||||
(click)="save()">Save</button>
|
(click)="save()">
|
||||||
|
<mat-icon>save</mat-icon>Save
|
||||||
|
</button>
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
|
|
||||||
@if (responseError) {
|
@if (responseError) {
|
||||||
@ -128,4 +136,4 @@
|
|||||||
</mat-card-footer>
|
</mat-card-footer>
|
||||||
}
|
}
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</body>
|
</content>
|
@ -9,12 +9,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete {
|
.delete {
|
||||||
background-color: #ea5151;
|
background-color: #ea5151;
|
||||||
color: #ba1a1a;
|
color: #ba1a1a;
|
||||||
|
@ -9,27 +9,31 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { createItemExistsInArrayValidator } from '../../shared/validators/item-exists-in-array';
|
import { createItemExistsInArrayValidator } from '../../shared/validators/item-exists-in-array';
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'action-item-edit',
|
selector: 'action-item-edit',
|
||||||
imports: [
|
imports: [
|
||||||
ReactiveFormsModule,
|
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
|
MatIconModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatSelectModule
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
],
|
],
|
||||||
templateUrl: './action-item-edit.component.html',
|
templateUrl: './action-item-edit.component.html',
|
||||||
styleUrl: './action-item-edit.component.scss'
|
styleUrl: './action-item-edit.component.scss'
|
||||||
})
|
})
|
||||||
export class ActionItemEditComponent implements OnInit {
|
export class ActionItemEditComponent implements OnInit {
|
||||||
private readonly client = inject(HermesClientService);
|
private readonly client = inject(HermesClientService);
|
||||||
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
|
|
||||||
private readonly data = inject<{ action: RedeemableAction, actions: RedeemableAction[] }>(MAT_DIALOG_DATA);
|
private readonly data = inject<{ action: RedeemableAction, actions: RedeemableAction[] }>(MAT_DIALOG_DATA);
|
||||||
|
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
|
||||||
|
|
||||||
action = this.data.action;
|
action = this.data.action;
|
||||||
actions = this.data.actions;
|
actions = this.data.actions;
|
||||||
|
|
||||||
readonly actionEntries: ({ [key: string]: any[] }) = {
|
readonly actionEntries: ({ [key: string]: any[] }) = {
|
||||||
'SLEEP': [
|
'SLEEP': [
|
||||||
{
|
{
|
||||||
@ -257,7 +261,7 @@ export class ActionItemEditComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
if (this.formGroup.invalid || this.waitForResponse) {
|
if (!this.formGroup.dirty || this.formGroup.invalid || this.waitForResponse) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,18 +274,18 @@ export class ActionItemEditComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.action.name = this.formGroup.get('name')!.value!;
|
|
||||||
this.action.type = this.formGroup.get('type')!.value!;
|
|
||||||
this.action.data = {}
|
|
||||||
for (const entry of this.actionEntries[this.action.type]) {
|
|
||||||
this.action.data[entry.key] = entry.control.value!.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(this.action.type in this.actionEntries)) {
|
if (!(this.action.type in this.actionEntries)) {
|
||||||
this.waitForResponse = false;
|
this.waitForResponse = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.action.name = this.formGroup.get('name')!.value!;
|
||||||
|
this.action.type = this.formGroup.get('type')!.value!;
|
||||||
|
this.action.data = {};
|
||||||
|
for (const entry of this.actionEntries[this.action.type]) {
|
||||||
|
this.action.data[entry.key] = entry.control.value!.toString();
|
||||||
|
}
|
||||||
|
|
||||||
const isNewAction = !this.action.user_id;
|
const isNewAction = !this.action.user_id;
|
||||||
const requestType = isNewAction ? 'create_redeemable_action' : 'update_redeemable_action';
|
const requestType = isNewAction ? 'create_redeemable_action' : 'update_redeemable_action';
|
||||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == requestType && d.d.data.name == this.action.name)
|
this.client.first((d: any) => d.op == 4 && d.d.request.type == requestType && d.d.data.name == this.action.name)
|
||||||
@ -297,8 +301,8 @@ export class ActionItemEditComponent implements OnInit {
|
|||||||
complete: () => this.waitForResponse = false,
|
complete: () => this.waitForResponse = false,
|
||||||
});
|
});
|
||||||
if (isNewAction)
|
if (isNewAction)
|
||||||
this.client.createRedeemableAction(this.action.name, this.action.type, this.action.data);
|
this.client.createRedeemableAction(this.action.name, this.action.type, false, this.action.data);
|
||||||
else
|
else
|
||||||
this.client.updateRedeemableAction(this.action.name, this.action.type, this.action.data);
|
this.client.updateRedeemableAction(this.action.name, this.action.type, false, this.action.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<main>
|
<main>
|
||||||
@for (action of actions; track action.name) {
|
@for (action of actions(); track action.name) {
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="container"
|
class="container"
|
||||||
(click)="modify(action)">
|
(click)="modify(action)">
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@use '@angular/material' as mat;
|
||||||
|
|
||||||
main {
|
main {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(1, 1fr);
|
grid-template-columns: repeat(1, 1fr);
|
||||||
@ -15,7 +17,7 @@ main {
|
|||||||
border: 1px solid grey;
|
border: 1px solid grey;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: white;
|
background-color: transparent;
|
||||||
|
|
||||||
& span {
|
& span {
|
||||||
display: block;
|
display: block;
|
||||||
@ -27,7 +29,6 @@ main {
|
|||||||
|
|
||||||
& .subtitle {
|
& .subtitle {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
color: lightgrey;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
import { Component, inject, input } from '@angular/core';
|
||||||
import { MatListModule } from '@angular/material/list';
|
import { MatListModule } from '@angular/material/list';
|
||||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@ -7,23 +7,30 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { ActionItemEditComponent } from '../action-item-edit/action-item-edit.component';
|
import { ActionItemEditComponent } from '../action-item-edit/action-item-edit.component';
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'action-list',
|
selector: 'action-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatButtonModule, MatFormFieldModule, MatIconModule, MatListModule],
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatListModule,
|
||||||
|
MatSelectModule,
|
||||||
|
],
|
||||||
templateUrl: './action-list.component.html',
|
templateUrl: './action-list.component.html',
|
||||||
styleUrl: './action-list.component.scss'
|
styleUrl: './action-list.component.scss'
|
||||||
})
|
})
|
||||||
export class ActionListComponent {
|
export class ActionListComponent {
|
||||||
@Input() actions: RedeemableAction[] = []
|
actions = input.required<RedeemableAction[]>({ alias: 'actions' });
|
||||||
@Output() actionsChange = new EventEmitter<RedeemableAction>();
|
|
||||||
readonly dialog = inject(MatDialog);
|
readonly dialog = inject(MatDialog);
|
||||||
readonly client = inject(HermesClientService);
|
readonly client = inject(HermesClientService);
|
||||||
opened = false;
|
|
||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
this.openDialog({ user_id: '', name: '', type: '', data: {} });
|
this.openDialog({ user_id: '', name: '', type: '', has_message: false, data: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
modify(action: RedeemableAction): void {
|
modify(action: RedeemableAction): void {
|
||||||
@ -31,27 +38,8 @@ export class ActionListComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private openDialog(action: RedeemableAction): void {
|
private openDialog(action: RedeemableAction): void {
|
||||||
if (this.opened)
|
this.dialog.open(ActionItemEditComponent, {
|
||||||
return;
|
data: { action: { user_id: action.user_id, name: action.name, type: action.type, data: action.data }, actions: this.actions() },
|
||||||
|
|
||||||
this.opened = true;
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(ActionItemEditComponent, {
|
|
||||||
data: { action: { user_id: action.user_id, name: action.name, type: action.type, data: action.data }, actions: this.actions },
|
|
||||||
});
|
|
||||||
|
|
||||||
const isNewAction = action.name.length <= 0;
|
|
||||||
dialogRef.afterClosed().subscribe((result: RedeemableAction | undefined) => {
|
|
||||||
this.opened = false;
|
|
||||||
if (!result)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (isNewAction) {
|
|
||||||
this.actionsChange.emit(result);
|
|
||||||
} else {
|
|
||||||
action.type = result.type;
|
|
||||||
action.data = result.data;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<body>
|
<content>
|
||||||
<h3>Redeemable Actions</h3>
|
<h3>Redeemable Actions</h3>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<article>
|
<article>
|
||||||
<mat-form-field>
|
<mat-form-field subscriptSizing="dynamic">
|
||||||
<mat-label>Filter by type</mat-label>
|
<mat-label>Filter by type</mat-label>
|
||||||
<mat-select (selectionChange)="onFilterChange($event.value)"
|
<mat-select value="0"
|
||||||
value="0">
|
(selectionChange)="filter = filters[$event.value]">
|
||||||
<mat-select-trigger>
|
<mat-select-trigger>
|
||||||
<mat-icon matPrefix>filter_list</mat-icon> {{filter.name}}
|
<mat-icon matPrefix>filter_list</mat-icon> {{filter.name}}
|
||||||
</mat-select-trigger>
|
</mat-select-trigger>
|
||||||
@ -17,18 +17,16 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<mat-form-field>
|
<mat-form-field subscriptSizing="dynamic">
|
||||||
<mat-label>Search</mat-label>
|
<mat-label>Search</mat-label>
|
||||||
<input matInput
|
<input matInput
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name of action"
|
placeholder="Name of action"
|
||||||
[formControl]="searchControl"
|
[formControl]="searchControl" />
|
||||||
[(ngModel)]="search">
|
|
||||||
<mat-icon matPrefix>search</mat-icon>
|
<mat-icon matPrefix>search</mat-icon>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
<action-list class="center"
|
<action-list class="list center"
|
||||||
[actions]="actions"
|
[actions]="actions" />
|
||||||
(actionsChange)="items.push($event)" />
|
</content>
|
||||||
</body>
|
|
@ -1,12 +1,12 @@
|
|||||||
body,
|
content,
|
||||||
h3 {
|
h3 {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
content {
|
||||||
height: 100vh;
|
display: flex;
|
||||||
overflow: auto;
|
flex-direction: column;
|
||||||
margin-top: 3em;
|
margin-top: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,8 +19,7 @@ section {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 70%;
|
width: 70%;
|
||||||
margin-left: auto;
|
margin: 0 auto;
|
||||||
margin-right: auto;
|
|
||||||
|
|
||||||
@media (max-width:1250px) {
|
@media (max-width:1250px) {
|
||||||
display: block;
|
display: block;
|
||||||
@ -30,6 +29,7 @@ section {
|
|||||||
article {
|
article {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, inject, OnInit } from '@angular/core';
|
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActionListComponent } from "../action-list/action-list.component";
|
import { ActionListComponent } from "../action-list/action-list.component";
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@ -9,6 +9,9 @@ import RedeemableAction from '../../shared/models/redeemable-action';
|
|||||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import RedeemableActionService from '../../shared/services/redeemable-action.service';
|
import RedeemableActionService from '../../shared/services/redeemable-action.service';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { containsLettersInOrder } from '../../shared/utils/string-compare';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
interface IActionFilter {
|
interface IActionFilter {
|
||||||
name: string
|
name: string
|
||||||
@ -17,19 +20,20 @@ interface IActionFilter {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'actions',
|
selector: 'actions',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
ActionListComponent,
|
ActionListComponent,
|
||||||
ReactiveFormsModule,
|
MatButtonModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatSelectModule
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
],
|
],
|
||||||
templateUrl: './actions.component.html',
|
templateUrl: './actions.component.html',
|
||||||
styleUrl: './actions.component.scss'
|
styleUrl: './actions.component.scss'
|
||||||
})
|
})
|
||||||
export class ActionsComponent implements OnInit {
|
export class ActionsComponent implements OnInit, OnDestroy {
|
||||||
filters: IActionFilter[] = [
|
filters: IActionFilter[] = [
|
||||||
{ name: 'All', filter: _ => true },
|
{ name: 'All', filter: _ => true },
|
||||||
{ name: 'Local File', filter: data => data.type.includes('_FILE') },
|
{ name: 'Local File', filter: data => data.type.includes('_FILE') },
|
||||||
@ -43,55 +47,29 @@ export class ActionsComponent implements OnInit {
|
|||||||
private readonly client = inject(HermesClientService);
|
private readonly client = inject(HermesClientService);
|
||||||
private readonly redeemableActionService = inject(RedeemableActionService);
|
private readonly redeemableActionService = inject(RedeemableActionService);
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
|
||||||
|
private readonly subscriptions: (Subscription | undefined)[] = [];
|
||||||
|
|
||||||
filter = this.filters[0];
|
filter = this.filters[0];
|
||||||
searchControl = new FormControl('');
|
searchControl = new FormControl<string>('');
|
||||||
search = '';
|
_actions: RedeemableAction[] = [];
|
||||||
items: RedeemableAction[] = [];
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.data.subscribe(data => {
|
this.route.data.subscribe(data => this._actions = data['redeemableActions'] || []);
|
||||||
if (!data['redeemableActions'])
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.actions = [...data['redeemableActions']];
|
this.subscriptions.push(this.redeemableActionService.changes$?.subscribe(a => this._actions = a));
|
||||||
});
|
|
||||||
this.redeemableActionService.create$?.subscribe(d => {
|
|
||||||
if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.actions.push(d.data);
|
|
||||||
});
|
|
||||||
this.redeemableActionService.update$?.subscribe(d => {
|
|
||||||
if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const action = this.actions.find(a => a.name == d.data.name);
|
|
||||||
if (action) {
|
|
||||||
action.type = d.data.type;
|
|
||||||
action.data = d.data.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.redeemableActionService.delete$?.subscribe(d => {
|
|
||||||
if (d.error)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.items = this.actions.filter(a => a.name != d.request.data.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.fetchRedeemableActions();
|
this.client.fetchRedeemableActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.filter(s => s).forEach(s => s?.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
get actions(): RedeemableAction[] {
|
get actions(): RedeemableAction[] {
|
||||||
const searchLower = this.search.toLowerCase();
|
const searchLower = this.searchControl.value!.toLowerCase();
|
||||||
return this.items.filter(this.filter.filter)
|
return this._actions.filter(this.filter.filter)
|
||||||
.filter((action) => action.name.toLowerCase().includes(searchLower));
|
.filter((action) => containsLettersInOrder(action.name.toLowerCase(), searchLower));
|
||||||
}
|
|
||||||
|
|
||||||
set actions(value) {
|
|
||||||
this.items = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilterChange(event: any): void {
|
|
||||||
this.filter = this.filters[event];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
<main class="main">
|
<main>
|
||||||
<navigation class="navigation" />
|
<topbar class="top" />
|
||||||
|
<div class="below-topbar"
|
||||||
|
[class.grid]="isSidebarOpen"
|
||||||
|
[class.full]="!isSidebarOpen">
|
||||||
|
@if (isSidebarOpen) {
|
||||||
|
<sidebar class="navigation" />
|
||||||
|
}
|
||||||
<router-outlet class="content" />
|
<router-outlet class="content" />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
@ -1,4 +1,8 @@
|
|||||||
.main {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20em 0px 1fr;
|
grid-template-columns: 20em 0px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
@ -1,61 +1,95 @@
|
|||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { Component, OnInit, Inject, PLATFORM_ID, NgZone, OnDestroy, inject } from '@angular/core';
|
import { Component, OnInit, Inject, PLATFORM_ID, NgZone, OnDestroy, inject, HostBinding } from '@angular/core';
|
||||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
import { Router, RouterOutlet } from '@angular/router';
|
||||||
import { HermesClientService } from './hermes-client.service';
|
import { HermesClientService } from './hermes-client.service';
|
||||||
import { AuthUserGuard } from './shared/auth/auth.user.guard'
|
import { AuthUserGuard } from './shared/auth/auth.user.guard'
|
||||||
import { first, Subscription, timeout } from 'rxjs';
|
import { first, Subscription, timeout } from 'rxjs';
|
||||||
import { NavigationComponent } from "./navigation/navigation.component";
|
|
||||||
import EventService from './shared/services/EventService';
|
import EventService from './shared/services/EventService';
|
||||||
import { ApiAuthenticationService } from './shared/services/api/api-authentication.service';
|
import { ApiAuthenticationService } from './shared/services/api/api-authentication.service';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { ApiKeyService } from './shared/services/api/api-key.service';
|
import { ApiKeyService } from './shared/services/api/api-key.service';
|
||||||
|
import { ThemeService } from './shared/services/theme.service';
|
||||||
|
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { SidebarComponent } from "./navigation/sidebar/sidebar.component";
|
||||||
|
import { Topbar as TopbarComponent } from "./navigation/topbar/topbar.component";
|
||||||
import ApiKey from './shared/models/api-key';
|
import ApiKey from './shared/models/api-key';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterOutlet, AuthModule, NavigationComponent],
|
imports: [
|
||||||
|
AuthModule,
|
||||||
|
RouterOutlet,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
SidebarComponent,
|
||||||
|
TopbarComponent,
|
||||||
|
],
|
||||||
providers: [AuthUserGuard],
|
providers: [AuthUserGuard],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss'
|
styleUrl: './app.component.scss'
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
private readonly keyService = inject(ApiKeyService);
|
private readonly keyService = inject(ApiKeyService);
|
||||||
|
private readonly overlayContainer = inject(OverlayContainer);
|
||||||
|
private readonly themeService = inject(ThemeService);
|
||||||
|
|
||||||
private isBrowser: boolean;
|
|
||||||
private ngZone: NgZone;
|
private ngZone: NgZone;
|
||||||
private subscriptions: Subscription[];
|
private subscriptions: Subscription[];
|
||||||
|
|
||||||
|
authentication = inject(ApiAuthenticationService);
|
||||||
|
isSidebarOpen: boolean = true
|
||||||
|
|
||||||
|
@HostBinding('class.dark-theme')
|
||||||
|
get isDarkTheme() {
|
||||||
|
return this.themeService.isDarkTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostBinding('class.light-theme')
|
||||||
|
get isLightTheme() {
|
||||||
|
return this.themeService.isLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private auth: ApiAuthenticationService, private client: HermesClientService, private events: EventService, private router: Router, ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) {
|
constructor(private auth: ApiAuthenticationService, private client: HermesClientService, private events: EventService, private router: Router, ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||||
this.ngZone = ngZone;
|
this.ngZone = ngZone;
|
||||||
this.isBrowser = isPlatformBrowser(this.platformId);
|
|
||||||
this.subscriptions = [];
|
this.subscriptions = [];
|
||||||
|
|
||||||
this.subscriptions.push(this.events.listen('tts_login_ack', async _ => {
|
this.subscriptions.push(this.events.listen('tts_login_ack', async _ => {
|
||||||
const url = router.url;
|
const url = router.url;
|
||||||
const params = router.parseUrl(url).queryParams;
|
const params = router.parseUrl(url).queryParams;
|
||||||
|
const redirect = params['rd'];
|
||||||
|
|
||||||
if (params && 'rd' in params) {
|
if (redirect && !(url.startsWith(redirect) || redirect.startsWith(url))) {
|
||||||
await this.router.navigate([params['rd']]);
|
await this.router.navigate([redirect]);
|
||||||
} else if (url == '/' || url.startsWith('/login') || url.startsWith('/tts-login')) {
|
} else if (url == '/' || url.startsWith('/login') || url.startsWith('/tts-login')) {
|
||||||
await this.router.navigate(['policies']);
|
await this.router.navigate(['policies']);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
this.subscriptions.push(this.events.listen('tts_logoff', async _ => {
|
|
||||||
await this.router.navigate(['tts-login'], {
|
this.addSubscription(this.events.listen('login', () => {
|
||||||
queryParams: {
|
this.keyService.fetch()
|
||||||
rd: this.router.url.substring(1)
|
.pipe(timeout(3000), first())
|
||||||
}
|
.subscribe(async (d: ApiKey[]) => {
|
||||||
|
if (d.length > 0)
|
||||||
|
this.client.login(d[0].id);
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.subscriptions.push(this.events.listen('tts_logoff', async _ => await this.router.navigate(['tts-login'])));
|
||||||
|
this.subscriptions.push(this.events.listen('toggle_sidebar', () => this.isSidebarOpen = !this.isSidebarOpen))
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!this.isBrowser)
|
if (!isPlatformBrowser(this.platformId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.auth.update();
|
this.auth.update(localStorage.getItem('jwt'));
|
||||||
|
|
||||||
|
this.subscriptions.push(this.events.listen('login', async () => await this.router.navigate(['tts-login'])));
|
||||||
|
|
||||||
this.addSubscription(this.events.listen('logoff', async (message) => {
|
this.addSubscription(this.events.listen('logoff', async (message) => {
|
||||||
localStorage.removeItem('jwt');
|
localStorage.removeItem('jwt');
|
||||||
@ -69,15 +103,18 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.addSubscription(this.events.listen('login', () => {
|
let currentTheme = localStorage.getItem('ui-theme') ?? this.themeService.theme;
|
||||||
this.keyService.fetch(true)
|
if (currentTheme == 'light' || currentTheme == 'dark') {
|
||||||
.pipe(timeout(3000), first())
|
this.themeService.theme = currentTheme;
|
||||||
.subscribe(async (d: ApiKey[]) => {
|
} else {
|
||||||
if (d.length > 0)
|
this.themeService.theme = 'dark';
|
||||||
this.client.login(d[0].id);
|
}
|
||||||
else if (['/login', '/auth'].some(partial => document.location.href.includes(partial)))
|
this.overlayContainer.getContainerElement().classList.add(this.themeService.theme + '-theme');
|
||||||
await this.router.navigate(['tts-login']);
|
|
||||||
});
|
this.addSubscription(this.events.listen('theme_change', data => {
|
||||||
|
const classList = this.overlayContainer.getContainerElement().classList;
|
||||||
|
classList.remove(data.previous_theme + '-theme');
|
||||||
|
classList.add(data.current_theme + '-theme');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.ngZone.runOutsideAngular(() => setInterval(() => this.client.heartbeat(), 15000));
|
this.ngZone.runOutsideAngular(() => setInterval(() => this.client.heartbeat(), 15000));
|
||||||
@ -85,9 +122,11 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
for (let s of this.subscriptions) {
|
for (let s of this.subscriptions) {
|
||||||
|
if (s) {
|
||||||
s.unsubscribe();
|
s.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private addSubscription(s: Subscription) {
|
private addSubscription(s: Subscription) {
|
||||||
this.subscriptions.push(s);
|
this.subscriptions.push(s);
|
||||||
|
@ -17,6 +17,6 @@ export const appConfig: ApplicationConfig = {
|
|||||||
}])
|
}])
|
||||||
),
|
),
|
||||||
provideOAuthClient(),
|
provideOAuthClient(),
|
||||||
provideClientHydration(), provideAnimationsAsync()
|
provideClientHydration(), provideAnimationsAsync(), provideAnimationsAsync()
|
||||||
]
|
]
|
||||||
};
|
};
|
13
src/app/app.module.ts
Normal file
13
src/app/app.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { TtsFiltersModule } from './tts-filters/tts-filters.module';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
|||||||
import { PolicyComponent } from './policies/policy/policy.component';
|
import { PolicyComponent } from './policies/policy/policy.component';
|
||||||
import { AuthUserGuard } from './shared/auth/auth.user.guard';
|
import { AuthUserGuard } from './shared/auth/auth.user.guard';
|
||||||
import { LoginComponent } from './auth/login/login.component';
|
import { LoginComponent } from './auth/login/login.component';
|
||||||
import { TtsLoginComponent } from './auth/tts-login/tts-login.component';
|
|
||||||
import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component';
|
import { TwitchAuthCallbackComponent } from './twitch-auth-callback/twitch-auth-callback.component';
|
||||||
import { FiltersComponent } from './tts-filters/filters/filters.component';
|
import { FiltersComponent } from './tts-filters/filters/filters.component';
|
||||||
import { AuthAdminGuard } from './shared/auth/auth.admin.guard';
|
import { AuthAdminGuard } from './shared/auth/auth.admin.guard';
|
||||||
@ -23,6 +22,8 @@ import PermissionResolver from './shared/resolvers/permission-resolver';
|
|||||||
import { ConnectionsComponent } from './connections/connections/connections.component';
|
import { ConnectionsComponent } from './connections/connections/connections.component';
|
||||||
import ConnectionResolver from './shared/resolvers/connection-resolver';
|
import ConnectionResolver from './shared/resolvers/connection-resolver';
|
||||||
import { ConnectionCallbackComponent } from './connections/callback/callback.component';
|
import { ConnectionCallbackComponent } from './connections/callback/callback.component';
|
||||||
|
import { KeysComponent } from './keys/keys/keys.component';
|
||||||
|
import { TtsLoginComponent } from './auth/tts-login/tts-login.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -80,6 +81,14 @@ export const routes: Routes = [
|
|||||||
permissions: PermissionResolver,
|
permissions: PermissionResolver,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'keys',
|
||||||
|
component: KeysComponent,
|
||||||
|
canActivate: [AuthUserGuard],
|
||||||
|
resolve: {
|
||||||
|
keys: ApiKeyResolver,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
component: LoginComponent,
|
component: LoginComponent,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { LoginComponent } from './login/login.component';
|
import { LoginComponent } from './login/login.component';
|
||||||
import { TtsLoginComponent } from './tts-login/tts-login.component';
|
|
||||||
import { ImpersonationComponent } from './impersonation/impersonation.component';
|
import { ImpersonationComponent } from './impersonation/impersonation.component';
|
||||||
import { UserCardComponent } from './user-card/user-card.component';
|
import { UserCardComponent } from './user-card/user-card.component';
|
||||||
|
|
||||||
@ -8,7 +7,6 @@ import { UserCardComponent } from './user-card/user-card.component';
|
|||||||
declarations: [],
|
declarations: [],
|
||||||
imports: [
|
imports: [
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
TtsLoginComponent,
|
|
||||||
ImpersonationComponent,
|
ImpersonationComponent,
|
||||||
UserCardComponent,
|
UserCardComponent,
|
||||||
]
|
]
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
@if (isAdmin()) {
|
@if (isAdmin()) {
|
||||||
<main>
|
<mat-form-field class="mat-small"
|
||||||
<mat-form-field>
|
subscriptSizing="dynamic">
|
||||||
<mat-label>User to impersonate</mat-label>
|
<mat-label>User to impersonate</mat-label>
|
||||||
<mat-select (selectionChange)="onChange($event)"
|
<mat-select [formControl]="impersonationControl">
|
||||||
[(value)]="impersonated">
|
<mat-option [value]="auth.getUserId()">{{getUsername()}}</mat-option>
|
||||||
<mat-option>{{getUsername()}}</mat-option>
|
@for (user of (users$ | async | excludeById : auth.getUserId()); track user.id) {
|
||||||
@for (user of users; track user.id) {
|
|
||||||
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</main>
|
|
||||||
}
|
}
|
@ -2,5 +2,4 @@ main {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 1em;
|
|
||||||
}
|
}
|
@ -1,63 +1,80 @@
|
|||||||
import { Component, inject, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import EventService from '../../shared/services/EventService';
|
import EventService from '../../shared/services/EventService';
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
import { Router } from '@angular/router';
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { timeout, first } from 'rxjs';
|
import { UserService } from '../../shared/services/user.service';
|
||||||
import ApiKey from '../../shared/models/api-key';
|
import { AsyncPipe } from '@angular/common';
|
||||||
import { ApiKeyService } from '../../shared/services/api/api-key.service';
|
import { ExcludeByIdPipe } from '../../shared/pipes/exclude-by-id.pipe';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'impersonation',
|
selector: 'impersonation',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatCardModule, MatSelectModule],
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
ExcludeByIdPipe,
|
||||||
|
MatCardModule,
|
||||||
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
templateUrl: './impersonation.component.html',
|
templateUrl: './impersonation.component.html',
|
||||||
styleUrl: './impersonation.component.scss'
|
styleUrl: './impersonation.component.scss'
|
||||||
})
|
})
|
||||||
export class ImpersonationComponent implements OnInit {
|
export class ImpersonationComponent implements OnInit {
|
||||||
private readonly keyService = inject(ApiKeyService);
|
private readonly client = inject(HermesClientService);
|
||||||
|
private readonly userService = inject(UserService);
|
||||||
|
private readonly events = inject(EventService);
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
impersonated: string | undefined;
|
readonly auth = inject(ApiAuthenticationService);
|
||||||
users: { id: string, name: string }[];
|
|
||||||
|
|
||||||
constructor(private client: HermesClientService, private auth: ApiAuthenticationService, private router: Router, private events: EventService, private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
|
impersonationControl = new FormControl<string>(this.auth.getUserId());
|
||||||
this.users = []
|
users$ = this.userService.fetch();
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!isPlatformBrowser(this.platformId) || !this.auth.isAdmin()) {
|
if (!this.auth.isAdmin()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.http.get(environment.API_HOST + '/admin/users', {
|
this.users$.subscribe(users => {
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
|
||||||
}
|
|
||||||
}).subscribe((data: any) => {
|
|
||||||
this.users = data.filter((d: any) => d.name != this.auth.getUsername());
|
|
||||||
const id = this.auth.getImpersonatedId();
|
const id = this.auth.getImpersonatedId();
|
||||||
if (id && this.users.find(u => u.id == id)) {
|
if (id && users.find(u => u.id == id)) {
|
||||||
this.impersonated = id;
|
this.impersonationControl.setValue(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.events.listen('impersonation', (userId) => {
|
this.impersonationControl.valueChanges.subscribe((impersonationId) => {
|
||||||
const url = this.router.url;
|
if (impersonationId == this.auth.getImpersonatedId())
|
||||||
this.client.first(d => d.op == 2 && !d.d.another_client)
|
return;
|
||||||
.subscribe(async _ =>
|
|
||||||
await setTimeout(async () =>
|
if (impersonationId == this.auth.getUserId()) {
|
||||||
await this.router.navigate([url.substring(1)]), 500));
|
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
||||||
this.keyService.fetch(true)
|
headers: {
|
||||||
.pipe(timeout(3000), first())
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||||
.subscribe(async (d: ApiKey[]) => {
|
},
|
||||||
if (d.length > 0)
|
body: {
|
||||||
this.client.login(d[0].id);
|
impersonation: impersonationId
|
||||||
|
}
|
||||||
|
}).subscribe(async (data: any) => {
|
||||||
|
this.client.disconnect(true);
|
||||||
|
this.events.emit('impersonation', impersonationId);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.http.put(environment.API_HOST + '/admin/impersonate', {
|
||||||
|
impersonation: impersonationId
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||||
|
}
|
||||||
|
}).subscribe(async (data: any) => {
|
||||||
|
this.client.disconnect(true);
|
||||||
|
this.events.emit('impersonation', impersonationId);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,35 +85,4 @@ export class ImpersonationComponent implements OnInit {
|
|||||||
public getUsername() {
|
public getUsername() {
|
||||||
return this.auth.getUsername();
|
return this.auth.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChange(e: any) {
|
|
||||||
if (!this.auth.isAdmin())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!e.value) {
|
|
||||||
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
impersonation: e.value
|
|
||||||
}
|
|
||||||
}).subscribe(async (data: any) => {
|
|
||||||
this.client.disconnect();
|
|
||||||
this.events.emit('impersonation', e.value);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.http.put(environment.API_HOST + '/admin/impersonate', {
|
|
||||||
impersonation: e.value
|
|
||||||
}, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
|
||||||
}
|
|
||||||
}).subscribe(async (data: any) => {
|
|
||||||
this.client.disconnect();
|
|
||||||
this.events.emit('impersonation', e.value);
|
|
||||||
await this.router.navigate(['tts-login']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
6
src/app/auth/login-button/login-button.component.html
Normal file
6
src/app/auth/login-button/login-button.component.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<button mat-icon-button
|
||||||
|
class="neutral"
|
||||||
|
matTooltip="Navigate to the log in page"
|
||||||
|
(click)="login()">
|
||||||
|
<mat-icon>login</mat-icon>
|
||||||
|
</button>
|
23
src/app/auth/login-button/login-button.component.spec.ts
Normal file
23
src/app/auth/login-button/login-button.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginButtonComponent } from './login-button.component';
|
||||||
|
|
||||||
|
describe('LoginButtonComponent', () => {
|
||||||
|
let component: LoginButtonComponent;
|
||||||
|
let fixture: ComponentFixture<LoginButtonComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LoginButtonComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoginButtonComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
28
src/app/auth/login-button/login-button.component.ts
Normal file
28
src/app/auth/login-button/login-button.component.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'login-button',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
|
templateUrl: './login-button.component.html',
|
||||||
|
styleUrl: './login-button.component.scss'
|
||||||
|
})
|
||||||
|
export class LoginButtonComponent {
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
login() {
|
||||||
|
this.router.navigate(['login']);
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
.mat-mdc-card-content {
|
.mat-mdc-card-content {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -1,32 +1,15 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { Router, RouterModule } from '@angular/router';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'login',
|
selector: 'login',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatCardModule, RouterModule],
|
imports: [MatCardModule],
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrl: './login.component.scss'
|
styleUrl: './login.component.scss'
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit, OnDestroy {
|
export class LoginComponent {
|
||||||
subscription: Subscription | null;
|
|
||||||
|
|
||||||
constructor(private router: Router) {
|
|
||||||
this.subscription = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
if (this.subscription)
|
|
||||||
this.subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
login() {
|
login() {
|
||||||
document.location.replace(environment.API_HOST + '/auth');
|
document.location.replace(environment.API_HOST + '/auth');
|
||||||
}
|
}
|
||||||
|
6
src/app/auth/logoff-button/logoff-button.component.html
Normal file
6
src/app/auth/logoff-button/logoff-button.component.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<button mat-icon-button
|
||||||
|
class="danger"
|
||||||
|
matTooltip="Log off"
|
||||||
|
(click)="logoff()">
|
||||||
|
<mat-icon>logout</mat-icon>
|
||||||
|
</button>
|
23
src/app/auth/logoff-button/logoff-button.component.spec.ts
Normal file
23
src/app/auth/logoff-button/logoff-button.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LogoffButtonComponent } from './logoff-button.component';
|
||||||
|
|
||||||
|
describe('LogoffButtonComponent', () => {
|
||||||
|
let component: LogoffButtonComponent;
|
||||||
|
let fixture: ComponentFixture<LogoffButtonComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LogoffButtonComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LogoffButtonComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
28
src/app/auth/logoff-button/logoff-button.component.ts
Normal file
28
src/app/auth/logoff-button/logoff-button.component.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'logoff-button',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
|
templateUrl: './logoff-button.component.html',
|
||||||
|
styleUrl: './logoff-button.component.scss'
|
||||||
|
})
|
||||||
|
export class LogoffButtonComponent {
|
||||||
|
private readonly auth = inject(ApiAuthenticationService);
|
||||||
|
|
||||||
|
logoff() {
|
||||||
|
this.auth.logout();
|
||||||
|
}
|
||||||
|
}
|
@ -9,15 +9,16 @@
|
|||||||
<mat-card-content class="content">
|
<mat-card-content class="content">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>API Key</mat-label>
|
<mat-label>API Key</mat-label>
|
||||||
<mat-select [(value)]="selected_api_key">
|
<mat-select [formControl]="keyControl">
|
||||||
@for (key of api_keys; track key.id) {
|
@for (key of api_keys; track key.id) {
|
||||||
<mat-option [value]="key.id">{{key.label}}</mat-option>
|
<mat-option [value]="key.id">{{key.label}}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
<mat-card-actions align="end">
|
<mat-card-actions>
|
||||||
<button mat-raised-button
|
<button mat-raised-button
|
||||||
|
[disabled]="disabled"
|
||||||
(click)="login()">Log In</button>
|
(click)="login()">Log In</button>
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -1,58 +1,71 @@
|
|||||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import EventService from '../../shared/services/EventService';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { first, Subscription, timeout } from 'rxjs';
|
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import EventService from '../../shared/services/EventService';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
import { ApiKeyService } from '../../shared/services/api/api-key.service';
|
import { ApiKeyService } from '../../shared/services/api/api-key.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tts-login',
|
selector: 'tts-login',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatButtonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatInputModule, FormsModule],
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
templateUrl: './tts-login.component.html',
|
templateUrl: './tts-login.component.html',
|
||||||
styleUrl: './tts-login.component.scss'
|
styleUrl: './tts-login.component.scss'
|
||||||
})
|
})
|
||||||
export class TtsLoginComponent implements OnInit, OnDestroy {
|
export class TtsLoginComponent implements OnInit, OnDestroy {
|
||||||
private readonly client = inject(HermesClientService);
|
private readonly client = inject(HermesClientService);
|
||||||
private readonly keyService = inject(ApiKeyService);
|
|
||||||
private readonly events = inject(EventService);
|
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly keyService = inject(ApiKeyService);
|
||||||
|
private readonly eventService = inject(EventService);
|
||||||
|
|
||||||
|
keyControl = new FormControl<string | null>('');
|
||||||
api_keys: { id: string, label: string }[] = [];
|
api_keys: { id: string, label: string }[] = [];
|
||||||
selected_api_key: string | undefined;
|
subscriptions: (Subscription | null)[] = [];
|
||||||
|
disabled: boolean = false;
|
||||||
private subscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.data.subscribe(d => this.api_keys = d['keys']);
|
this.route.data.subscribe(d => this.api_keys = d['keys']);
|
||||||
|
|
||||||
this.subscriptions.push(this.events.listen('tts_logoff', async _ => {
|
this.subscriptions.push(this.eventService.listen('impersonation', _ => this.reset()));
|
||||||
this.selected_api_key = undefined;
|
this.subscriptions.push(this.eventService.listen('logoff', impersonation => {
|
||||||
}));
|
if (!impersonation)
|
||||||
this.subscriptions.push(this.events.listen('impersonation', _ => {
|
this.reset();
|
||||||
this.selected_api_key = undefined;
|
|
||||||
|
|
||||||
this.keyService.fetch(true)
|
|
||||||
.pipe(timeout(3000), first())
|
|
||||||
.subscribe(d => this.api_keys = d);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subscriptions.forEach(s => s.unsubscribe());
|
for (let subscription of this.subscriptions) {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
login(): void {
|
login(): void {
|
||||||
if (!this.selected_api_key)
|
if (!this.keyControl.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.client.login(this.selected_api_key);
|
this.client.login(this.keyControl.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private reset() {
|
||||||
|
this.disabled = true;
|
||||||
|
this.api_keys = [];
|
||||||
|
this.keyService.fetch().subscribe(keys => {
|
||||||
|
this.api_keys = keys;
|
||||||
|
this.disabled = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,7 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<impersonation />
|
<impersonation />
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
<mat-card-actions class="actions"
|
<mat-card-actions class="actions">
|
||||||
align="end">
|
|
||||||
<div>
|
<div>
|
||||||
@if (isTTSLoggedIn) {
|
@if (isTTSLoggedIn) {
|
||||||
<button mat-raised-button
|
<button mat-raised-button
|
||||||
|
@ -2,6 +2,7 @@ import { Component, inject, OnInit } from '@angular/core';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'connection-callback',
|
selector: 'connection-callback',
|
||||||
@ -34,14 +35,17 @@ export class ConnectionCallbackComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.http.get(`https://beta.tomtospeech.com/api/auth/connections?token=${params['access_token']}&state=${params['state']}&expires_in=${params['expires_in']}`).subscribe(async (d: any) => {
|
this.http.get(`${environment.API_HOST}/auth/connections?token=${params['access_token']}&state=${params['state']}&expires_in=${params['expires_in']}`).subscribe({
|
||||||
|
next: async (d: any) => {
|
||||||
const data = d.data;
|
const data = d.data;
|
||||||
this.success = true;
|
this.success = true;
|
||||||
|
|
||||||
await setTimeout(async () => {
|
await setTimeout(async () => {
|
||||||
this.client.createConnection(data.connection.name, data.connection.type, data.connection.clientId, params['access_token'], data.connection.grantType, params['scope'], data.expires_at);
|
this.client.createConnection(data.connection.name, data.connection.type, data.connection.clientId, params['access_token'], data.connection.grantType, params['scope'], data.expires_at);
|
||||||
await this.router.navigate(['connections']);
|
await this.router.navigate(['connections'])
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
},
|
||||||
|
error: async () => await this.router.navigate(['connections'])
|
||||||
});
|
});
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
@ -41,14 +41,14 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
|
||||||
<mat-card-actions class="actions"
|
<mat-card-actions class="actions">
|
||||||
align="end">
|
<button mat-button
|
||||||
<button mat-raised-button
|
class="neutral"
|
||||||
class="warning"
|
disabled="{{waitForResponse}}"
|
||||||
(click)="dialogRef.close()">Cancel</button>
|
(click)="dialogRef.close()">Cancel</button>
|
||||||
<button mat-raised-button
|
<button mat-button
|
||||||
class="confirm"
|
class="confirm"
|
||||||
disabled="{{form.invalid || waitForResponse}}"
|
disabled="{{!form.dirty || form.invalid || waitForResponse}}"
|
||||||
(click)="submit()">Add</button>
|
(click)="submit()">Add</button>
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
|
|
||||||
|
@ -7,10 +7,9 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { ActionItemEditComponent } from '../../actions/action-item-edit/action-item-edit.component';
|
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'connection-item-edit',
|
selector: 'connection-item-edit',
|
||||||
@ -27,10 +26,10 @@ import { DOCUMENT } from '@angular/common';
|
|||||||
styleUrl: './connection-item-edit.component.scss'
|
styleUrl: './connection-item-edit.component.scss'
|
||||||
})
|
})
|
||||||
export class ConnectionItemEditComponent {
|
export class ConnectionItemEditComponent {
|
||||||
private readonly client = inject(HermesClientService);
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly data = inject<{ name: string }>(MAT_DIALOG_DATA);
|
||||||
|
readonly dialogRef = inject(MatDialogRef<ConnectionItemEditComponent>);
|
||||||
|
|
||||||
readonly data = inject<{ name: string }>(MAT_DIALOG_DATA);
|
|
||||||
readonly nameControl = new FormControl<string>('', [Validators.required]);
|
readonly nameControl = new FormControl<string>('', [Validators.required]);
|
||||||
readonly clientIdControl = new FormControl<string>('', [Validators.required]);
|
readonly clientIdControl = new FormControl<string>('', [Validators.required]);
|
||||||
readonly typeControl = new FormControl<string>('', [Validators.required]);
|
readonly typeControl = new FormControl<string>('', [Validators.required]);
|
||||||
@ -40,7 +39,6 @@ export class ConnectionItemEditComponent {
|
|||||||
type: this.typeControl,
|
type: this.typeControl,
|
||||||
});
|
});
|
||||||
|
|
||||||
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
|
|
||||||
|
|
||||||
responseError: string | undefined;
|
responseError: string | undefined;
|
||||||
waitForResponse = false;
|
waitForResponse = false;
|
||||||
@ -53,11 +51,11 @@ export class ConnectionItemEditComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit(): void {
|
submit(): void {
|
||||||
if (this.form.invalid || this.waitForResponse) {
|
if (!this.form.dirty || this.form.invalid || this.waitForResponse) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.http.post('/api/auth/connections', {
|
this.http.post(environment.API_HOST + '/auth/connections', {
|
||||||
name: this.nameControl.value,
|
name: this.nameControl.value,
|
||||||
type: this.typeControl.value,
|
type: this.typeControl.value,
|
||||||
client_id: this.clientIdControl.value,
|
client_id: this.clientIdControl.value,
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
<section [class.twitch]="connection().type == 'twitch'"
|
<section [class.twitch]="connection().type == 'twitch'"
|
||||||
[class.spotify]="connection().type == 'spotify'">
|
[class.nightbot]="connection().type == 'nightbot'">
|
||||||
{{connection().name}}
|
{{connection().name}}
|
||||||
|
|
||||||
|
@if (isExpired) {
|
||||||
|
<mat-icon matTooltip="Connection has expired."
|
||||||
|
class="danger">error</mat-icon>
|
||||||
|
} @else if (isExpiringSoon) {
|
||||||
|
<mat-icon matTooltip="Connection is soon going to expire."
|
||||||
|
class="warning">warning</mat-icon>
|
||||||
|
}
|
||||||
|
|
||||||
<article class="right">
|
<article class="right">
|
||||||
<button mat-button
|
<button mat-button
|
||||||
class="neutral"
|
class="neutral"
|
||||||
(click)="renew(connection())">
|
(click)="renew()">
|
||||||
<mat-icon>refresh</mat-icon>
|
<mat-icon>refresh</mat-icon>
|
||||||
Renew
|
Renew
|
||||||
</button>
|
</button>
|
||||||
<button mat-button
|
<button mat-button
|
||||||
class="danger"
|
class="danger"
|
||||||
(click)="delete(connection())">
|
(click)="delete()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
@ -2,6 +2,14 @@ section {
|
|||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.twitch {
|
||||||
|
border-left: 1em solid #6441A5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nightbot {
|
||||||
|
border-left: 1em solid #3D5D9A;
|
||||||
|
}
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,13 @@ import { Connection } from '../../shared/models/connection';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'connection-item',
|
selector: 'connection-item',
|
||||||
@ -15,26 +17,35 @@ import { HermesClientService } from '../../hermes-client.service';
|
|||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
|
MatTooltipModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
],
|
],
|
||||||
templateUrl: './connection-item.component.html',
|
templateUrl: './connection-item.component.html',
|
||||||
styleUrl: './connection-item.component.scss'
|
styleUrl: './connection-item.component.scss'
|
||||||
})
|
})
|
||||||
export class ConnectionItemComponent {
|
export class ConnectionItemComponent {
|
||||||
router = inject(Router);
|
private readonly http = inject(HttpClient);
|
||||||
http = inject(HttpClient);
|
private readonly client = inject(HermesClientService);
|
||||||
client = inject(HermesClientService);
|
|
||||||
|
|
||||||
connection = input.required<Connection>();
|
connection = input.required<Connection>();
|
||||||
|
|
||||||
constructor(@Inject(DOCUMENT) private document: Document) { }
|
constructor(@Inject(DOCUMENT) private document: Document) { }
|
||||||
|
|
||||||
delete(conn: Connection) {
|
delete() {
|
||||||
this.client.deleteConnection(conn.name);
|
this.client.deleteConnection(this.connection().name);
|
||||||
}
|
}
|
||||||
|
|
||||||
renew(conn: Connection) {
|
get isExpired() {
|
||||||
this.http.post('/api/auth/connections', {
|
return moment(this.connection().expires_at).toDate().getTime() < new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isExpiringSoon() {
|
||||||
|
return moment(this.connection().expires_at).toDate().getTime() < moment.now() + moment.duration(7, 'd').asMilliseconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
renew() {
|
||||||
|
const conn = this.connection();
|
||||||
|
this.http.post(environment.API_HOST + '/auth/connections', {
|
||||||
name: conn.name,
|
name: conn.name,
|
||||||
type: conn.type,
|
type: conn.type,
|
||||||
client_id: conn.client_id,
|
client_id: conn.client_id,
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
@use '@angular/material' as mat;
|
@use '@angular/material' as mat;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
background-color: rgb(202, 68, 255);
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 0 0;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 500px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
@include mat.all-component-densities(-5);
|
@include mat.all-component-densities(-5);
|
||||||
|
|
||||||
@include mat.form-field-overrides((
|
@include mat.form-field-overrides((
|
||||||
@ -8,13 +15,6 @@ ul {
|
|||||||
outlined-focus-label-text-color: rgb(155, 57, 194),
|
outlined-focus-label-text-color: rgb(155, 57, 194),
|
||||||
outlined-focus-outline-color: rgb(155, 57, 194),
|
outlined-focus-outline-color: rgb(155, 57, 194),
|
||||||
));
|
));
|
||||||
|
|
||||||
background-color: rgb(202, 68, 255);
|
|
||||||
border-radius: 15px;
|
|
||||||
margin: 0 0;
|
|
||||||
padding: 0;
|
|
||||||
max-width: 500px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul li {
|
ul li {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
<content>
|
||||||
<h3>Connections</h3>
|
<h3>Connections</h3>
|
||||||
|
|
||||||
<connection-list [connections]="connections" />
|
<connection-list [connections]="connections" />
|
||||||
|
</content>
|
@ -38,13 +38,15 @@
|
|||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
<mat-card-actions align="end">
|
<mat-card-actions>
|
||||||
<button mat-button
|
<button mat-button
|
||||||
|
class="neutral"
|
||||||
[disabled]="waitForResponse"
|
[disabled]="waitForResponse"
|
||||||
(click)="cancel()">
|
(click)="cancel()">
|
||||||
<mat-icon>cancel</mat-icon>Cancel
|
<mat-icon>cancel</mat-icon>Cancel
|
||||||
</button>
|
</button>
|
||||||
<button mat-button
|
<button mat-button
|
||||||
|
class="confirm"
|
||||||
[disabled]="waitForResponse || formGroup.invalid"
|
[disabled]="waitForResponse || formGroup.invalid"
|
||||||
(click)="add()">
|
(click)="add()">
|
||||||
<mat-icon>add</mat-icon>Add
|
<mat-icon>add</mat-icon>Add
|
||||||
|
@ -50,7 +50,7 @@ export class GroupItemEditComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
if (this.formGroup.invalid || this.waitForResponse)
|
if (!this.formGroup.dirty || this.formGroup.invalid || this.waitForResponse)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.waitForResponse = true;
|
this.waitForResponse = true;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
article {
|
article {
|
||||||
background-color: #f0f0f0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
border: grey solid 1px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
|
|
||||||
|
@ -37,7 +37,8 @@
|
|||||||
[groups]="groups"
|
[groups]="groups"
|
||||||
[policies]="policies"
|
[policies]="policies"
|
||||||
[group]="group?.id" />
|
[group]="group?.id" />
|
||||||
<policy-table [policies]="policies" />
|
<policy-table [policies]="policies"
|
||||||
|
[groups]="groups"/>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
<mat-expansion-panel>
|
<mat-expansion-panel>
|
||||||
|
@ -34,7 +34,7 @@ export class HermesClientService {
|
|||||||
return this.listen();
|
return this.listen();
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnect() {
|
public disconnect(impersonated: boolean = false) {
|
||||||
if (!this.connected)
|
if (!this.connected)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ export class HermesClientService {
|
|||||||
this.session_id = undefined;
|
this.session_id = undefined;
|
||||||
this.api_key = undefined;
|
this.api_key = undefined;
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
this.events.emit('tts_logoff', null);
|
this.events.emit('tts_logoff', impersonated);
|
||||||
}
|
}
|
||||||
|
|
||||||
public filter(predicate: (data: any) => boolean): Observable<any> | undefined {
|
public filter(predicate: (data: any) => boolean): Observable<any> | undefined {
|
||||||
@ -156,14 +156,14 @@ export class HermesClientService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public createRedeemableAction(name: string, type: string, d: { [key: string]: any }) {
|
public createRedeemableAction(name: string, type: string, has_message: boolean, d: { [key: string]: any }) {
|
||||||
if (!this.logged_in)
|
if (!this.logged_in)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.send(3, {
|
this.send(3, {
|
||||||
request_id: null,
|
request_id: null,
|
||||||
type: "create_redeemable_action",
|
type: "create_redeemable_action",
|
||||||
data: { name, type, data: d },
|
data: { name, type, has_message, data: d },
|
||||||
nounce: this.session_id,
|
nounce: this.session_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -439,14 +439,14 @@ export class HermesClientService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateRedeemableAction(name: string, type: string, d: { [key: string]: any }) {
|
public updateRedeemableAction(name: string, type: string, has_message: boolean, d: { [key: string]: any }) {
|
||||||
if (!this.logged_in)
|
if (!this.logged_in)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.send(3, {
|
this.send(3, {
|
||||||
request_id: null,
|
request_id: null,
|
||||||
type: "update_redeemable_action",
|
type: "update_redeemable_action",
|
||||||
data: { name, type, data: d },
|
data: { name, type, has_message, data: d },
|
||||||
nounce: this.session_id,
|
nounce: this.session_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { OnInit, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||||
import { catchError, first, timeout } from 'rxjs/operators';
|
import { catchError, first, timeout } from 'rxjs/operators';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
@ -8,13 +8,9 @@ import { EMPTY, Observable, Observer, throwError } from 'rxjs';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class HermesSocketService implements OnInit {
|
export class HermesSocketService {
|
||||||
private socket: WebSocketSubject<any> | undefined = undefined
|
private socket: WebSocketSubject<any> | undefined = undefined
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
public connect(): void {
|
public connect(): void {
|
||||||
if (!this.socket || this.socket.closed) {
|
if (!this.socket || this.socket.closed) {
|
||||||
@ -22,9 +18,10 @@ export class HermesSocketService implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public first(predicate: (data: any) => boolean): Observable<any> {
|
public first<T>(predicate: (data: T) => boolean): Observable<T> {
|
||||||
if (!this.socket || this.socket.closed)
|
if (!this.socket || this.socket.closed) {
|
||||||
return new Observable().pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')));
|
throw new Error('Socket is ' + (this.socket ? 'closed' : 'null') + '.');
|
||||||
|
}
|
||||||
|
|
||||||
return this.socket.pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')), first(predicate));
|
return this.socket.pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')), first(predicate));
|
||||||
}
|
}
|
||||||
@ -43,7 +40,11 @@ export class HermesSocketService implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get$(): Observable<any> | undefined {
|
public get$(): Observable<any> | undefined {
|
||||||
return this.socket?.asObservable().pipe(catchError(_ => EMPTY));
|
if (!this.socket || this.socket.closed) {
|
||||||
|
throw new Error('Socket is ' + (this.socket ? 'closed' : 'null') + '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.socket.asObservable().pipe(catchError(_ => EMPTY));
|
||||||
}
|
}
|
||||||
|
|
||||||
public subscribe(subscriptions: Partial<Observer<any>> | ((value: any) => void)) {
|
public subscribe(subscriptions: Partial<Observer<any>> | ((value: any) => void)) {
|
||||||
|
38
src/app/keys/key-item-edit/key-item-edit.component.html
Normal file
38
src/app/keys/key-item-edit/key-item-edit.component.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<mat-card>
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title-group>
|
||||||
|
<mat-card-title>Add API Key</mat-card-title>
|
||||||
|
<mat-card-subtitle></mat-card-subtitle>
|
||||||
|
</mat-card-title-group>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Key Label</mat-label>
|
||||||
|
<input matInput
|
||||||
|
[formControl]="labelControl" />
|
||||||
|
@if (labelControl.invalid && (labelControl.dirty || labelControl.touched)) {
|
||||||
|
@if (labelControl.hasError('required')) {
|
||||||
|
<small class="error">This field is required.</small>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</mat-card-content>
|
||||||
|
|
||||||
|
<mat-card-actions class="actions">
|
||||||
|
<button mat-button
|
||||||
|
class="neutral"
|
||||||
|
disabled="{{waitForResponse}}"
|
||||||
|
(click)="dialogRef.close()">Cancel</button>
|
||||||
|
<button mat-button
|
||||||
|
class="confirm"
|
||||||
|
disabled="{{!form.dirty || form.invalid || waitForResponse}}"
|
||||||
|
(click)="submit()">Add</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
|
||||||
|
@if (responseError) {
|
||||||
|
<mat-card-footer>
|
||||||
|
<small class="error below">{{responseError}}</small>
|
||||||
|
</mat-card-footer>
|
||||||
|
}
|
||||||
|
</mat-card>
|
23
src/app/keys/key-item-edit/key-item-edit.component.spec.ts
Normal file
23
src/app/keys/key-item-edit/key-item-edit.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KeyItemEditComponent } from './key-item-edit.component';
|
||||||
|
|
||||||
|
describe('KeyItemEditComponent', () => {
|
||||||
|
let component: KeyItemEditComponent;
|
||||||
|
let fixture: ComponentFixture<KeyItemEditComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [KeyItemEditComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(KeyItemEditComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
68
src/app/keys/key-item-edit/key-item-edit.component.ts
Normal file
68
src/app/keys/key-item-edit/key-item-edit.component.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import EventService from '../../shared/services/EventService';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'key-item-edit',
|
||||||
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
|
templateUrl: './key-item-edit.component.html',
|
||||||
|
styleUrl: './key-item-edit.component.scss'
|
||||||
|
})
|
||||||
|
export class KeyItemEditComponent {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly events = inject(EventService);
|
||||||
|
readonly data = inject<{ name: string }>(MAT_DIALOG_DATA);
|
||||||
|
|
||||||
|
readonly labelControl = new FormControl<string>('', [Validators.required]);
|
||||||
|
readonly form = new FormGroup({
|
||||||
|
name: this.labelControl,
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly dialogRef = inject(MatDialogRef<KeyItemEditComponent>);
|
||||||
|
|
||||||
|
responseError: string | undefined;
|
||||||
|
waitForResponse = false;
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.labelControl.setValue(this.data.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(): void {
|
||||||
|
if (!this.form.dirty || this.form.invalid || this.waitForResponse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = this.labelControl.value;
|
||||||
|
this.http.post(environment.API_HOST + '/keys', { label },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||||
|
}
|
||||||
|
}).subscribe(async (d: any) => {
|
||||||
|
this.events.emit('add_api_key', {
|
||||||
|
id: d.key,
|
||||||
|
label: d.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dialogRef.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
20
src/app/keys/key-item/key-item.component.html
Normal file
20
src/app/keys/key-item/key-item.component.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<section>
|
||||||
|
{{(isVisible ? key().id : key().label)}}
|
||||||
|
|
||||||
|
<article class="right">
|
||||||
|
<button mat-button
|
||||||
|
class="neutral"
|
||||||
|
[disabled]="waitForResponse"
|
||||||
|
(click)="isVisible = !isVisible">
|
||||||
|
<mat-icon>{{(isVisible ? "visibility_off" : "visibility")}}</mat-icon>
|
||||||
|
{{(isVisible ? "Hide" : "View")}} Key
|
||||||
|
</button>
|
||||||
|
<button mat-button
|
||||||
|
class="danger"
|
||||||
|
[disabled]="waitForResponse"
|
||||||
|
(click)="delete()">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
|
</section>
|
11
src/app/keys/key-item/key-item.component.scss
Normal file
11
src/app/keys/key-item/key-item.component.scss
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
section {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
23
src/app/keys/key-item/key-item.component.spec.ts
Normal file
23
src/app/keys/key-item/key-item.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KeyItemComponent } from './key-item.component';
|
||||||
|
|
||||||
|
describe('KeyItemComponent', () => {
|
||||||
|
let component: KeyItemComponent;
|
||||||
|
let fixture: ComponentFixture<KeyItemComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [KeyItemComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(KeyItemComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
49
src/app/keys/key-item/key-item.component.ts
Normal file
49
src/app/keys/key-item/key-item.component.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Component, inject, input } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import ApiKey from '../../shared/models/api-key';
|
||||||
|
import EventService from '../../shared/services/EventService';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'key-item',
|
||||||
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
|
templateUrl: './key-item.component.html',
|
||||||
|
styleUrl: './key-item.component.scss'
|
||||||
|
})
|
||||||
|
export class KeyItemComponent {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly events = inject(EventService);
|
||||||
|
|
||||||
|
key = input.required<ApiKey>();
|
||||||
|
isVisible: boolean = false;
|
||||||
|
waitForResponse = false;
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
if (this.waitForResponse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key_id = this.key().id;
|
||||||
|
this.http.delete(environment.API_HOST + '/keys',
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
key: key_id,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||||
|
}
|
||||||
|
}).subscribe(async (d: any) => {
|
||||||
|
this.events.emit('delete_api_key', key_id);
|
||||||
|
this.waitForResponse = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
28
src/app/keys/key-list/key-list.component.html
Normal file
28
src/app/keys/key-list/key-list.component.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<ul>
|
||||||
|
<li class="header">
|
||||||
|
<mat-form-field appearance="outline"
|
||||||
|
subscriptSizing="dynamic">
|
||||||
|
<mat-label>Label Filter</mat-label>
|
||||||
|
<input matInput
|
||||||
|
placeholder="Filter keys by label"
|
||||||
|
[formControl]="searchControl" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="add()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
@for (key of keys; track key.id) {
|
||||||
|
<li>
|
||||||
|
<key-item [key]="key" />
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (!keys.length) {
|
||||||
|
@if (searchControl.value) {
|
||||||
|
<p class="notice">No API keys match the filter.</p>
|
||||||
|
} @else {
|
||||||
|
<p class="notice">No API keys available.</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ul>
|
38
src/app/keys/key-list/key-list.component.scss
Normal file
38
src/app/keys/key-list/key-list.component.scss
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
@use '@angular/material' as mat;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
background-color: rgb(202, 68, 255);
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 0 0;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 600px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@include mat.all-component-densities(-5);
|
||||||
|
|
||||||
|
@include mat.form-field-overrides((
|
||||||
|
outlined-outline-color: rgb(167, 88, 199),
|
||||||
|
outlined-focus-label-text-color: rgb(155, 57, 194),
|
||||||
|
outlined-focus-outline-color: rgb(155, 57, 194),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
background-color: rgb(240, 165, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li.header {
|
||||||
|
background-color: rgb(215, 115, 255);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul .notice {
|
||||||
|
text-align: center;
|
||||||
|
}
|
23
src/app/keys/key-list/key-list.component.spec.ts
Normal file
23
src/app/keys/key-list/key-list.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KeyListComponent } from './key-list.component';
|
||||||
|
|
||||||
|
describe('KeyListComponent', () => {
|
||||||
|
let component: KeyListComponent;
|
||||||
|
let fixture: ComponentFixture<KeyListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [KeyListComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(KeyListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
61
src/app/keys/key-list/key-list.component.ts
Normal file
61
src/app/keys/key-list/key-list.component.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { Component, inject, Input } from '@angular/core';
|
||||||
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { containsLettersInOrder } from '../../shared/utils/string-compare';
|
||||||
|
import { KeyItemComponent } from '../key-item/key-item.component';
|
||||||
|
import { KeyItemEditComponent } from '../key-item-edit/key-item-edit.component';
|
||||||
|
import ApiKey from '../../shared/models/api-key';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'key-list',
|
||||||
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
KeyItemComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './key-list.component.html',
|
||||||
|
styleUrl: './key-list.component.scss'
|
||||||
|
})
|
||||||
|
export class KeyListComponent {
|
||||||
|
private readonly _dialog = inject(MatDialog);
|
||||||
|
|
||||||
|
private _keys: ApiKey[] = [];
|
||||||
|
|
||||||
|
readonly searchControl = new FormControl<string>('');
|
||||||
|
|
||||||
|
opened = false;
|
||||||
|
|
||||||
|
|
||||||
|
get keys() {
|
||||||
|
return this._keys.filter(c => containsLettersInOrder(c.label, this.searchControl.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input({ required: true })
|
||||||
|
set keys(value: ApiKey[]) {
|
||||||
|
this._keys = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
add() {
|
||||||
|
if (this.opened)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.opened = true;
|
||||||
|
|
||||||
|
const dialogRef = this._dialog.open(KeyItemEditComponent, {
|
||||||
|
data: { name: this.searchControl.value },
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((_: any) => this.opened = false);
|
||||||
|
}
|
||||||
|
}
|
12
src/app/keys/keys.module.ts
Normal file
12
src/app/keys/keys.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class KeysModule { }
|
2
src/app/keys/keys/keys.component.html
Normal file
2
src/app/keys/keys/keys.component.html
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<h3>API Keys</h3>
|
||||||
|
<key-list [keys]="keys" />
|
0
src/app/keys/keys/keys.component.scss
Normal file
0
src/app/keys/keys/keys.component.scss
Normal file
23
src/app/keys/keys/keys.component.spec.ts
Normal file
23
src/app/keys/keys/keys.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KeysComponent } from './keys.component';
|
||||||
|
|
||||||
|
describe('KeysComponent', () => {
|
||||||
|
let component: KeysComponent;
|
||||||
|
let fixture: ComponentFixture<KeysComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [KeysComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(KeysComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
39
src/app/keys/keys/keys.component.ts
Normal file
39
src/app/keys/keys/keys.component.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Component, inject, OnDestroy } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { KeyListComponent } from '../key-list/key-list.component';
|
||||||
|
import ApiKey from '../../shared/models/api-key';
|
||||||
|
import EventService from '../../shared/services/EventService';
|
||||||
|
import { ApiKeyService } from '../../shared/services/api/api-key.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'keys',
|
||||||
|
imports: [KeyListComponent],
|
||||||
|
templateUrl: './keys.component.html',
|
||||||
|
styleUrl: './keys.component.scss'
|
||||||
|
})
|
||||||
|
export class KeysComponent implements OnDestroy {
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly events = inject(EventService);
|
||||||
|
private readonly keyService = inject(ApiKeyService);
|
||||||
|
|
||||||
|
subscriptions: (Subscription | undefined)[] = [];
|
||||||
|
keys: ApiKey[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.route.data.subscribe(payload => {
|
||||||
|
this.keys = payload['keys'] ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.subscriptions.push(this.events.listen('add_api_key', _ => this.keyService.fetch().subscribe(keys => this.keys = keys)));
|
||||||
|
this.subscriptions.push(this.events.listen('delete_api_key', _ => this.keyService.fetch().subscribe(keys => this.keys = keys)));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
for (let subscription of this.subscriptions) {
|
||||||
|
if (subscription)
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
$primary_background_color: #EEEEEE;
|
|
||||||
$primary_font_color: #111111;
|
|
||||||
|
|
||||||
$secondary_background_color: #DDDDDD;
|
|
||||||
$secondary_font_color: #333333;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
list-style: none;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 1em;
|
|
||||||
border: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-size: large;
|
|
||||||
text-decoration: none;
|
|
||||||
color: $primary_font_color;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
background-color: #FAFAFA;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.active {
|
|
||||||
background-color: #F5F5F5;
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { HermesClientService } from '../hermes-client.service';
|
|
||||||
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { AuthModule } from '../auth/auth.module';
|
|
||||||
import { UserCardComponent } from "../auth/user-card/user-card.component";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'navigation',
|
|
||||||
standalone: true,
|
|
||||||
imports: [RouterModule, MatCardModule, AuthModule, UserCardComponent],
|
|
||||||
templateUrl: './navigation.component.html',
|
|
||||||
styleUrl: './navigation.component.scss'
|
|
||||||
})
|
|
||||||
export class NavigationComponent {
|
|
||||||
constructor(private auth: ApiAuthenticationService, private hermes: HermesClientService) { }
|
|
||||||
|
|
||||||
isLoggedIn() {
|
|
||||||
return this.auth.isAuthenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
isAdmin() {
|
|
||||||
return this.isLoggedIn() && this.auth.isAdmin()
|
|
||||||
}
|
|
||||||
|
|
||||||
isTTSLoggedIn() {
|
|
||||||
return this.hermes?.logged_in ?? false;
|
|
||||||
}
|
|
||||||
}
|
|
12
src/app/navigation/navigation.module.ts
Normal file
12
src/app/navigation/navigation.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class NavigationModule { }
|
@ -1,7 +1,66 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<user-card class="card" />
|
|
||||||
<ul>
|
<ul>
|
||||||
@if (!isLoggedIn()) {
|
@if (isLoggedIn()) {
|
||||||
|
@if (isTTSLoggedIn()) {
|
||||||
|
<li>
|
||||||
|
<a mat-raised-button
|
||||||
|
routerLink="/policies"
|
||||||
|
routerLinkActive="active">
|
||||||
|
Policies
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a mat-raised-button
|
||||||
|
routerLink="/filters"
|
||||||
|
routerLinkActive="active">
|
||||||
|
Filters
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a mat-raised-button
|
||||||
|
routerLink="/actions"
|
||||||
|
routerLinkActive="active">
|
||||||
|
Actions
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a mat-raised-button
|
||||||
|
routerLink="/redemptions"
|
||||||
|
routerLinkActive="active">
|
||||||
|
Redemptions
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a mat-raised-button
|
||||||
|
routerLink="/groups"
|
||||||
|
routerLinkActive="active">
|
||||||
|
Groups
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a mat-raised-button
|
||||||
|
routerLink="/connections"
|
||||||
|
routerLinkActive="active">
|
||||||
|
Connections
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
} @else {
|
||||||
|
<li>
|
||||||
|
<a mat-raised-button
|
||||||
|
routerLink="/tts-login"
|
||||||
|
routerLinkActive="active">
|
||||||
|
TTS Login
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
<li>
|
||||||
|
<a mat-raised-button
|
||||||
|
routerLink="/keys"
|
||||||
|
routerLinkActive="active">
|
||||||
|
API Keys
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
} @else {
|
||||||
<li>
|
<li>
|
||||||
<a routerLink="/login"
|
<a routerLink="/login"
|
||||||
routerLinkActive="active">
|
routerLinkActive="active">
|
||||||
@ -9,52 +68,5 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@if (isLoggedIn() && !isTTSLoggedIn()) {
|
|
||||||
<li>
|
|
||||||
<a routerLink="/tts-login"
|
|
||||||
routerLinkActive="active">
|
|
||||||
TTS Login
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
@if (isLoggedIn() && isTTSLoggedIn()) {
|
|
||||||
<li>
|
|
||||||
<a routerLink="/policies"
|
|
||||||
routerLinkActive="active">
|
|
||||||
Policies
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a routerLink="/filters"
|
|
||||||
routerLinkActive="active">
|
|
||||||
Filters
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a routerLink="/actions"
|
|
||||||
routerLinkActive="active">
|
|
||||||
Actions
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a routerLink="/redemptions"
|
|
||||||
routerLinkActive="active">
|
|
||||||
Redemptions
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a routerLink="/groups"
|
|
||||||
routerLinkActive="active">
|
|
||||||
Groups
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a routerLink="/connections"
|
|
||||||
routerLinkActive="active">
|
|
||||||
Connections
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
31
src/app/navigation/sidebar/sidebar.component.scss
Normal file
31
src/app/navigation/sidebar/sidebar.component.scss
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
@use "@angular/material" as mat;
|
||||||
|
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding: 1em;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: large;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
@include mat.button-overrides(
|
||||||
|
(
|
||||||
|
protected-container-height: 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
@ -1,18 +1,18 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { NavigationComponent } from './navigation.component';
|
import { SidebarComponent } from './sidebar.component';
|
||||||
|
|
||||||
describe('NavigationComponent', () => {
|
describe('NavigationComponent', () => {
|
||||||
let component: NavigationComponent;
|
let component: SidebarComponent;
|
||||||
let fixture: ComponentFixture<NavigationComponent>;
|
let fixture: ComponentFixture<SidebarComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NavigationComponent]
|
imports: [SidebarComponent]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(NavigationComponent);
|
fixture = TestBed.createComponent(SidebarComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
41
src/app/navigation/sidebar/sidebar.component.ts
Normal file
41
src/app/navigation/sidebar/sidebar.component.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
|
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { AuthModule } from '../../auth/auth.module';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'sidebar',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
AuthModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatSidenavModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
RouterModule,
|
||||||
|
],
|
||||||
|
templateUrl: './sidebar.component.html',
|
||||||
|
styleUrl: './sidebar.component.scss'
|
||||||
|
})
|
||||||
|
export class SidebarComponent {
|
||||||
|
constructor(private auth: ApiAuthenticationService, private hermes: HermesClientService) { }
|
||||||
|
|
||||||
|
isLoggedIn() {
|
||||||
|
return this.auth.isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin() {
|
||||||
|
return this.isLoggedIn() && this.auth.isAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
|
isTTSLoggedIn() {
|
||||||
|
return this.hermes?.logged_in ?? false;
|
||||||
|
}
|
||||||
|
}
|
37
src/app/navigation/topbar/topbar.component.html
Normal file
37
src/app/navigation/topbar/topbar.component.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<mat-toolbar class="top">
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="toggleSidebar()">
|
||||||
|
<mat-icon>menu</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span>Tom-to-Speech</span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
@if (isLoggedIn) {
|
||||||
|
<div class="links">
|
||||||
|
@if (showImpersonation) {
|
||||||
|
<impersonation />
|
||||||
|
} @else {
|
||||||
|
<div class="userInfo">
|
||||||
|
<span class="username">{{impersonatedName ?? username}}</span>
|
||||||
|
@if (impersonatedId) {
|
||||||
|
<br />
|
||||||
|
<span class="impersonated">Impersonating from {{username}}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (isAdminLoggedIn) {
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="showImpersonation = !showImpersonation">
|
||||||
|
<mat-icon>supervisor_account</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<theme />
|
||||||
|
|
||||||
|
@if (isLoggedIn) {
|
||||||
|
<logoff-button />
|
||||||
|
} @else {
|
||||||
|
<login-button />
|
||||||
|
}
|
||||||
|
</mat-toolbar>
|
22
src/app/navigation/topbar/topbar.component.scss
Normal file
22
src/app/navigation/topbar/topbar.component.scss
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links > * {
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfo {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 10px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impersonated {
|
||||||
|
font-size: x-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
impersonation {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
23
src/app/navigation/topbar/topbar.component.spec.ts
Normal file
23
src/app/navigation/topbar/topbar.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TopbarComponent } from './topbar.component';
|
||||||
|
|
||||||
|
describe('TopbarComponent', () => {
|
||||||
|
let component: TopbarComponent;
|
||||||
|
let fixture: ComponentFixture<TopbarComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TopbarComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(TopbarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
84
src/app/navigation/topbar/topbar.component.ts
Normal file
84
src/app/navigation/topbar/topbar.component.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Component, inject, OnDestroy } from '@angular/core';
|
||||||
|
import { AuthVisitorGuard } from '../../shared/auth/auth.visitor.guard';
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { AuthModule } from '../../auth/auth.module';
|
||||||
|
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||||
|
import { ImpersonationComponent } from '../../auth/impersonation/impersonation.component';
|
||||||
|
import EventService from '../../shared/services/EventService';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { ThemeComponent } from "../../theme/theme.component";
|
||||||
|
import { LogoffButtonComponent } from "../../auth/logoff-button/logoff-button.component";
|
||||||
|
import { LoginButtonComponent } from "../../auth/login-button/login-button.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'topbar',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
AuthModule,
|
||||||
|
ImpersonationComponent,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
ThemeComponent,
|
||||||
|
LoginButtonComponent,
|
||||||
|
LogoffButtonComponent,
|
||||||
|
],
|
||||||
|
providers: [AuthVisitorGuard],
|
||||||
|
templateUrl: './topbar.component.html',
|
||||||
|
styleUrl: './topbar.component.scss'
|
||||||
|
})
|
||||||
|
export class Topbar implements OnDestroy {
|
||||||
|
private readonly auth = inject(ApiAuthenticationService);
|
||||||
|
private readonly events = inject(EventService);
|
||||||
|
|
||||||
|
private subscriptions: (Subscription | null)[] = [];
|
||||||
|
private _showImpersonation: boolean = false
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.subscriptions.push(this.events.listen('impersonation', () => { this.auth.update(localStorage.getItem('jwt')); this.showImpersonation = false; }));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
for (let subscription of this.subscriptions) {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLoggedIn() {
|
||||||
|
return this.auth.isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAdminLoggedIn() {
|
||||||
|
return this.auth.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
get username() {
|
||||||
|
return this.auth.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
get impersonatedId() {
|
||||||
|
return this.auth.getImpersonatedId();
|
||||||
|
}
|
||||||
|
|
||||||
|
get impersonatedName() {
|
||||||
|
return this.auth.getImpersonatedName();
|
||||||
|
}
|
||||||
|
|
||||||
|
get showImpersonation() {
|
||||||
|
return this._showImpersonation;
|
||||||
|
}
|
||||||
|
|
||||||
|
set showImpersonation(value: any) {
|
||||||
|
if (this.auth.isAdmin()) {
|
||||||
|
this._showImpersonation = !!value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidebar() {
|
||||||
|
this.events.emit('toggle_sidebar', undefined);
|
||||||
|
}
|
||||||
|
}
|
@ -23,12 +23,19 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
|
||||||
<mat-card-actions class="actions">
|
<mat-card-actions class="actions">
|
||||||
<button mat-raised-button
|
<button mat-button
|
||||||
(click)="dialogRef.close()">Cancel</button>
|
class="neutral"
|
||||||
|
disabled="{{waitForResponse}}"
|
||||||
|
(click)="dialogRef.close()">
|
||||||
|
<mat-icon>cancel</mat-icon>Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
<button mat-raised-button
|
<button mat-button
|
||||||
disabled="{{pathControl.invalid || waitForResponse}}"
|
class="confirm"
|
||||||
(click)="submit()">{{data.isNew ? "Add" : "Save"}}</button>
|
disabled="{{!pathControl.dirty || pathControl.invalid || waitForResponse}}"
|
||||||
|
(click)="submit()">
|
||||||
|
<mat-icon>{{data.isNew ? "add" : "save"}}</mat-icon>{{data.isNew ? "Add" : "Save"}}
|
||||||
|
</button>
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
|
|
||||||
@if (responseError) {
|
@if (responseError) {
|
||||||
|
@ -58,7 +58,7 @@ export class PermissionItemEditComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
if (this.form.invalid || this.waitForResponse) {
|
if (!this.form.dirty || this.form.invalid || this.waitForResponse) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
@use '@angular/material' as mat;
|
@use '@angular/material' as mat;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
background-color: rgb(202, 68, 255);
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 0 0;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 500px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
@include mat.all-component-densities(-5);
|
@include mat.all-component-densities(-5);
|
||||||
|
|
||||||
@include mat.form-field-overrides((
|
@include mat.form-field-overrides((
|
||||||
@ -8,13 +15,6 @@ ul {
|
|||||||
outlined-focus-label-text-color: rgb(155, 57, 194),
|
outlined-focus-label-text-color: rgb(155, 57, 194),
|
||||||
outlined-focus-outline-color: rgb(155, 57, 194),
|
outlined-focus-outline-color: rgb(155, 57, 194),
|
||||||
));
|
));
|
||||||
|
|
||||||
background-color: rgb(202, 68, 255);
|
|
||||||
border-radius: 15px;
|
|
||||||
margin: 0 0;
|
|
||||||
padding: 0;
|
|
||||||
max-width: 500px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul li {
|
ul li {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
import { Component, inject, input } from '@angular/core';
|
||||||
import { Policy } from '../../shared/models/policy';
|
import { Policy } from '../../shared/models/policy';
|
||||||
import { PolicyItemEditComponent } from '../policy-item-edit/policy-item-edit.component';
|
import { PolicyItemEditComponent } from '../policy-item-edit/policy-item-edit.component';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
@ -17,28 +17,21 @@ import { Group } from '../../shared/models/group';
|
|||||||
})
|
})
|
||||||
export class PolicyAddButtonComponent {
|
export class PolicyAddButtonComponent {
|
||||||
private readonly dialog = inject(MatDialog);
|
private readonly dialog = inject(MatDialog);
|
||||||
@Input({ required: true }) policies: Policy[] = [];
|
|
||||||
@Input({ required: true }) groups: Group[] = [];
|
policies = input.required<Policy[]>();
|
||||||
@Input() group: string | undefined = undefined;
|
groups = input.required<Group[]>();
|
||||||
@Output() policy = new EventEmitter<Policy>();
|
group = input<string | undefined>();
|
||||||
|
|
||||||
|
|
||||||
openDialog(): void {
|
openDialog(): void {
|
||||||
const dialogRef = this.dialog.open(PolicyItemEditComponent, {
|
this.dialog.open(PolicyItemEditComponent, {
|
||||||
data: {
|
data: {
|
||||||
policies: this.policies,
|
policies: this.policies(),
|
||||||
groups: this.groups,
|
groups: this.groups(),
|
||||||
group_id: this.group,
|
group_id: this.group(),
|
||||||
groupDisabled: !!this.group,
|
groupDisabled: !!this.group(),
|
||||||
isNew: true,
|
isNew: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((result: Policy) => {
|
|
||||||
if (!result)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.policy.emit(result);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,8 +7,11 @@
|
|||||||
[formControl]="policyControl"
|
[formControl]="policyControl"
|
||||||
[matAutocomplete]="auto" />
|
[matAutocomplete]="auto" />
|
||||||
<mat-autocomplete #auto="matAutocomplete">
|
<mat-autocomplete #auto="matAutocomplete">
|
||||||
@for (option of filteredPolicies | async; track option) {
|
@for (option of filteredPolicies | async; track option.path) {
|
||||||
<mat-option [value]="option">{{option}}</mat-option>
|
<mat-option [value]="option.path">
|
||||||
|
<p class="path">{{option.path}}</p>
|
||||||
|
<p class="description muted">{{option.description}}</p>
|
||||||
|
</mat-option>
|
||||||
}
|
}
|
||||||
</mat-autocomplete>
|
</mat-autocomplete>
|
||||||
@if (policyControl.invalid && (policyControl.dirty || policyControl.touched)) {
|
@if (policyControl.invalid && (policyControl.dirty || policyControl.touched)) {
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: #999999
|
||||||
|
}
|
@ -4,9 +4,14 @@ import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angu
|
|||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { map, Observable, startWith } from 'rxjs';
|
import { EMPTY, map, Observable, startWith } from 'rxjs';
|
||||||
|
|
||||||
const Policies = [
|
interface PolicyData {
|
||||||
|
path: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Policies: PolicyData[] = [
|
||||||
{ path: "tts", description: "Anything to do with TTS" },
|
{ path: "tts", description: "Anything to do with TTS" },
|
||||||
{ path: "tts.chat", description: "Anything to do with chat" },
|
{ path: "tts.chat", description: "Anything to do with chat" },
|
||||||
{ path: "tts.chat.bits.read", description: "To read chat messages with bits via TTS" },
|
{ path: "tts.chat.bits.read", description: "To read chat messages with bits via TTS" },
|
||||||
@ -43,14 +48,7 @@ const Policies = [
|
|||||||
export class PolicyDropdownComponent {
|
export class PolicyDropdownComponent {
|
||||||
@Input() policy: string | null = '';
|
@Input() policy: string | null = '';
|
||||||
@Input({ alias: 'control' }) policyControl = new FormControl('', [Validators.required]);
|
@Input({ alias: 'control' }) policyControl = new FormControl('', [Validators.required]);
|
||||||
filteredPolicies: Observable<string[]>;
|
filteredPolicies: Observable<PolicyData[]> = EMPTY;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.filteredPolicies = this.policyControl.valueChanges.pipe(
|
|
||||||
startWith(''),
|
|
||||||
map(value => this._filter(value || '')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.policyControl.setValue(this.policy);
|
this.policyControl.setValue(this.policy);
|
||||||
@ -60,13 +58,12 @@ export class PolicyDropdownComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _filter(value: string): string[] {
|
private _filter(value: string): PolicyData[] {
|
||||||
const filterValue = value.toLowerCase();
|
const filterValue = value.toLowerCase();
|
||||||
const names = Policies.map(p => p.path);
|
if (Policies.map(p => p.path).includes(filterValue)) {
|
||||||
if (names.includes(filterValue)) {
|
return Policies;
|
||||||
return names;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return names.filter(option => option.toLowerCase().includes(filterValue));
|
return Policies.filter(option => option.path.toLowerCase().includes(filterValue));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,22 +52,18 @@
|
|||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
<mat-card-actions align="end">
|
<mat-card-actions>
|
||||||
<button mat-button
|
<button mat-button
|
||||||
|
class="neutral"
|
||||||
(click)="dialogRef.close()">
|
(click)="dialogRef.close()">
|
||||||
<mat-icon>cancel</mat-icon>Cancel
|
<mat-icon>cancel</mat-icon>Cancel
|
||||||
</button>
|
</button>
|
||||||
@if (isNew) {
|
|
||||||
<button mat-button
|
<button mat-button
|
||||||
|
class="confirm"
|
||||||
|
[disabled]="!formGroup.dirty || formGroup.invalid || waitForResponse"
|
||||||
(click)="save()">
|
(click)="save()">
|
||||||
<mat-icon>add</mat-icon>Add
|
<mat-icon>{{(isNew ? 'add' : 'save')}}</mat-icon>{{(isNew ? 'Add' : 'Save')}}
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
|
||||||
<button mat-button
|
|
||||||
(click)="save()">
|
|
||||||
<mat-icon>save</mat-icon>Save
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
|
|
||||||
@if (responseError) {
|
@if (responseError) {
|
||||||
|
@ -2,7 +2,7 @@ import { AfterViewInit, Component, inject, OnInit, ViewChild } from '@angular/co
|
|||||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
@ -18,6 +18,7 @@ import { PolicyDropdownComponent } from '../policy-dropdown/policy-dropdown.comp
|
|||||||
GroupDropdownComponent,
|
GroupDropdownComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
@ -78,7 +79,7 @@ export class PolicyItemEditComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
if (this.formGroup.invalid || this.waitForResponse)
|
if (!this.formGroup.dirty || this.formGroup.invalid || this.waitForResponse)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.waitForResponse = true;
|
this.waitForResponse = true;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<table mat-table
|
<table mat-table
|
||||||
[dataSource]="policies"
|
[dataSource]="policies()"
|
||||||
class="mat-elevation-z8">
|
class="mat-elevation-z8">
|
||||||
<ng-container matColumnDef="path">
|
<ng-container matColumnDef="path">
|
||||||
<th mat-header-cell
|
<th mat-header-cell
|
||||||
|
@ -1,79 +1,48 @@
|
|||||||
import { AfterViewInit, Component, inject, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core';
|
import { Component, inject, input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { MatTable, MatTableModule } from '@angular/material/table';
|
import { MatTable, MatTableModule } from '@angular/material/table';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import EventService from '../../shared/services/EventService';
|
|
||||||
import { Policy } from '../../shared/models/policy';
|
import { Policy } from '../../shared/models/policy';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
import { HermesClientService } from '../../hermes-client.service';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { PolicyItemEditComponent } from '../policy-item-edit/policy-item-edit.component';
|
import { PolicyItemEditComponent } from '../policy-item-edit/policy-item-edit.component';
|
||||||
import { Group } from '../../shared/models/group';
|
import { Group } from '../../shared/models/group';
|
||||||
|
import PolicyService from '../../shared/services/policy.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'policy-table',
|
selector: 'policy-table',
|
||||||
imports: [FormsModule, MatButtonModule, MatTableModule, MatIconModule],
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatIconModule,
|
||||||
|
],
|
||||||
templateUrl: './policy-table.component.html',
|
templateUrl: './policy-table.component.html',
|
||||||
styleUrl: './policy-table.component.scss'
|
styleUrl: './policy-table.component.scss'
|
||||||
})
|
})
|
||||||
export class PolicyTableComponent implements OnInit, OnDestroy, AfterViewInit {
|
export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly client = inject(HermesClientService);
|
||||||
private readonly hermes = inject(HermesClientService);
|
private readonly policyService = inject(PolicyService);
|
||||||
private readonly events = inject(EventService);
|
|
||||||
private readonly dialog = inject(MatDialog);
|
private readonly dialog = inject(MatDialog);
|
||||||
|
|
||||||
@Input() policies: Policy[] = [];
|
policies = input.required<Policy[]>();
|
||||||
|
groups = input.required<Group[]>();
|
||||||
|
|
||||||
@ViewChild(MatTable) table: MatTable<Policy>;
|
@ViewChild(MatTable) table: MatTable<Policy>;
|
||||||
|
|
||||||
readonly displayedColumns = ['path', 'group', 'usage', 'span', 'actions'];
|
readonly displayedColumns = ['path', 'group', 'usage', 'span', 'actions'];
|
||||||
private readonly _subscriptions: any[] = [];
|
private readonly _subscriptions: any[] = [];
|
||||||
|
|
||||||
groups: Group[] = [];
|
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.table = {} as MatTable<Policy>;
|
this.table = {} as MatTable<Policy>;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.data.subscribe(r => {
|
this._subscriptions.push(this.policyService.create$?.subscribe(_ => this.table.renderRows()));
|
||||||
this.groups = [...r['groups']];
|
this._subscriptions.push(this.policyService.update$?.subscribe(_ => this.table.renderRows()));
|
||||||
});
|
this._subscriptions.push(this.policyService.delete$?.subscribe(_ => this.table.renderRows()));
|
||||||
|
|
||||||
this._subscriptions.push(this.events.listen('addPolicy', (payload) => {
|
|
||||||
if (!payload || this.policies.map(p => p.path).includes(payload))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.policies.push(payload);
|
|
||||||
this.table.renderRows();
|
|
||||||
}));
|
|
||||||
|
|
||||||
this._subscriptions.push(this.hermes.subscribeToRequests('create_policy', response => {
|
|
||||||
const policy = this.policies.find(p => p.path == response.data.path);
|
|
||||||
if (policy == null) {
|
|
||||||
this.policies.push(response.data);
|
|
||||||
}
|
|
||||||
this.table.renderRows();
|
|
||||||
}));
|
|
||||||
|
|
||||||
this._subscriptions.push(this.hermes.subscribeToRequests('update_policy', response => {
|
|
||||||
const policy = this.policies.find(p => p.id == response.data.id);
|
|
||||||
if (policy != null) {
|
|
||||||
policy.id = response.data.id;
|
|
||||||
policy.group_id = response.data.group_id;
|
|
||||||
}
|
|
||||||
this.table.renderRows();
|
|
||||||
}));
|
|
||||||
|
|
||||||
this._subscriptions.push(this.hermes.subscribeToRequests('delete_policy', response => {
|
|
||||||
this.policies = this.policies.filter(p => p.id != response.request.data.id);
|
|
||||||
this.table.renderRows();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.table.renderRows();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -81,33 +50,23 @@ export class PolicyTableComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete(policy: Policy) {
|
delete(policy: Policy) {
|
||||||
this.hermes.deletePolicy(policy.id);
|
this.client.deletePolicy(policy.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
edit(policy: Policy) {
|
edit(policy: Policy) {
|
||||||
const dialogRef = this.dialog.open(PolicyItemEditComponent, {
|
this.dialog.open(PolicyItemEditComponent, {
|
||||||
data: {
|
data: {
|
||||||
policies: this.policies,
|
policies: this.policies(),
|
||||||
groups: this.groups,
|
groups: this.groups(),
|
||||||
policy_id: policy.id,
|
policy_id: policy.id,
|
||||||
group_id: policy.group_id,
|
group_id: policy.group_id,
|
||||||
groupDisabled: true,
|
groupDisabled: true,
|
||||||
isNew: false,
|
isNew: false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((result: Policy) => {
|
|
||||||
if (!result)
|
|
||||||
return;
|
|
||||||
|
|
||||||
policy.group_id = result.group_id;
|
|
||||||
policy.path = result.path;
|
|
||||||
policy.usage = result.usage;
|
|
||||||
policy.span = result.span;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroupById(group_id: string) {
|
getGroupById(group_id: string) {
|
||||||
return this.groups.find((g: Group) => g.id == group_id);
|
return this.groups().find((g: Group) => g.id == group_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
<h4>Policies</h4>
|
<h4>Policies</h4>
|
||||||
<div class="add">
|
<div class="add">
|
||||||
<policy-add-button [policies]="policies"
|
<policy-add-button [policies]="policies"
|
||||||
[groups]="groups"
|
[groups]="groups" />
|
||||||
(policy)="addPolicy($event)" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<policy-table [policies]="policies" />
|
<policy-table [policies]="policies"
|
||||||
|
[groups]="groups" />
|
||||||
</div>
|
</div>
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject, OnDestroy } from '@angular/core';
|
||||||
import { PolicyTableComponent } from "../policy-table/policy-table.component";
|
import { PolicyTableComponent } from "../policy-table/policy-table.component";
|
||||||
import { Policy } from '../../shared/models/policy';
|
import { Policy } from '../../shared/models/policy';
|
||||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||||
@ -6,43 +6,55 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { PolicyAddButtonComponent } from "../policy-add-button/policy-add-button.component";
|
import { PolicyAddButtonComponent } from "../policy-add-button/policy-add-button.component";
|
||||||
import { Group } from '../../shared/models/group';
|
import { Group } from '../../shared/models/group';
|
||||||
|
import PolicyService from '../../shared/services/policy.service';
|
||||||
|
import GroupService from '../../shared/services/group.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'policy',
|
selector: 'policy',
|
||||||
imports: [MatButtonModule, MatIconModule, PolicyTableComponent, RouterModule, PolicyAddButtonComponent],
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
PolicyTableComponent,
|
||||||
|
RouterModule,
|
||||||
|
PolicyAddButtonComponent,
|
||||||
|
],
|
||||||
templateUrl: './policy.component.html',
|
templateUrl: './policy.component.html',
|
||||||
styleUrl: './policy.component.scss'
|
styleUrl: './policy.component.scss'
|
||||||
})
|
})
|
||||||
export class PolicyComponent {
|
export class PolicyComponent implements OnDestroy {
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
|
||||||
|
private readonly policyService = inject(PolicyService);
|
||||||
|
private readonly groupService = inject(GroupService);
|
||||||
|
|
||||||
|
private readonly _subscriptions: any[] = [];
|
||||||
private _policies: Policy[] = [];
|
private _policies: Policy[] = [];
|
||||||
groups: Group[] = [];
|
private _groups: Group[] = [];
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.route.data.subscribe((data) => {
|
this.route.data.subscribe((data) => {
|
||||||
const policies = [...data['policies']];
|
this._policies = data['policies'];
|
||||||
policies.sort(this.compare);
|
this._groups = data['groups'];
|
||||||
this._policies = policies;
|
|
||||||
|
|
||||||
this.groups = [...data['groups']];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._subscriptions.push(this.policyService.delete$?.subscribe(_ =>
|
||||||
|
this.policyService.fetch().subscribe(p => this._policies = p)));
|
||||||
|
|
||||||
|
this._subscriptions.push(this.groupService.deleteGroup$?.subscribe(_ =>
|
||||||
|
this.groupService.fetch().subscribe(g => this._groups = g.groups)));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this._subscriptions.filter(s => !!s).forEach(s => s.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
get policies() {
|
get policies() {
|
||||||
return this._policies;
|
return this._policies;
|
||||||
}
|
}
|
||||||
|
|
||||||
addPolicy(policy: Policy) {
|
get groups() {
|
||||||
let index = -1;
|
return this._groups;
|
||||||
for (let i = 0; i < this._policies.length; i++) {
|
|
||||||
const comp = this.compare(policy, this._policies[i]);
|
|
||||||
if (comp < 0) {
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._policies.splice(index >= 0 ? index : this._policies.length, 0, policy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compare(a: Policy, b: Policy) {
|
compare(a: Policy, b: Policy) {
|
||||||
|
@ -31,23 +31,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="buttons">
|
</mat-card-content>
|
||||||
<button mat-icon-button
|
|
||||||
class="save"
|
<mat-card-actions>
|
||||||
[disabled]="waitForResponse || formGroups.invalid"
|
|
||||||
(click)="save()">
|
|
||||||
<mat-icon>save</mat-icon>
|
|
||||||
</button>
|
|
||||||
@if (redemption.id) {
|
@if (redemption.id) {
|
||||||
<button mat-icon-button
|
<button mat-button
|
||||||
class="delete"
|
class="danger"
|
||||||
[disabled]="waitForResponse"
|
[disabled]="waitForResponse"
|
||||||
(click)="delete()">
|
(click)="delete()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>Delete
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
<button mat-button
|
||||||
</mat-card-content>
|
class="confirm"
|
||||||
|
[disabled]="waitForResponse || formGroups.invalid"
|
||||||
|
(click)="save()">
|
||||||
|
<mat-icon>save</mat-icon>Save
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
|
||||||
@if (responseError) {
|
@if (responseError) {
|
||||||
<mat-card-footer>
|
<mat-card-footer>
|
||||||
<small class="error below">{{responseError}}</small>
|
<small class="error below">{{responseError}}</small>
|
||||||
|
@ -1,46 +1,9 @@
|
|||||||
.mat-mdc-card {
|
|
||||||
margin: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-mdc-card-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
column-gap: 1em;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
margin: -0.75em;
|
margin: -0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
|
||||||
.mdc-button {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width:1000px) {
|
|
||||||
.mat-mdc-card-content {
|
.mat-mdc-card-content {
|
||||||
flex-direction: column;
|
display: grid;
|
||||||
margin-bottom: 0.5em;
|
row-gap: 1em;
|
||||||
display: flex;
|
align-self: center;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button~button {
|
|
||||||
margin-left: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete {
|
|
||||||
background-color: rgb(255, 152, 152);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, inject, Input, model, OnInit, signal } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import Redemption from '../../shared/models/redemption';
|
import Redemption from '../../shared/models/redemption';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
@ -14,8 +14,6 @@ import TwitchRedemption from '../../shared/models/twitch-redemption';
|
|||||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||||
import { integerValidator } from '../../shared/validators/integer';
|
import { integerValidator } from '../../shared/validators/integer';
|
||||||
import { createTypeValidator } from '../../shared/validators/of-type';
|
import { createTypeValidator } from '../../shared/validators/of-type';
|
||||||
import RedemptionService from '../../shared/services/redemption.service';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'redemption-item-edit',
|
selector: 'redemption-item-edit',
|
||||||
@ -33,10 +31,9 @@ import { Subscription } from 'rxjs';
|
|||||||
styleUrl: './redemption-item-edit.component.scss'
|
styleUrl: './redemption-item-edit.component.scss'
|
||||||
})
|
})
|
||||||
export class RedemptionItemEditComponent implements OnInit {
|
export class RedemptionItemEditComponent implements OnInit {
|
||||||
readonly client = inject(HermesClientService);
|
private readonly client = inject(HermesClientService);
|
||||||
readonly redemptionService = inject(RedemptionService);
|
private readonly dialogRef = inject(MatDialogRef<RedemptionItemEditComponent>);
|
||||||
readonly dialogRef = inject(MatDialogRef<RedemptionItemEditComponent>);
|
private readonly data = inject<{ redemption: Redemption, twitchRedemptions: { [id: string]: TwitchRedemption }, redeemableActions: RedeemableAction[] }>(MAT_DIALOG_DATA);
|
||||||
readonly data = inject<{ redemption: Redemption, twitchRedemptions: TwitchRedemption[], redeemableActions: RedeemableAction[] }>(MAT_DIALOG_DATA);
|
|
||||||
|
|
||||||
redemptionFormControl = new FormControl<TwitchRedemption | string | undefined>(undefined, [Validators.required, createTypeValidator('Object')]);
|
redemptionFormControl = new FormControl<TwitchRedemption | string | undefined>(undefined, [Validators.required, createTypeValidator('Object')]);
|
||||||
redemptionErrorMessages: { [errorKey: string]: string } = {
|
redemptionErrorMessages: { [errorKey: string]: string } = {
|
||||||
@ -66,16 +63,17 @@ export class RedemptionItemEditComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
redemption: Redemption = { id: '', user_id: '', redemption_id: '', action_name: '', order: 0, state: true };
|
redemption: Redemption = { id: '', user_id: '', redemption_id: '', action_name: '', order: 0, state: true };
|
||||||
twitchRedemptions: TwitchRedemption[] = [];
|
twitchRedemptions: { [id: string]: TwitchRedemption } = {};
|
||||||
redeemableActions: RedeemableAction[] = [];
|
redeemableActions: RedeemableAction[] = [];
|
||||||
|
|
||||||
waitForResponse = false;
|
waitForResponse = false;
|
||||||
responseError: string | undefined = undefined;
|
responseError: string | undefined = undefined;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.redemption = this.data.redemption;
|
this.redemption = this.data.redemption;
|
||||||
this.orderFormControl.setValue(this.redemption.order);
|
|
||||||
this.twitchRedemptions = this.data.twitchRedemptions;
|
this.twitchRedemptions = this.data.twitchRedemptions;
|
||||||
this.redeemableActions = this.data.redeemableActions;
|
this.redeemableActions = this.data.redeemableActions;
|
||||||
|
this.orderFormControl.setValue(this.redemption.order);
|
||||||
this.orderErrorMessageKeys = Object.keys(this.orderErrorMessages);
|
this.orderErrorMessageKeys = Object.keys(this.orderErrorMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +82,9 @@ export class RedemptionItemEditComponent implements OnInit {
|
|||||||
if (this.waitForResponse)
|
if (this.waitForResponse)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.waitForResponse = true
|
this.waitForResponse = true;
|
||||||
|
this.responseError = undefined;
|
||||||
|
|
||||||
const id = this.redemption.id;
|
const id = this.redemption.id;
|
||||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'delete_redemption' && d.d.request.data.id == id)
|
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'delete_redemption' && d.d.request.data.id == id)
|
||||||
?.subscribe({
|
?.subscribe({
|
||||||
@ -102,11 +102,12 @@ export class RedemptionItemEditComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
if (this.formGroups.invalid || this.waitForResponse)
|
if (!this.formGroups.dirty || this.formGroups.invalid || this.waitForResponse)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.waitForResponse = true;
|
this.waitForResponse = true;
|
||||||
this.responseError = undefined;
|
this.responseError = undefined;
|
||||||
|
|
||||||
const order = this.orderFormControl.value;
|
const order = this.orderFormControl.value;
|
||||||
if (order == null) {
|
if (order == null) {
|
||||||
this.responseError = 'Order must be an integer.';
|
this.responseError = 'Order must be an integer.';
|
||||||
@ -114,16 +115,14 @@ export class RedemptionItemEditComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.waitForResponse = true;
|
|
||||||
const isNew = !this.redemption.id;
|
const isNew = !this.redemption.id;
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_redemption' && d.d.request.data.action == (this.redemption.action_name ?? '') && d.d.request.data.redemption == (this.redemption.redemption_id ?? ''))
|
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_redemption' && d.d.request.data.action == this.redemption.action_name && d.d.request.data.redemption == this.redemption.redemption_id)
|
||||||
?.subscribe({
|
?.subscribe({
|
||||||
next: (d) => {
|
next: (d) => {
|
||||||
if (d.d.error) {
|
if (d.d.error) {
|
||||||
this.responseError = d.d.error;
|
this.responseError = d.d.error;
|
||||||
} else {
|
} else {
|
||||||
this.redemption.order = order;
|
|
||||||
this.dialogRef.close(d.d.data);
|
this.dialogRef.close(d.d.data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -132,13 +131,13 @@ export class RedemptionItemEditComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
this.client.createRedemption(this.redemption.redemption_id, this.redemption.action_name, order);
|
this.client.createRedemption(this.redemption.redemption_id, this.redemption.action_name, order);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_redemption' && d.d.data.id == this.redemption.id)
|
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_redemption' && d.d.data.id == this.redemption.id)
|
||||||
?.subscribe({
|
?.subscribe({
|
||||||
next: (d) => {
|
next: (d) => {
|
||||||
if (d.d.error) {
|
if (d.d.error) {
|
||||||
this.responseError = d.d.error;
|
this.responseError = d.d.error;
|
||||||
} else {
|
} else {
|
||||||
this.redemption.order = order;
|
|
||||||
this.dialogRef.close(d.d.data);
|
this.dialogRef.close(d.d.data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="content">
|
<content>
|
||||||
<button mat-button
|
<button mat-button
|
||||||
class="add"
|
class="add"
|
||||||
(click)="add()"><mat-icon>add</mat-icon> Add Redemption</button>
|
(click)="add()"><mat-icon>add</mat-icon> Add Redemption</button>
|
||||||
@ -14,10 +14,12 @@
|
|||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption"
|
<twitch-redemption-dropdown [twitchRedemptions]="twitchRedemptions()"
|
||||||
[search]="true" />
|
[(twitchRedemptionId)]="filter_redemption"
|
||||||
<action-dropdown [(action)]="filter_action_name"
|
|
||||||
[search]="true" />
|
[search]="true" />
|
||||||
|
<action-dropdown [search]="true"
|
||||||
|
[actions]="actions()"
|
||||||
|
[(action)]="filter_action_name" />
|
||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
@ -29,8 +31,7 @@
|
|||||||
<th mat-header-cell
|
<th mat-header-cell
|
||||||
*matHeaderCellDef>Twitch Redemption Name</th>
|
*matHeaderCellDef>Twitch Redemption Name</th>
|
||||||
<td mat-cell
|
<td mat-cell
|
||||||
*matCellDef="let redemption">{{getTwitchRedemptionNameById(redemption.redemption_id) || 'Unknown
|
*matCellDef="let redemption">{{getTwitchRedemptionNameById(redemption.redemption_id) || 'Unknown Twitch Redemption'}}</td>
|
||||||
Twitch Redemption'}}</td>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="action-name">
|
<ng-container matColumnDef="action-name">
|
||||||
@ -65,4 +66,4 @@
|
|||||||
*matRowDef="let row; columns: displayedColumns;"></tr>
|
*matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</content>
|
@ -1,9 +1,3 @@
|
|||||||
.content {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add {
|
.add {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 3em;
|
margin-top: 3em;
|
||||||
@ -21,10 +15,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
min-width: 800px;
|
width: 800px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 60vh;
|
height: 60vh;
|
||||||
overflow: auto;
|
overflow: scroll;
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
@ -1,23 +1,19 @@
|
|||||||
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
import { Component, inject, input, signal } from '@angular/core';
|
||||||
import RedemptionService from '../../shared/services/redemption.service';
|
|
||||||
import Redemption from '../../shared/models/redemption';
|
import Redemption from '../../shared/models/redemption';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { TwitchRedemptionDropdownComponent } from "../twitch-redemption-dropdown/twitch-redemption-dropdown.component";
|
import { TwitchRedemptionDropdownComponent } from "../twitch-redemption-dropdown/twitch-redemption-dropdown.component";
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { ActionDropdownComponent } from '../../actions/action-dropdown/action-dropdown.component';
|
import { ActionDropdownComponent } from '../../actions/action-dropdown/action-dropdown.component';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import TwitchRedemption from '../../shared/models/twitch-redemption';
|
|
||||||
import { RedemptionItemEditComponent } from '../redemption-item-edit/redemption-item-edit.component';
|
import { RedemptionItemEditComponent } from '../redemption-item-edit/redemption-item-edit.component';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||||
import { MatExpansionModule } from '@angular/material/expansion';
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||||
import { Subscription } from 'rxjs';
|
import { toTwitchRedemptionDict } from '../../shared/transformers/twitch-redemption.transformer';
|
||||||
import { HermesClientService } from '../../hermes-client.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'redemption-list',
|
selector: 'redemption-list',
|
||||||
@ -36,97 +32,44 @@ import { HermesClientService } from '../../hermes-client.service';
|
|||||||
templateUrl: './redemption-list.component.html',
|
templateUrl: './redemption-list.component.html',
|
||||||
styleUrl: './redemption-list.component.scss'
|
styleUrl: './redemption-list.component.scss'
|
||||||
})
|
})
|
||||||
export class RedemptionListComponent implements OnDestroy {
|
export class RedemptionListComponent {
|
||||||
private readonly client = inject(HermesClientService);
|
|
||||||
private readonly redemptionService = inject(RedemptionService);
|
|
||||||
private readonly route = inject(ActivatedRoute);
|
|
||||||
readonly dialog = inject(MatDialog);
|
readonly dialog = inject(MatDialog);
|
||||||
private _redemptions: Redemption[] = [];
|
|
||||||
private _twitchRedemptions: TwitchRedemption[] = [];
|
|
||||||
private _twitchRedemptionsDict: { [id: string]: string } = {};
|
|
||||||
private _actions: RedeemableAction[] = [];
|
|
||||||
displayedColumns: string[] = ['twitch-redemption', 'action-name', 'order', 'misc'];
|
displayedColumns: string[] = ['twitch-redemption', 'action-name', 'order', 'misc'];
|
||||||
filter_redemption: string | undefined;
|
filter_redemption: string | undefined;
|
||||||
filter_action_name: string | undefined;
|
filter_action_name: string | undefined;
|
||||||
readonly panelOpenState = signal(false);
|
readonly panelOpenState = signal(false);
|
||||||
private _subscriptions: Subscription[] = []
|
|
||||||
|
|
||||||
|
_redemptions = input.required<Redemption[]>({ alias: 'redemptions' });
|
||||||
constructor() {
|
twitchRedemptions = input.required({
|
||||||
this.route.data.subscribe(r => {
|
alias: 'twitchRedemptions',
|
||||||
this._twitchRedemptions = r['twitchRedemptions'];
|
transform: toTwitchRedemptionDict,
|
||||||
this._twitchRedemptionsDict = Object.assign({}, ...r['twitchRedemptions'].map((t: TwitchRedemption) => ({ [t.id]: t.title })));
|
|
||||||
this._actions = r['redeemableActions'];
|
|
||||||
|
|
||||||
let redemptions = [...r['redemptions']];
|
|
||||||
redemptions.sort((a: Redemption, b: Redemption) => this.compare(a, b));
|
|
||||||
this._redemptions = redemptions;
|
|
||||||
});
|
});
|
||||||
|
actions = input.required<RedeemableAction[]>();
|
||||||
|
|
||||||
let subscription = this.redemptionService.create$?.subscribe(d => {
|
|
||||||
if (d.error || !d.data || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
|
||||||
return;
|
|
||||||
|
|
||||||
let index = -1;
|
|
||||||
for (let i = 0; i < this._redemptions.length; i++) {
|
|
||||||
const comp = this.compare(d.data, this._redemptions[i]);
|
|
||||||
if (comp < 0) {
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._redemptions.splice(index >= 0 ? index : this._redemptions.length, 0, d.data);
|
|
||||||
});
|
|
||||||
if (subscription)
|
|
||||||
this._subscriptions.push(subscription);
|
|
||||||
|
|
||||||
subscription = this.redemptionService.update$?.subscribe(d => {
|
|
||||||
if (d.error || !d.data || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const redemption = this._redemptions.find(r => r.id = d.data.id);
|
|
||||||
if (redemption) {
|
|
||||||
redemption.action_name = d.data.action_name;
|
|
||||||
redemption.redemption_id = d.data.redemption_id;
|
|
||||||
redemption.order = d.data.order;
|
|
||||||
redemption.state = d.data.state;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (subscription)
|
|
||||||
this._subscriptions.push(subscription);
|
|
||||||
|
|
||||||
subscription = this.redemptionService.delete$?.subscribe(d => {
|
|
||||||
if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this._redemptions = this._redemptions.filter(r => r.id != d.request.data.id);
|
|
||||||
});
|
|
||||||
if (subscription)
|
|
||||||
this._subscriptions.push(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.panelOpenState.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._subscriptions.forEach(s => s.unsubscribe());
|
|
||||||
}
|
|
||||||
|
|
||||||
compare(a: Redemption, b: Redemption) {
|
compare(a: Redemption, b: Redemption) {
|
||||||
return this._twitchRedemptionsDict[a.redemption_id].localeCompare(this._twitchRedemptionsDict[b.redemption_id]) || a.order - b.order;
|
const dict = this.twitchRedemptions();
|
||||||
|
if (!dict) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return dict[a.redemption_id]?.title.localeCompare(dict[b.redemption_id]?.title) || a.order - b.order;
|
||||||
}
|
}
|
||||||
|
|
||||||
get redemptions() {
|
get redemptions() {
|
||||||
const redemptionFilter = this.filter_redemption?.toString().toLowerCase();
|
|
||||||
const actionFilter = this.filter_action_name?.toString().toLowerCase();
|
const actionFilter = this.filter_action_name?.toString().toLowerCase();
|
||||||
let filtered = this._redemptions.filter(r => !redemptionFilter || this._twitchRedemptionsDict[r.redemption_id].toLowerCase().includes(redemptionFilter));
|
const redemptionFilter = this.filter_redemption?.toString().toLowerCase();
|
||||||
|
let filtered = this._redemptions();
|
||||||
|
if (redemptionFilter) {
|
||||||
|
filtered = filtered.filter(r => this.twitchRedemptions()![r.redemption_id]?.title.toLowerCase().includes(redemptionFilter) || r.redemption_id == redemptionFilter);
|
||||||
|
}
|
||||||
|
if (actionFilter) {
|
||||||
filtered = filtered.filter(r => !actionFilter || r.action_name.toLowerCase().includes(actionFilter));
|
filtered = filtered.filter(r => !actionFilter || r.action_name.toLowerCase().includes(actionFilter));
|
||||||
|
}
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTwitchRedemptionNameById(id: string) {
|
getTwitchRedemptionNameById(id: string) {
|
||||||
return this._twitchRedemptionsDict[id];
|
return this.twitchRedemptions()![id]?.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(): void {
|
add(): void {
|
||||||
@ -134,39 +77,9 @@ export class RedemptionListComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openDialog(redemption: Redemption): void {
|
openDialog(redemption: Redemption): void {
|
||||||
const dialogRef = this.dialog.open(RedemptionItemEditComponent, {
|
this.dialog.open(RedemptionItemEditComponent, {
|
||||||
data: { redemption: { ...redemption }, twitchRedemptions: this._twitchRedemptions, redeemableActions: this._actions },
|
data: { redemption: { ...redemption }, twitchRedemptions: this.twitchRedemptions(), redeemableActions: this.actions() },
|
||||||
maxWidth: '100vw'
|
maxWidth: '100vw'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((result: Redemption | string) => {
|
|
||||||
if (!result)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (typeof result === 'string') {
|
|
||||||
// Deleted
|
|
||||||
this._redemptions = this._redemptions.filter(r => r.id != result);
|
|
||||||
} else {
|
|
||||||
const redemption = this._redemptions.find(r => r.id == result.id);
|
|
||||||
if (redemption) {
|
|
||||||
// Updated
|
|
||||||
redemption.action_name = result.action_name;
|
|
||||||
redemption.redemption_id = result.redemption_id;
|
|
||||||
redemption.order = result.order;
|
|
||||||
redemption.state = result.state;
|
|
||||||
} else {
|
|
||||||
// Created
|
|
||||||
let index = -1;
|
|
||||||
for (let i = 0; i < this._redemptions.length; i++) {
|
|
||||||
const comp = this.compare(result, this._redemptions[i]);
|
|
||||||
if (comp < 0) {
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._redemptions.splice(index >= 0 ? index : this._redemptions.length, 0, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,7 @@
|
|||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<redemption-list />
|
<redemption-list [redemptions]="redemptions$ | async"
|
||||||
|
[twitchRedemptions]="twitchRedemptions$ | async"
|
||||||
|
[actions]="actions$ | async" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user