diff --git a/src/app/actions/action-list/action-list.component.html b/src/app/actions/action-list/action-list.component.html index e3f2f1b..28b824c 100644 --- a/src/app/actions/action-list/action-list.component.html +++ b/src/app/actions/action-list/action-list.component.html @@ -1,5 +1,5 @@
- @for (action of actions; track $index) { + @for (action of actions; track action.name) { diff --git a/src/app/groups/group-page/group-page.component.ts b/src/app/groups/group-page/group-page.component.ts index 1fa8f0c..4141542 100644 --- a/src/app/groups/group-page/group-page.component.ts +++ b/src/app/groups/group-page/group-page.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, OnInit } from '@angular/core'; +import { Component, inject, OnDestroy } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Group } from '../../shared/models/group'; import { Policy } from '../../shared/models/policy'; @@ -15,6 +15,12 @@ import { HermesClientService } from '../../hermes-client.service'; import { GroupChatter } from '../../shared/models/group-chatter'; import { TwitchUsersModule } from "../../twitch-users/twitch-users.module"; import { SpecialGroups } from '../../shared/utils/groups'; +import { PermissionListComponent } from "../../permissions/permission-list/permission-list.component"; +import { Permission } from '../../shared/models/permission'; +import { Subscription } from 'rxjs'; +import { PermissionService } from '../../shared/services/permission.service'; +import GroupService from '../../shared/services/group.service'; +import PolicyService from '../../shared/services/policy.service'; @Component({ imports: [ @@ -29,30 +35,42 @@ import { SpecialGroups } from '../../shared/utils/groups'; PolicyTableComponent, PolicyTableComponent, TwitchUsersModule, + PermissionListComponent ], templateUrl: './group-page.component.html', styleUrl: './group-page.component.scss' }) -export class GroupPageComponent { +export class GroupPageComponent implements OnDestroy { private readonly _router = inject(Router); private readonly _route = inject(ActivatedRoute); + private readonly _groupService = inject(GroupService); + private readonly _permissionService = inject(PermissionService); + private readonly _policyService = inject(PolicyService); private readonly _client = inject(HermesClientService); private _group: Group | undefined; private _chatters: GroupChatter[]; private _policies: Policy[]; + private _permissions: Permission[]; + + isSpecialGroup: boolean; + _groups: Group[]; + + private readonly subscriptions: (Subscription | undefined)[] = []; - isSpecialGroup = false; - groups: Group[] = []; constructor() { + this.isSpecialGroup = false + this._groups = []; this._chatters = []; + this._permissions = []; this._policies = []; - this._route.params.subscribe((p: any) => { - const group_id = p.id; + this._route.params.subscribe((params: any) => { + // Fetch the group id from the query params. + const group_id = params['id']; this._route.data.subscribe(async (data: any) => { - this.groups = [...data['groups']]; + this._groups = data['groups']; const group = this.groups.find((g: Group) => g.id == group_id); if (!group) { @@ -62,29 +80,88 @@ export class GroupPageComponent { this._group = group; this.isSpecialGroup = SpecialGroups.includes(this.group!.name); - this._chatters = [...data['chatters'].filter((c: GroupChatter) => c.group_id == group_id)]; - this._policies = [...data['policies'].filter((p: Policy) => p.group_id == group_id)]; + this._chatters = data['chatters']; + this._permissions = data['permissions']; + this._policies = data['policies']; }); }); + + this.subscriptions.push(this._permissionService.delete$?.subscribe(d => { + if (d.error) { + return; + } + + this._permissionService.fetch().subscribe(permissions => this._permissions = permissions); + })); + + this.subscriptions.push(this._groupService.deleteGroup$?.subscribe(d => { + if (d.error) { + return; + } + + this._groupService.fetch().subscribe(data => this._groups = data.groups); + })); + + this.subscriptions.push(this._groupService.deleteChatter$?.subscribe(d => { + if (d.error) { + return; + } + + this._groupService.fetch().subscribe(data => this._chatters = data.chatters); + })); + + this.subscriptions.push(this._policyService.delete$?.subscribe(d => { + if (d.error) { + return; + } + + this._policyService.fetch().subscribe(policies => this._policies = policies); + })); + } + + ngOnDestroy(): void { + if (this.subscriptions) { + for (let subscription of this.subscriptions) { + if (subscription) + subscription.unsubscribe(); + } + } } get group() { return this._group; } + get groups() { + return this._groups; + } + get chatters() { - return this._chatters; + if (!this._group) { + return []; + } + return this._chatters.filter((c: GroupChatter) => c.group_id == this._group!.id); + } + + get permissions() { + if (!this._group) { + return []; + } + return this._permissions.filter((p: Permission) => p.group_id == this._group!.id); } get policies() { - return this._policies; + if (!this._group) { + return []; + } + return this._policies.filter((p: Policy) => p.group_id == this._group!.id); } delete() { if (!this.group) return; - this._client.first(d => d.d.request.type == 'delete_group' && d.d.request.data.group == this.group!.id) + this._client.first(d => d.d.request.type == 'delete_group' && d.d.request.data.id == this.group!.id) .subscribe(async () => await this._router.navigate(['groups'])); this._client.deleteGroup(this.group.id); } diff --git a/src/app/groups/groups/groups.component.html b/src/app/groups/groups/groups.component.html index 556f071..bacca2d 100644 --- a/src/app/groups/groups/groups.component.html +++ b/src/app/groups/groups/groups.component.html @@ -14,4 +14,7 @@ } \ No newline at end of file + [groups]="groups" + [chatters]="chatters" + [permissions]="permissions" + [policies]="policies" /> \ No newline at end of file diff --git a/src/app/groups/groups/groups.component.ts b/src/app/groups/groups/groups.component.ts index 567ebbe..7e2e548 100644 --- a/src/app/groups/groups/groups.component.ts +++ b/src/app/groups/groups/groups.component.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { Component, inject, OnDestroy } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { ActivatedRoute, RouterModule } from '@angular/router'; @@ -13,6 +13,10 @@ import { MatMenuModule } from '@angular/material/menu'; import { HermesClientService } from '../../hermes-client.service'; import { GroupChatter } from '../../shared/models/group-chatter'; import { SpecialGroups } from '../../shared/utils/groups'; +import { Permission } from '../../shared/models/permission'; +import { Subscription } from 'rxjs'; +import { PermissionService } from '../../shared/services/permission.service'; +import PolicyService from '../../shared/services/policy.service'; @Component({ selector: 'groups', @@ -27,100 +31,108 @@ import { SpecialGroups } from '../../shared/utils/groups'; templateUrl: './groups.component.html', styleUrl: './groups.component.scss' }) -export class GroupsComponent { - private readonly _groupService = inject(GroupService); +export class GroupsComponent implements OnDestroy { private readonly _client = inject(HermesClientService); private readonly _route = inject(ActivatedRoute); private readonly _dialog = inject(MatDialog); - + private readonly _groupService = inject(GroupService); + private readonly _permissionService = inject(PermissionService); + private readonly _policyService = inject(PolicyService); + private readonly subscriptions: (Subscription | undefined)[] = []; + readonly specialGroups = SpecialGroups; - items: { group: Group, chatters: GroupChatter[], policies: Policy[] }[] = []; + private _groups: Group[] = []; + private _chatters: GroupChatter[] = []; + private _permissions: Permission[] = []; + private _policies: Policy[] = []; + + opened = false; constructor() { this._route.data.subscribe(payload => { - const groups = payload['groups']; - const chatters = payload['chatters']; - const policies = payload['policies']; - const elements: { group: Group, chatters: GroupChatter[], policies: Policy[] }[] = []; + this._groups = payload['groups']; + this._chatters = payload['chatters']; + this._permissions = payload['permissions']; + this._policies = payload['policies']; + }); - for (let group of groups) { - elements.push({ - group: group, - chatters: chatters.filter((c: GroupChatter) => c.group_id == group.id), - policies: policies.filter((p: Policy) => p.group_id == group.id), - }); + this.subscriptions.push(this._permissionService.delete$?.subscribe(d => { + if (d.error) { + return; } - this.items = elements; - }); + this._permissionService.fetch().subscribe(permissions => this._permissions = permissions); + })); - this._groupService.createGroup$?.subscribe(d => { - if (d.error || !d.data || d.request.nounce != null && d.request.nounce.startsWith(this._client.session_id)) + this.subscriptions.push(this._groupService.deleteGroup$?.subscribe(d => { + if (d.error) { return; - - let index = -1; - for (let i = 0; i < this.items.length; i++) { - const comp = this.compare(d.data, this.items[i].group); - if (comp < 0) { - index = i; - break; - } } - this.items.splice(index >= 0 ? index : this.items.length, 0, { group: d.data, chatters: [], policies: [] }); - }); - this._groupService.updateGroup$?.subscribe(d => { - if (d.error || !d.data || d.request.nounce != null && d.request.nounce.startsWith(this._client.session_id)) + this._groupService.fetch().subscribe(data => this._groups = data.groups); + })); + + this.subscriptions.push(this._groupService.deleteChatter$?.subscribe(d => { + if (d.error) { return; - - const group = this.items.find(r => r.group.id = d.data.id)?.group; - if (group) { - group.id = d.data.id; - group.name = d.data.name; - group.priority = d.data.priority; } - }); - this._groupService.deleteGroup$?.subscribe(d => { - if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this._client.session_id)) + this._groupService.fetch().subscribe(data => this._chatters = data.chatters); + })); + + this.subscriptions.push(this._policyService.delete$?.subscribe(d => { + if (d.error) { return; + } - this.items = this.items.filter(r => r.group.id != d.request.data.id); - }); + this._policyService.fetch().subscribe(policies => this._policies = policies); + })); } + ngOnDestroy(): void { + if (this.subscriptions) { + for (let subscription of this.subscriptions) { + if (subscription) + subscription.unsubscribe(); + } + } + } + + get groups() { + return this._groups; + } + + get chatters() { + return this._chatters; + } + + get permissions() { + return this._permissions; + } + + get policies() { + return this._policies; + } openDialog(groupName: string): void { - const group = { id: '', user_id: '', name: groupName, priority: 0 }; + if (this.opened) { + return; + } + + this.opened = true; const dialogRef = this._dialog.open(GroupItemEditComponent, { - data: { group, isSpecial: groupName.length > 0 }, + data: { group: { id: '', user_id: '', name: groupName, priority: 0 }, isSpecial: groupName.length > 0 }, }); - const isNewGroup = group.id.length <= 0; - dialogRef.afterClosed().subscribe((result: Group | undefined) => { - if (!result) - return; - - - if (isNewGroup) { - this.items.push({ group: result, chatters: [], policies: [] }); - } else { - const same = this.items.find(i => i.group.id == group.id); - if (same == null) - return; - - same.group.name = result.name; - same.group.priority = result.priority; - } - }); + dialogRef.afterClosed().subscribe((result: Group | undefined) => this.opened = false); } compare(a: Group, b: Group) { - return a.name.localeCompare(b.name); + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); } exists(groupName: string) { - return this.items.some(g => g.group.name == groupName); + return this._groups.some(g => g.name == groupName); } } \ No newline at end of file diff --git a/src/app/hermes-client.service.ts b/src/app/hermes-client.service.ts index dcfaf4c..72d9821 100644 --- a/src/app/hermes-client.service.ts +++ b/src/app/hermes-client.service.ts @@ -112,6 +112,17 @@ export class HermesClientService { }); } + public createGroupPermission(groupId: string, path: string, allow: boolean | null) { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "create_group_permission", + data: { group: groupId, path, allow }, + }); + } + public createPolicy(groupId: string, path: string, usage: number, timespan: number) { if (!this.logged_in) return; @@ -119,9 +130,7 @@ export class HermesClientService { this.send(3, { request_id: null, type: "create_policy", - data: { - groupId, path, count: usage, span: timespan - }, + data: { groupId, path, count: usage, span: timespan }, }); } @@ -183,6 +192,17 @@ export class HermesClientService { }); } + public deleteGroupPermission(id: string) { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "delete_group_permission", + data: { id }, + }); + } + public deletePolicy(id: string) { if (!this.logged_in) return; @@ -252,13 +272,13 @@ export class HermesClientService { }); } - public fetchPermissionsAndGroups() { + public fetchPermissions() { if (!this.logged_in) return; this.send(3, { request_id: null, - type: "get_permissions", + type: "get_group_permissions", data: null, }); } @@ -331,6 +351,17 @@ export class HermesClientService { }); } + public updateGroupPermission(id: string, groupId: string, path: string, allow: boolean | null) { + if (!this.logged_in) + return; + + this.send(3, { + request_id: null, + type: "update_group_permission", + data: { id, group: groupId, path, allow }, + }); + } + public updatePolicy(id: string, groupId: string, path: string, usage: number, timespan: number) { if (!this.logged_in) return; @@ -338,9 +369,7 @@ export class HermesClientService { this.send(3, { request_id: null, type: "update_policy", - data: { - id, groupId, path, count: usage, span: timespan - }, + data: { id, groupId, path, count: usage, span: timespan }, }); } diff --git a/src/app/permissions/permission-item-edit/permission-item-edit.component.html b/src/app/permissions/permission-item-edit/permission-item-edit.component.html new file mode 100644 index 0000000..fff6e30 --- /dev/null +++ b/src/app/permissions/permission-item-edit/permission-item-edit.component.html @@ -0,0 +1,39 @@ + + + + {{data.isNew ? "Add" : "Edit"}} Group Permission + @if (data.group) { + in {{data.group.name}} + } + + + + + + + + Permission + + @for (item of states; track $index) { + {{item.label}} + } + + + + + + + + + + + @if (responseError) { + + {{responseError}} + + } + \ No newline at end of file diff --git a/src/app/permissions/permission-item-edit/permission-item-edit.component.scss b/src/app/permissions/permission-item-edit/permission-item-edit.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/permissions/permission-item-edit/permission-item-edit.component.spec.ts b/src/app/permissions/permission-item-edit/permission-item-edit.component.spec.ts new file mode 100644 index 0000000..0a6cef1 --- /dev/null +++ b/src/app/permissions/permission-item-edit/permission-item-edit.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PermissionItemEditComponent } from './permission-item-edit.component'; + +describe('PermissionItemEditComponent', () => { + let component: PermissionItemEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PermissionItemEditComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PermissionItemEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/permissions/permission-item-edit/permission-item-edit.component.ts b/src/app/permissions/permission-item-edit/permission-item-edit.component.ts new file mode 100644 index 0000000..5d8614e --- /dev/null +++ b/src/app/permissions/permission-item-edit/permission-item-edit.component.ts @@ -0,0 +1,99 @@ +import { Component, inject, OnInit } 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 { HermesClientService } from '../../hermes-client.service'; +import { Group } from '../../shared/models/group'; +import { Permission } from '../../shared/models/permission'; +import { MatSelectModule } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; +import { PolicyDropdownComponent } from "../../policies/policy-dropdown/policy-dropdown.component"; + +@Component({ + selector: 'permission-item-edit', + imports: [ + MatButtonModule, + MatCardModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatSelectModule, + ReactiveFormsModule, + PolicyDropdownComponent, +], + templateUrl: './permission-item-edit.component.html', + styleUrl: './permission-item-edit.component.scss' +}) +export class PermissionItemEditComponent implements OnInit { + private readonly client = inject(HermesClientService); + readonly data = inject<{ permission: Permission, group: Group, groups: Group[], isNew: boolean }>(MAT_DIALOG_DATA); + readonly dialogRef = inject(MatDialogRef); + + readonly pathControl = new FormControl('', [Validators.required]); + readonly stateControl = new FormControl(null, [Validators.required]); + private form = new FormGroup({ + path: this.pathControl, + state: this.stateControl, + }); + + readonly states: { label: string, value: boolean | null }[] = [ + { + label: 'Allow', value: true + }, + { + label: 'Deny', value: false + }, + ] + + responseError: string | undefined; + waitForResponse = false; + + + ngOnInit() { + this.pathControl.setValue(this.data.permission.path); + this.stateControl.setValue(this.data.permission.allow); + } + + submit() { + if (this.form.invalid || this.waitForResponse) { + return; + } + + this.waitForResponse = true; + this.responseError = undefined; + + if (this.data.isNew) { + this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_group_permission' && d.d.request.data.path == this.pathControl.value) + .subscribe({ + next: (d) => { + console.log('sdifhsdiofs data', d); + if (d.d.error) { + this.responseError = d.d.error; + } else { + this.dialogRef.close(d.d.data); + } + }, + error: () => this.responseError = 'Something went wrong.', + complete: () => this.waitForResponse = false, + }); + this.client.createGroupPermission(this.data.group.id, this.pathControl.value!, this.stateControl.value); + } else { + this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_group_permission' && d.d.request.data.id == this.data.permission.id) + .subscribe({ + next: (d) => { + if (d.d.error) { + this.responseError = d.d.error; + } else { + this.dialogRef.close(d.d.data); + } + }, + error: () => this.responseError = 'Something went wrong.', + complete: () => this.waitForResponse = false, + }); + this.client.updateGroupPermission(this.data.permission.id, this.data.group.id, this.pathControl.value!, this.stateControl.value); + } + } +} diff --git a/src/app/permissions/permission-item/permission-item.component.html b/src/app/permissions/permission-item/permission-item.component.html new file mode 100644 index 0000000..a8abd4c --- /dev/null +++ b/src/app/permissions/permission-item/permission-item.component.html @@ -0,0 +1,20 @@ +
+

{{permission.path}}

+
+ + + +
+
\ No newline at end of file diff --git a/src/app/permissions/permission-item/permission-item.component.scss b/src/app/permissions/permission-item/permission-item.component.scss new file mode 100644 index 0000000..a19f081 --- /dev/null +++ b/src/app/permissions/permission-item/permission-item.component.scss @@ -0,0 +1,39 @@ +section { + padding: 1em; +} + +.enabled { + border: 0.5em solid rgb(41, 255, 41); + border-top: 0; + border-right: 0; + border-bottom: 0; +} + +.disabled { + border: 0.5em solid rgb(255, 41, 41); + border-top: 0; + border-right: 0; + border-bottom: 0; +} + +.inherited { + border: 0.5em solid rgb(255, 255, 255); + border-top: 0; + border-right: 0; + border-bottom: 0; +} + +p { + margin-left: 0.5em; + display: inline; +} + +.right { + float: right; + display: inline; + padding-right: 1em; +} + +.right .mat-mdc-button { + align-items: center; +} \ No newline at end of file diff --git a/src/app/permissions/permission-item/permission-item.component.spec.ts b/src/app/permissions/permission-item/permission-item.component.spec.ts new file mode 100644 index 0000000..9342231 --- /dev/null +++ b/src/app/permissions/permission-item/permission-item.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PermissionItemComponent } from './permission-item.component'; + +describe('PermissionItemComponent', () => { + let component: PermissionItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PermissionItemComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PermissionItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/permissions/permission-item/permission-item.component.ts b/src/app/permissions/permission-item/permission-item.component.ts new file mode 100644 index 0000000..23cbf18 --- /dev/null +++ b/src/app/permissions/permission-item/permission-item.component.ts @@ -0,0 +1,79 @@ +import { Component, inject, Input } from '@angular/core'; +import { Permission } from '../../shared/models/permission'; +import { Group } from '../../shared/models/group'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDialog } from '@angular/material/dialog'; +import { HermesClientService } from '../../hermes-client.service'; +import EventService from '../../shared/services/EventService'; +import { PermissionItemEditComponent } from '../permission-item-edit/permission-item-edit.component'; + +@Component({ + selector: 'permission-item', + imports: [ + MatButtonModule, + MatIconModule, + ], + templateUrl: './permission-item.component.html', + styleUrl: './permission-item.component.scss' +}) +export class PermissionItemComponent { + @Input({ required: true }) permission: Permission = { id: '', group_id: '', user_id: '', path: '', allow: null }; + @Input({ required: true }) group: Group | undefined; + + readonly dialog = inject(MatDialog); + readonly client = inject(HermesClientService); + readonly events = inject(EventService); + + responseError: string | undefined; + waitForResponse = false; + opened = false; + + + delete() { + if (this.opened || this.waitForResponse) { + return; + } + + this.waitForResponse = true; + this.responseError = undefined; + + const permissionId = this.permission.id; + + this.client.first((d: any) => d.op == 4 && d.d.request.type == 'delete_group_permission' && d.d.request.data.id == permissionId) + .subscribe({ + next: (d) => { + if (d.d.error) { + this.responseError = d.d.error; + } else { + this.events.emit('delete_group_permission', permissionId); + } + }, + error: () => this.responseError = 'Something went wrong.', + complete: () => this.waitForResponse = false, + }); + this.client.deleteGroupPermission(this.permission.id); + } + + edit() { + if (this.opened || this.waitForResponse) { + return; + } + + this.opened = true; + + const dialogRef = this.dialog.open(PermissionItemEditComponent, { + data: { permission: this.permission, group: this.group, groups: [], isNew: false }, + }); + + dialogRef.afterClosed().subscribe((permission: Permission) => { + this.opened = false; + if (!permission) + return; + + this.permission.group_id = permission.group_id; + this.permission.path = permission.path; + this.permission.allow = permission.allow; + }); + } +} diff --git a/src/app/permissions/permission-list/permission-list.component.html b/src/app/permissions/permission-list/permission-list.component.html new file mode 100644 index 0000000..554c784 --- /dev/null +++ b/src/app/permissions/permission-list/permission-list.component.html @@ -0,0 +1,29 @@ +
    +
  • + + Filter + + + + +
  • + @for (permission of permissions; track permission.id) { +
  • + +
  • + } + @if (!permissions.length) { + @if (searchControl.value) { +

    No permission matches the filter.

    + } @else { +

    This group has no permissions. Cannot do anything.

    + } + } +
\ No newline at end of file diff --git a/src/app/permissions/permission-list/permission-list.component.scss b/src/app/permissions/permission-list/permission-list.component.scss new file mode 100644 index 0000000..e3e4013 --- /dev/null +++ b/src/app/permissions/permission-list/permission-list.component.scss @@ -0,0 +1,38 @@ +@use '@angular/material' as mat; + +ul { + @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), + )); + + background-color: rgb(202, 68, 255); + border-radius: 15px; + margin: 0 0; + padding: 0; + max-width: 500px; + overflow: hidden; +} + +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; +} \ No newline at end of file diff --git a/src/app/permissions/permission-list/permission-list.component.spec.ts b/src/app/permissions/permission-list/permission-list.component.spec.ts new file mode 100644 index 0000000..0f6aaea --- /dev/null +++ b/src/app/permissions/permission-list/permission-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PermissionListComponent } from './permission-list.component'; + +describe('PermissionListComponent', () => { + let component: PermissionListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PermissionListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PermissionListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/permissions/permission-list/permission-list.component.ts b/src/app/permissions/permission-list/permission-list.component.ts new file mode 100644 index 0000000..eb80d3e --- /dev/null +++ b/src/app/permissions/permission-list/permission-list.component.ts @@ -0,0 +1,65 @@ +import { Component, inject, Input } from '@angular/core'; +import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { containsLettersInOrder } from '../../shared/utils/string-compare'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialog } from '@angular/material/dialog'; +import { HermesClientService } from '../../hermes-client.service'; +import { Group } from '../../shared/models/group'; +import EventService from '../../shared/services/EventService'; +import { PermissionItemComponent } from '../permission-item/permission-item.component'; +import { Permission } from '../../shared/models/permission'; +import { PermissionItemEditComponent } from '../permission-item-edit/permission-item-edit.component'; + +@Component({ + selector: 'permission-list', + imports: [ + MatButtonModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + ReactiveFormsModule, + PermissionItemComponent, + ], + templateUrl: './permission-list.component.html', + styleUrl: './permission-list.component.scss' +}) +export class PermissionListComponent { + @Input({ required: true }) groups: Group[] = []; + @Input({ alias: 'permissions', required: true }) _permissions: Permission[] = []; + @Input() group: Group | undefined; + + readonly dialog = inject(MatDialog); + readonly client = inject(HermesClientService); + readonly events = inject(EventService); + readonly searchControl = new FormControl(''); + + opened = false; + + + add() { + if (!this.group || this.opened) { + return; + } + + this.opened = true; + + const groupId = this.group.id; + + const dialogRef = this.dialog.open(PermissionItemEditComponent, { + data: { permission: { id: '', user_id: '', group_id: groupId, path: '', allow: null }, group: this.group, groups: [], isNew: true }, + }); + + dialogRef.afterClosed().subscribe((permission: Permission) => this.opened = false); + } + + get permissions() { + return this._permissions.filter(p => containsLettersInOrder(p.path, this.searchControl.value)); + } + + getGroupById(groupId: string) { + return this.groups.find(g => g.id == groupId); + } +} \ No newline at end of file diff --git a/src/app/permissions/permissions.module.ts b/src/app/permissions/permissions.module.ts new file mode 100644 index 0000000..6e116d9 --- /dev/null +++ b/src/app/permissions/permissions.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + + + +@NgModule({ + declarations: [], + imports: [ + CommonModule + ] +}) +export class PermissionsModule { } diff --git a/src/app/policies/policy-dropdown/policy-dropdown.component.ts b/src/app/policies/policy-dropdown/policy-dropdown.component.ts index d4e95cc..318e737 100644 --- a/src/app/policies/policy-dropdown/policy-dropdown.component.ts +++ b/src/app/policies/policy-dropdown/policy-dropdown.component.ts @@ -25,7 +25,7 @@ const Policies = [ { path: "tts.commands.version", description: "To use !version command" }, { path: "tts.commands.voice", description: "To use !voice command" }, { path: "tts.commands.voice.admin", description: "To use !voice command on others" }, -] +]; @Component({ selector: 'policy-dropdown', @@ -42,7 +42,7 @@ const Policies = [ }) export class PolicyDropdownComponent { @Input() policy: string | null = ''; - policyControl = new FormControl('', [Validators.required]); + @Input({ alias: 'control' }) policyControl = new FormControl('', [Validators.required]); filteredPolicies: Observable; constructor() { diff --git a/src/app/policies/policy-table/policy-table.component.html b/src/app/policies/policy-table/policy-table.component.html index 5269179..06239a3 100644 --- a/src/app/policies/policy-table/policy-table.component.html +++ b/src/app/policies/policy-table/policy-table.component.html @@ -45,9 +45,10 @@ diff --git a/src/app/shared/models/permission.ts b/src/app/shared/models/permission.ts new file mode 100644 index 0000000..5012105 --- /dev/null +++ b/src/app/shared/models/permission.ts @@ -0,0 +1,7 @@ +export interface Permission { + id: string; + user_id: string; + group_id: string; + path: string; + allow: boolean | null; +} \ No newline at end of file diff --git a/src/app/shared/resolvers/permission-resolver.ts b/src/app/shared/resolvers/permission-resolver.ts new file mode 100644 index 0000000..bb3a83b --- /dev/null +++ b/src/app/shared/resolvers/permission-resolver.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { Permission } from '../models/permission'; +import { PermissionService } from '../services/permission.service'; + +@Injectable({ providedIn: 'root' }) +export default class PermissionResolver implements Resolve { + constructor(private service: PermissionService) { } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.service.fetch(); + } +} \ No newline at end of file diff --git a/src/app/shared/services/group.service.ts b/src/app/shared/services/group.service.ts index d46a294..972f74b 100644 --- a/src/app/shared/services/group.service.ts +++ b/src/app/shared/services/group.service.ts @@ -51,7 +51,7 @@ export default class GroupService { chatter.group_id = d.data.group_id; } }); - this.deleteChatter$?.subscribe(d => this.chatters = this.chatters.filter(r => r.group_id != d.request.data.group_id && r.chatter_id != d.request.data.chatter_id)); + this.deleteChatter$?.subscribe(d => this.chatters = this.chatters.filter(r => r.group_id != d.request.data.group && r.chatter_id != d.request.data.chatter)); this.events.listen('tts_logoff', () => { this.groups = []; diff --git a/src/app/shared/services/permission.service.spec.ts b/src/app/shared/services/permission.service.spec.ts new file mode 100644 index 0000000..1a77304 --- /dev/null +++ b/src/app/shared/services/permission.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { PermissionService } from './permission.service'; + +describe('PermissionService', () => { + let service: PermissionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PermissionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/services/permission.service.ts b/src/app/shared/services/permission.service.ts new file mode 100644 index 0000000..2cc8c5b --- /dev/null +++ b/src/app/shared/services/permission.service.ts @@ -0,0 +1,72 @@ +import { inject, Injectable } from '@angular/core'; +import { HermesClientService } from '../../hermes-client.service'; +import EventService from './EventService'; +import { Permission } from '../models/permission'; +import { map, Observable, of } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class PermissionService { + private readonly client = inject(HermesClientService); + private readonly events = inject(EventService); + private data: Permission[] = []; + private loaded = false; + create$: Observable | undefined; + update$: Observable | undefined; + delete$: Observable | undefined; + + constructor() { + this.create$ = this.client.filterByRequestType('create_group_permission'); + this.update$ = this.client.filterByRequestType('update_group_permission'); + this.delete$ = this.client.filterByRequestType('delete_group_permission'); + + this.create$?.subscribe(d => { + if (d.error) { + return; + } + + this.data.push(d.data); + }); + this.update$?.subscribe(d => { + if (d.error) { + return; + } + + const permission = this.data.find(p => p.id == d.data.id); + if (permission) { + permission.group_id = d.data.group_id; + permission.path = d.data.path; + permission.allow = d.data.allow; + permission.user_id = d.data.user_id; + } + }); + this.delete$?.subscribe(d => { + if (d.error) { + return; + } + + this.data = this.data.filter(r => r.id != d.request.data.id); + }); + + this.events.listen('tts_logoff', () => { + this.data = []; + this.loaded = false; + }); + } + + + fetch() { + if (this.loaded) { + return of(this.data); + } + + const $ = this.client.first(d => d.d.request.type == 'get_group_permissions')!.pipe(map(d => d.d.data)); + $.subscribe(d => { + this.data = d; + this.loaded = true; + }); + this.client.fetchPermissions(); + return $; + } +} diff --git a/src/app/shared/utils/string-compare.ts b/src/app/shared/utils/string-compare.ts index ab8d468..283c517 100644 --- a/src/app/shared/utils/string-compare.ts +++ b/src/app/shared/utils/string-compare.ts @@ -1,6 +1,10 @@ -export function containsLettersInOrder(value: string, inside: string): boolean { +export function containsLettersInOrder(value: string | null, inside: string | null): boolean { + if (!inside) + return true; + if (!value) + return false; return containsLettersInOrderInternal(value, inside, 0, 0); } @@ -11,7 +15,7 @@ function containsLettersInOrderInternal(value: string, inside: string, indexValu if (indexValue >= value.length) { return false; } - + const match = value.at(indexValue) == inside.at(indexInside); return containsLettersInOrderInternal(value, inside, indexValue + 1, indexInside + (match ? 1 : 0)); } \ No newline at end of file diff --git a/src/app/twitch-users/twitch-user-item-add/twitch-user-item-add.component.ts b/src/app/twitch-users/twitch-user-item-add/twitch-user-item-add.component.ts index 1db3b5d..589a56e 100644 --- a/src/app/twitch-users/twitch-user-item-add/twitch-user-item-add.component.ts +++ b/src/app/twitch-users/twitch-user-item-add/twitch-user-item-add.component.ts @@ -74,7 +74,7 @@ export class TwitchUserItemAddComponent implements OnInit { error: () => this.responseError = 'Something went wrong.', complete: () => this.waitForResponse = false, }); - this.client.createGroupChatter(this.data.group.id, response.user.id, response.user.login) + this.client.createGroupChatter(this.data.group.id, response.user.id, response.user.login); }); } } diff --git a/src/app/twitch-users/twitch-user-item/twitch-user-item.component.html b/src/app/twitch-users/twitch-user-item/twitch-user-item.component.html index 96799d1..c3cd334 100644 --- a/src/app/twitch-users/twitch-user-item/twitch-user-item.component.html +++ b/src/app/twitch-users/twitch-user-item/twitch-user-item.component.html @@ -1,5 +1,6 @@
diff --git a/src/app/twitch-users/twitch-user-item/twitch-user-item.component.ts b/src/app/twitch-users/twitch-user-item/twitch-user-item.component.ts index c7f1ce4..c6c827b 100644 --- a/src/app/twitch-users/twitch-user-item/twitch-user-item.component.ts +++ b/src/app/twitch-users/twitch-user-item/twitch-user-item.component.ts @@ -21,18 +21,28 @@ export class TwitchUserItemComponent { private readonly _client = inject(HermesClientService); private readonly _events = inject(EventService); - private _deleted = false; + + waitForResponse = false; + responseError: string | undefined; delete() { - if (this._deleted) + if (this.waitForResponse) return; - this._deleted = true; + this.waitForResponse = true; + this.responseError = undefined; this._client.first(d => d.d.request.type == 'delete_group_chatter' && d.d.request.data.group == this.user.group_id && d.d.request.data.chatter == this.user.chatter_id) - .subscribe(async (response) => { - console.log('delete group chatter', response) - this._events.emit('delete_group_chatter', this.user); + .subscribe({ + next: (d) => { + if (d.d.error) { + this.responseError = d.d.error; + } else { + this._events.emit('delete_group_chatter', this.user); + } + }, + error: () => this.responseError = 'Something went wrong.', + complete: () => this.waitForResponse = false, }); this._client.deleteGroupChatter(this.user.group_id, this.user.chatter_id.toString()); } diff --git a/src/app/twitch-users/twitch-user-list/twitch-user-list.component.ts b/src/app/twitch-users/twitch-user-list/twitch-user-list.component.ts index 1ff276c..24a67d4 100644 --- a/src/app/twitch-users/twitch-user-list/twitch-user-list.component.ts +++ b/src/app/twitch-users/twitch-user-list/twitch-user-list.component.ts @@ -8,10 +8,8 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { containsLettersInOrder } from '../../shared/utils/string-compare'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; -import { HermesClientService } from '../../hermes-client.service'; import { Group } from '../../shared/models/group'; import { TwitchUserItemAddComponent } from '../twitch-user-item-add/twitch-user-item-add.component'; -import EventService from '../../shared/services/EventService'; @Component({ selector: 'twitch-user-list', @@ -31,17 +29,10 @@ export class TwitchUserListComponent { @Input() group: Group | undefined; readonly dialog = inject(MatDialog); - readonly client = inject(HermesClientService); - readonly events = inject(EventService); readonly searchControl: FormControl = new FormControl(''); opened = false; - constructor() { - this.events.listen('delete_group_chatter', (chatter: GroupChatter) => { - this.twitchUsers.splice(this.twitchUsers.findIndex(c => c.group_id == chatter.group_id && c.chatter_id == chatter.chatter_id), 1); - }); - } get users(): GroupChatter[] { return this.twitchUsers.filter(u => containsLettersInOrder(u.chatter_label, this.searchControl.value)); @@ -57,12 +48,6 @@ export class TwitchUserListComponent { data: { username: this.searchControl.value, group: this.group }, }); - dialogRef.afterClosed().subscribe((chatter: GroupChatter) => { - this.opened = false; - if (!chatter) - return; - - this.twitchUsers.push(chatter); - }); + dialogRef.afterClosed().subscribe((chatter: GroupChatter) => this.opened = false); } } diff --git a/src/styles.scss b/src/styles.scss index db06f4e..9e41d52 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,5 +1,7 @@ /* You can add global styles to this file, and also import other style files */ +@use '@angular/material' as mat; + html, body { height: 100%; @@ -34,4 +36,45 @@ body { /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { background: rgb(122, 122, 122); +} + +.mat-small { + @include mat.all-component-densities(-5); +} + +.mat-large { + @include mat.all-component-densities(5); +} + +.confirm { + @include mat.button-overrides((text-state-layer-color: rgb(52, 255, 62), + text-label-text-color: rgb(71, 218, 78), + text-disabled-label-text-color: rgb(71, 218, 78), + )); +} + +.neutral { + @include mat.button-overrides((text-state-layer-color: rgb(64, 141, 255), + text-label-text-color: rgb(52, 106, 255), + text-disabled-label-text-color: rgb(52, 106, 255), + )); +} + +.warning { + @include mat.button-overrides((text-state-layer-color: rgb(255, 172, 63), + text-label-text-color: rgb(255, 145, 19), + text-disabled-label-text-color: rgb(255, 145, 19), + )); +} + +.danger { + @include mat.button-overrides((text-state-layer-color: rgb(255, 48, 48), + text-label-text-color: rgb(255, 52, 52), + text-disabled-label-text-color: rgb(255, 52, 52), + filled-label-text-color: rgb(255, 52, 52), + outlined-label-text-color: rgb(255, 52, 52), + protected-label-text-color: rgb(255, 52, 52), + protected-state-layer-color: rgb(255, 75, 75), + protected-ripple-color: rgb(255, 154, 154), + )); } \ No newline at end of file