Added group permissions. Added some global styles. Made groups rely on services' data.

This commit is contained in:
Tom
2025-03-22 21:58:30 +00:00
parent d19c5445d6
commit 9de4424736
38 changed files with 936 additions and 137 deletions

View File

@ -1,24 +1,28 @@
<article>
<section class="title">{{item().group.name}}
<section class="title">{{group().name}}
@if (special) {
<small class="tag">auto-generated</small>
}
</section>
<section class="">
{{item().group.priority}}
{{group().priority}}
<small class="muted block">priority</small>
</section>
<section>
@if (special) {
<p class="muted">Unknown</p>
} @else {
{{item().chatters.length}}
<small class="muted block">user{{item().chatters.length == 1 ? '' : 's'}}</small>
{{chatters().length}}
<small class="muted block">user{{chatters().length == 1 ? '' : 's'}}</small>
}
</section>
<section>
{{item().policies.length}}
<small class="muted block">polic{{item().chatters.length == 1 ? 'y' : 'ies'}}</small>
{{permissions().length}}
<small class="muted block">permission{{permissions().length == 1 ? '' : 's'}}</small>
</section>
<section>
{{policies().length}}
<small class="muted block">polic{{policies().length == 1 ? 'y' : 'ies'}}</small>
</section>
<section>
<button mat-button

View File

@ -7,6 +7,7 @@ import { Policy } from '../../shared/models/policy';
import { Router } from '@angular/router';
import { GroupChatter } from '../../shared/models/group-chatter';
import { SpecialGroups } from '../../shared/utils/groups';
import { Permission } from '../../shared/models/permission';
@Component({
selector: 'group-item',
@ -21,12 +22,15 @@ import { SpecialGroups } from '../../shared/utils/groups';
})
export class GroupItemComponent implements OnInit {
readonly router = inject(Router);
item = input.required<{ group: Group, chatters: GroupChatter[], policies: Policy[] }>();
group = input.required<Group>();
chatters = input.required<GroupChatter[]>();
permissions = input.required<Permission[]>();
policies = input.required<Policy[]>();
link: string = '';
special: boolean = true;
ngOnInit() {
this.special = SpecialGroups.includes(this.item().group.name);
this.link = 'groups/' + this.item().group.id;
this.special = SpecialGroups.includes(this.group().name);
this.link = 'groups/' + this.group().id;
}
}

View File

@ -1,7 +1,10 @@
<ul>
@for (group of groups; track $index) {
@for (group of groups; track group.id) {
<li>
<group-item [item]="group" />
<group-item [group]="group"
[chatters]="getChattersByGroup(group.id)"
[permissions]="getPermissionsByGroup(group.id)"
[policies]="getPoliciesByGroup(group.id)" />
</li>
}
</ul>

View File

@ -1,8 +1,9 @@
import { Component, Input } from '@angular/core';
import { Component, input, Input } from '@angular/core';
import { Group } from '../../shared/models/group';
import { GroupItemComponent } from "../group-item/group-item.component";
import { Policy } from '../../shared/models/policy';
import { GroupChatter } from '../../shared/models/group-chatter';
import { Permission } from '../../shared/models/permission';
@Component({
selector: 'group-list',
@ -12,25 +13,35 @@ import { GroupChatter } from '../../shared/models/group-chatter';
styleUrl: './group-list.component.scss'
})
export class GroupListComponent {
private _groups: { group: Group, chatters: GroupChatter[], policies: Policy[] }[] = [];
private _filter: (item: { group: Group, chatters: GroupChatter[], policies: Policy[] }) => boolean = _ => true;
_groups = input.required<Group[]>({ alias: 'groups' });
chatters = input.required<GroupChatter[]>();
permissions = input.required<Permission[]>();
policies = input.required<Policy[]>();
private _filter: (item: Group) => boolean = _ => true;
get filter(): (item: { group: Group, chatters: GroupChatter[], policies: Policy[] }) => boolean {
get filter(): (group: Group) => boolean {
return this._filter;
}
@Input({ alias: 'filter', required: false })
set filter(value: (item: { group: Group, chatters: GroupChatter[], policies: Policy[] }) => boolean) {
set filter(value: (item: Group) => boolean) {
this._filter = value;
}
get groups() {
return this._groups.filter(this._filter);
return this._groups().filter(g => this._filter(g));
}
@Input({ alias: 'groups', required: true })
set groups(value: { group: Group, chatters: GroupChatter[], policies: Policy[] }[]) {
this._groups = value;
getChattersByGroup(groupId: string) {
return this.chatters().filter(c => c.group_id == groupId);
}
getPermissionsByGroup(groupId: string) {
return this.permissions().filter(c => c.group_id == groupId);
}
getPoliciesByGroup(groupId: string) {
return this.policies().filter(c => c.group_id == groupId);
}
}

View File

@ -14,6 +14,18 @@
</mat-expansion-panel>
}
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Permissions</mat-panel-title>
<mat-panel-description class="muted">
{{permissions.length}} permission{{permissions.length == 1 ? '' : 's'}}
</mat-panel-description>
</mat-expansion-panel-header>
<permission-list [permissions]="permissions"
[groups]="groups"
[group]="group" />
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Policies</mat-panel-title>
@ -43,7 +55,7 @@
</article>
<article class="right">
<button mat-raised-button
class="delete"
class="danger"
(click)="delete()">
<mat-icon>delete</mat-icon>Delete this group.
</button>

View File

@ -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);
}

View File

@ -14,4 +14,7 @@
}
</mat-menu>
<group-list class="groups"
[groups]="items" />
[groups]="groups"
[chatters]="chatters"
[permissions]="permissions"
[policies]="policies" />

View File

@ -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);
}
}