Simple Managment webapp [LLM]
This commit is contained in:
@@ -5,6 +5,7 @@ import { environment } from '../../../environments/environment';
|
|||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
UserWithExtra,
|
UserWithExtra,
|
||||||
|
UserPreview,
|
||||||
Message,
|
Message,
|
||||||
MessageListParams,
|
MessageListParams,
|
||||||
MessageListResponse,
|
MessageListResponse,
|
||||||
@@ -41,6 +42,10 @@ export class ApiService {
|
|||||||
return this.http.get<UserWithExtra>(`${this.baseUrl}/users/${userId}`);
|
return this.http.get<UserWithExtra>(`${this.baseUrl}/users/${userId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserPreview(userId: string): Observable<UserPreview> {
|
||||||
|
return this.http.get<UserPreview>(`${this.baseUrl}/preview/users/${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
updateUser(userId: string, data: { username?: string; pro_token?: string }): Observable<User> {
|
updateUser(userId: string, data: { username?: string; pro_token?: string }): Observable<User> {
|
||||||
return this.http.patch<User>(`${this.baseUrl}/users/${userId}`, data);
|
return this.http.patch<User>(`${this.baseUrl}/users/${userId}`, data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './auth.service';
|
export * from './auth.service';
|
||||||
export * from './api.service';
|
export * from './api.service';
|
||||||
export * from './notification.service';
|
export * from './notification.service';
|
||||||
|
export * from './user-cache.service';
|
||||||
|
|||||||
55
webapp/src/app/core/services/user-cache.service.ts
Normal file
55
webapp/src/app/core/services/user-cache.service.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable, inject, signal } from '@angular/core';
|
||||||
|
import { Observable, of, tap, catchError, map, shareReplay } from 'rxjs';
|
||||||
|
import { ApiService } from './api.service';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { UserPreview } from '../models';
|
||||||
|
|
||||||
|
export interface ResolvedUser {
|
||||||
|
userId: string;
|
||||||
|
displayName: string;
|
||||||
|
isCurrentUser: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UserCacheService {
|
||||||
|
private apiService = inject(ApiService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
|
||||||
|
private cache = new Map<string, Observable<UserPreview | null>>();
|
||||||
|
|
||||||
|
resolveUser(userId: string): Observable<ResolvedUser> {
|
||||||
|
const currentUserId = this.authService.getUserId();
|
||||||
|
const isCurrentUser = userId === currentUserId;
|
||||||
|
|
||||||
|
return this.getUserPreview(userId).pipe(
|
||||||
|
map(preview => {
|
||||||
|
let displayName = preview?.username || userId;
|
||||||
|
if (isCurrentUser) {
|
||||||
|
displayName += ' (you)';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
displayName,
|
||||||
|
isCurrentUser
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUserPreview(userId: string): Observable<UserPreview | null> {
|
||||||
|
if (!this.cache.has(userId)) {
|
||||||
|
const request$ = this.apiService.getUserPreview(userId).pipe(
|
||||||
|
catchError(() => of(null)),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
this.cache.set(userId, request$);
|
||||||
|
}
|
||||||
|
return this.cache.get(userId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
<nz-card class="login-card">
|
<nz-card class="login-card">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<h1>SimpleCloudNotifier</h1>
|
<h1>SimpleCloudNotifier</h1>
|
||||||
<p>Sign in to manage your notifications</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (error()) {
|
@if (error()) {
|
||||||
@@ -14,10 +13,10 @@
|
|||||||
></nz-alert>
|
></nz-alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
<form (ngSubmit)="login()">
|
<form nz-form nzLayout="horizontal" (ngSubmit)="login()">
|
||||||
<nz-form-item>
|
<nz-form-item>
|
||||||
<nz-form-label>User ID</nz-form-label>
|
<nz-form-label [nzSpan]="7">User ID</nz-form-label>
|
||||||
<nz-form-control>
|
<nz-form-control [nzSpan]="17">
|
||||||
<nz-input-group nzPrefixIcon="user">
|
<nz-input-group nzPrefixIcon="user">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -32,8 +31,8 @@
|
|||||||
</nz-form-item>
|
</nz-form-item>
|
||||||
|
|
||||||
<nz-form-item>
|
<nz-form-item>
|
||||||
<nz-form-label>Admin Key</nz-form-label>
|
<nz-form-label [nzSpan]="7">Admin Key</nz-form-label>
|
||||||
<nz-form-control>
|
<nz-form-control [nzSpan]="17">
|
||||||
<nz-input-group nzPrefixIcon="key" [nzSuffix]="keySuffix">
|
<nz-input-group nzPrefixIcon="key" [nzSuffix]="keySuffix">
|
||||||
<input
|
<input
|
||||||
[type]="showKey() ? 'text' : 'password'"
|
[type]="showKey() ? 'text' : 'password'"
|
||||||
@@ -69,7 +68,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="login-footer">
|
<div class="login-footer">
|
||||||
<p>You need an admin key (with "A" permission) to access the dashboard.</p>
|
<p>You need an admin key to access the dashboard.</p>
|
||||||
</div>
|
</div>
|
||||||
</nz-card>
|
</nz-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -159,8 +159,10 @@
|
|||||||
[nzData]="subscriptions()"
|
[nzData]="subscriptions()"
|
||||||
[nzLoading]="loadingSubscriptions()"
|
[nzLoading]="loadingSubscriptions()"
|
||||||
[nzShowPagination]="false"
|
[nzShowPagination]="false"
|
||||||
|
[nzNoResult]="noResultTpl"
|
||||||
nzSize="small"
|
nzSize="small"
|
||||||
>
|
>
|
||||||
|
<ng-template #noResultTpl></ng-template>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Subscriber</th>
|
<th>Subscriber</th>
|
||||||
|
|||||||
@@ -6,37 +6,27 @@
|
|||||||
<span nz-icon nzType="reload"></span>
|
<span nz-icon nzType="reload"></span>
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
<button nz-button nzType="primary" (click)="openCreateModal()">
|
|
||||||
<span nz-icon nzType="plus"></span>
|
|
||||||
Create Channel
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nz-card class="filter-card">
|
|
||||||
<nz-radio-group [(ngModel)]="selector" (ngModelChange)="onSelectorChange()">
|
|
||||||
<label nz-radio-button nzValue="all">All</label>
|
|
||||||
<label nz-radio-button nzValue="owned">Owned</label>
|
|
||||||
<label nz-radio-button nzValue="subscribed">Subscribed</label>
|
|
||||||
<label nz-radio-button nzValue="subscribed_any">Subscribed (Any)</label>
|
|
||||||
</nz-radio-group>
|
|
||||||
</nz-card>
|
|
||||||
|
|
||||||
<nz-card>
|
<nz-card>
|
||||||
<nz-table
|
<nz-table
|
||||||
#channelTable
|
#channelTable
|
||||||
[nzData]="channels()"
|
[nzData]="channels()"
|
||||||
[nzLoading]="loading()"
|
[nzLoading]="loading()"
|
||||||
[nzShowPagination]="false"
|
[nzShowPagination]="false"
|
||||||
|
[nzNoResult]="noResultTpl"
|
||||||
nzSize="middle"
|
nzSize="middle"
|
||||||
>
|
>
|
||||||
|
<ng-template #noResultTpl></ng-template>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th nzWidth="25%">Name</th>
|
<th nzWidth="20%">Name</th>
|
||||||
<th nzWidth="20%">Internal Name</th>
|
<th nzWidth="15%">Internal Name</th>
|
||||||
|
<th nzWidth="15%">Owner</th>
|
||||||
<th nzWidth="15%">Status</th>
|
<th nzWidth="15%">Status</th>
|
||||||
<th nzWidth="15%">Messages</th>
|
<th nzWidth="15%">Messages</th>
|
||||||
<th nzWidth="25%">Last Sent</th>
|
<th nzWidth="20%">Last Sent</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -51,6 +41,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class="mono">{{ channel.internal_name }}</span>
|
<span class="mono">{{ channel.internal_name }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ getOwnerDisplayName(channel.owner_user_id) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<nz-tag [nzColor]="getSubscriptionStatus(channel).color">
|
<nz-tag [nzColor]="getSubscriptionStatus(channel).color">
|
||||||
{{ getSubscriptionStatus(channel).label }}
|
{{ getSubscriptionStatus(channel).label }}
|
||||||
@@ -69,7 +60,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5">
|
<td colspan="6">
|
||||||
<nz-empty nzNotFoundContent="No channels found"></nz-empty>
|
<nz-empty nzNotFoundContent="No channels found"></nz-empty>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -79,32 +70,3 @@
|
|||||||
</nz-card>
|
</nz-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create Channel Modal -->
|
|
||||||
<nz-modal
|
|
||||||
[(nzVisible)]="showCreateModal"
|
|
||||||
nzTitle="Create Channel"
|
|
||||||
(nzOnCancel)="closeCreateModal()"
|
|
||||||
(nzOnOk)="createChannel()"
|
|
||||||
[nzOkLoading]="creating()"
|
|
||||||
[nzOkDisabled]="!newChannelName.trim()"
|
|
||||||
>
|
|
||||||
<ng-container *nzModalContent>
|
|
||||||
<nz-form-item>
|
|
||||||
<nz-form-label>Channel Name</nz-form-label>
|
|
||||||
<nz-form-control>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
nz-input
|
|
||||||
placeholder="Enter channel name"
|
|
||||||
[(ngModel)]="newChannelName"
|
|
||||||
/>
|
|
||||||
</nz-form-control>
|
|
||||||
</nz-form-item>
|
|
||||||
|
|
||||||
<nz-form-item class="mb-0">
|
|
||||||
<label nz-checkbox [(ngModel)]="newChannelSubscribe">
|
|
||||||
Subscribe to this channel
|
|
||||||
</label>
|
|
||||||
</nz-form-item>
|
|
||||||
</ng-container>
|
|
||||||
</nz-modal>
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Component, inject, signal, OnInit } from '@angular/core';
|
import { Component, inject, signal, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { NzTableModule } from 'ng-zorro-antd/table';
|
import { NzTableModule } from 'ng-zorro-antd/table';
|
||||||
import { NzButtonModule } from 'ng-zorro-antd/button';
|
import { NzButtonModule } from 'ng-zorro-antd/button';
|
||||||
import { NzIconModule } from 'ng-zorro-antd/icon';
|
import { NzIconModule } from 'ng-zorro-antd/icon';
|
||||||
@@ -9,16 +8,11 @@ import { NzTagModule } from 'ng-zorro-antd/tag';
|
|||||||
import { NzBadgeModule } from 'ng-zorro-antd/badge';
|
import { NzBadgeModule } from 'ng-zorro-antd/badge';
|
||||||
import { NzEmptyModule } from 'ng-zorro-antd/empty';
|
import { NzEmptyModule } from 'ng-zorro-antd/empty';
|
||||||
import { NzCardModule } from 'ng-zorro-antd/card';
|
import { NzCardModule } from 'ng-zorro-antd/card';
|
||||||
import { NzRadioModule } from 'ng-zorro-antd/radio';
|
|
||||||
import { NzModalModule, NzModalService } from 'ng-zorro-antd/modal';
|
|
||||||
import { NzFormModule } from 'ng-zorro-antd/form';
|
|
||||||
import { NzInputModule } from 'ng-zorro-antd/input';
|
|
||||||
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
|
|
||||||
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||||
import { ApiService } from '../../../core/services/api.service';
|
import { ApiService } from '../../../core/services/api.service';
|
||||||
import { AuthService } from '../../../core/services/auth.service';
|
import { AuthService } from '../../../core/services/auth.service';
|
||||||
import { NotificationService } from '../../../core/services/notification.service';
|
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
||||||
import { ChannelWithSubscription, ChannelSelector } from '../../../core/models';
|
import { ChannelWithSubscription } from '../../../core/models';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -26,7 +20,6 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
|
||||||
NzTableModule,
|
NzTableModule,
|
||||||
NzButtonModule,
|
NzButtonModule,
|
||||||
NzIconModule,
|
NzIconModule,
|
||||||
@@ -34,11 +27,6 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
|||||||
NzBadgeModule,
|
NzBadgeModule,
|
||||||
NzEmptyModule,
|
NzEmptyModule,
|
||||||
NzCardModule,
|
NzCardModule,
|
||||||
NzRadioModule,
|
|
||||||
NzModalModule,
|
|
||||||
NzFormModule,
|
|
||||||
NzInputModule,
|
|
||||||
NzCheckboxModule,
|
|
||||||
NzToolTipModule,
|
NzToolTipModule,
|
||||||
RelativeTimePipe,
|
RelativeTimePipe,
|
||||||
],
|
],
|
||||||
@@ -48,19 +36,12 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
|||||||
export class ChannelListComponent implements OnInit {
|
export class ChannelListComponent implements OnInit {
|
||||||
private apiService = inject(ApiService);
|
private apiService = inject(ApiService);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private notification = inject(NotificationService);
|
private userCacheService = inject(UserCacheService);
|
||||||
private modal = inject(NzModalService);
|
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
|
||||||
channels = signal<ChannelWithSubscription[]>([]);
|
channels = signal<ChannelWithSubscription[]>([]);
|
||||||
|
ownerNames = signal<Map<string, ResolvedUser>>(new Map());
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
selector: ChannelSelector = 'all';
|
|
||||||
|
|
||||||
// Create channel modal
|
|
||||||
showCreateModal = signal(false);
|
|
||||||
newChannelName = '';
|
|
||||||
newChannelSubscribe = true;
|
|
||||||
creating = signal(false);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadChannels();
|
this.loadChannels();
|
||||||
@@ -71,10 +52,11 @@ export class ChannelListComponent implements OnInit {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.apiService.getChannels(userId, this.selector).subscribe({
|
this.apiService.getChannels(userId, 'all_any').subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.channels.set(response.channels);
|
this.channels.set(response.channels);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
this.resolveOwnerNames(response.channels);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
@@ -82,45 +64,24 @@ export class ChannelListComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectorChange(): void {
|
private resolveOwnerNames(channels: ChannelWithSubscription[]): void {
|
||||||
this.loadChannels();
|
const uniqueOwnerIds = [...new Set(channels.map(c => c.owner_user_id))];
|
||||||
|
for (const ownerId of uniqueOwnerIds) {
|
||||||
|
this.userCacheService.resolveUser(ownerId).subscribe(resolved => {
|
||||||
|
this.ownerNames.update(map => new Map(map).set(ownerId, resolved));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getOwnerDisplayName(ownerId: string): string {
|
||||||
|
const resolved = this.ownerNames().get(ownerId);
|
||||||
|
return resolved?.displayName || ownerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
viewChannel(channel: ChannelWithSubscription): void {
|
viewChannel(channel: ChannelWithSubscription): void {
|
||||||
this.router.navigate(['/channels', channel.channel_id]);
|
this.router.navigate(['/channels', channel.channel_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
openCreateModal(): void {
|
|
||||||
this.newChannelName = '';
|
|
||||||
this.newChannelSubscribe = true;
|
|
||||||
this.showCreateModal.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeCreateModal(): void {
|
|
||||||
this.showCreateModal.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
createChannel(): void {
|
|
||||||
const userId = this.authService.getUserId();
|
|
||||||
if (!userId || !this.newChannelName.trim()) return;
|
|
||||||
|
|
||||||
this.creating.set(true);
|
|
||||||
this.apiService.createChannel(userId, {
|
|
||||||
name: this.newChannelName.trim(),
|
|
||||||
subscribe: this.newChannelSubscribe
|
|
||||||
}).subscribe({
|
|
||||||
next: (channel) => {
|
|
||||||
this.notification.success('Channel created successfully');
|
|
||||||
this.closeCreateModal();
|
|
||||||
this.creating.set(false);
|
|
||||||
this.loadChannels();
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.creating.set(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubscriptionStatus(channel: ChannelWithSubscription): { label: string; color: string } {
|
getSubscriptionStatus(channel: ChannelWithSubscription): { label: string; color: string } {
|
||||||
const userId = this.authService.getUserId();
|
const userId = this.authService.getUserId();
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
[nzData]="clients()"
|
[nzData]="clients()"
|
||||||
[nzLoading]="loading()"
|
[nzLoading]="loading()"
|
||||||
[nzShowPagination]="false"
|
[nzShowPagination]="false"
|
||||||
|
[nzNoResult]="noResultTpl"
|
||||||
nzSize="middle"
|
nzSize="middle"
|
||||||
>
|
>
|
||||||
|
<ng-template #noResultTpl></ng-template>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th nzWidth="5%"></th>
|
<th nzWidth="5%"></th>
|
||||||
|
|||||||
@@ -19,8 +19,10 @@
|
|||||||
[nzData]="keys()"
|
[nzData]="keys()"
|
||||||
[nzLoading]="loading()"
|
[nzLoading]="loading()"
|
||||||
[nzShowPagination]="false"
|
[nzShowPagination]="false"
|
||||||
|
[nzNoResult]="noResultTpl"
|
||||||
nzSize="middle"
|
nzSize="middle"
|
||||||
>
|
>
|
||||||
|
<ng-template #noResultTpl></ng-template>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th nzWidth="25%">Name</th>
|
<th nzWidth="25%">Name</th>
|
||||||
|
|||||||
@@ -60,8 +60,10 @@
|
|||||||
[nzData]="messages()"
|
[nzData]="messages()"
|
||||||
[nzLoading]="loading()"
|
[nzLoading]="loading()"
|
||||||
[nzShowPagination]="false"
|
[nzShowPagination]="false"
|
||||||
|
[nzNoResult]="noResultTpl"
|
||||||
nzSize="middle"
|
nzSize="middle"
|
||||||
>
|
>
|
||||||
|
<ng-template #noResultTpl></ng-template>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th nzWidth="40%">Title</th>
|
<th nzWidth="40%">Title</th>
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
[nzData]="senders()"
|
[nzData]="senders()"
|
||||||
[nzLoading]="loading()"
|
[nzLoading]="loading()"
|
||||||
[nzShowPagination]="false"
|
[nzShowPagination]="false"
|
||||||
|
[nzNoResult]="noResultTpl"
|
||||||
nzSize="middle"
|
nzSize="middle"
|
||||||
>
|
>
|
||||||
|
<ng-template #noResultTpl></ng-template>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th nzWidth="40%">Sender Name</th>
|
<th nzWidth="40%">Sender Name</th>
|
||||||
|
|||||||
@@ -25,16 +25,19 @@
|
|||||||
[nzData]="subscriptions()"
|
[nzData]="subscriptions()"
|
||||||
[nzLoading]="loading()"
|
[nzLoading]="loading()"
|
||||||
[nzShowPagination]="false"
|
[nzShowPagination]="false"
|
||||||
|
[nzNoResult]="noResultTpl"
|
||||||
nzSize="middle"
|
nzSize="middle"
|
||||||
>
|
>
|
||||||
|
<ng-template #noResultTpl></ng-template>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th nzWidth="15%">Direction</th>
|
<th nzWidth="10%">Direction</th>
|
||||||
<th nzWidth="25%">Channel</th>
|
<th nzWidth="20%">Channel</th>
|
||||||
<th nzWidth="20%">Subscriber / Owner</th>
|
<th nzWidth="20%">Subscriber</th>
|
||||||
<th nzWidth="15%">Status</th>
|
<th nzWidth="20%">Owner</th>
|
||||||
<th nzWidth="15%">Created</th>
|
<th nzWidth="10%">Status</th>
|
||||||
<th nzWidth="10%">Actions</th>
|
<th nzWidth="12%">Created</th>
|
||||||
|
<th nzWidth="8%">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -48,15 +51,8 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class="mono">{{ sub.channel_internal_name }}</span>
|
<span class="mono">{{ sub.channel_internal_name }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>{{ getUserDisplayName(sub.subscriber_user_id) }}</td>
|
||||||
@if (isOutgoing(sub)) {
|
<td>{{ getUserDisplayName(sub.channel_owner_user_id) }}</td>
|
||||||
<span class="label">Owner:</span>
|
|
||||||
<span class="mono">{{ sub.channel_owner_user_id }}</span>
|
|
||||||
} @else {
|
|
||||||
<span class="label">Subscriber:</span>
|
|
||||||
<span class="mono">{{ sub.subscriber_user_id }}</span>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<nz-tag [nzColor]="getStatusInfo(sub).color">
|
<nz-tag [nzColor]="getStatusInfo(sub).color">
|
||||||
{{ getStatusInfo(sub).label }}
|
{{ getStatusInfo(sub).label }}
|
||||||
@@ -113,7 +109,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="7">
|
||||||
<nz-empty nzNotFoundContent="No subscriptions found"></nz-empty>
|
<nz-empty nzNotFoundContent="No subscriptions found"></nz-empty>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
|||||||
import { ApiService } from '../../../core/services/api.service';
|
import { ApiService } from '../../../core/services/api.service';
|
||||||
import { AuthService } from '../../../core/services/auth.service';
|
import { AuthService } from '../../../core/services/auth.service';
|
||||||
import { NotificationService } from '../../../core/services/notification.service';
|
import { NotificationService } from '../../../core/services/notification.service';
|
||||||
|
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
||||||
import { Subscription, SubscriptionFilter } from '../../../core/models';
|
import { Subscription, SubscriptionFilter } from '../../../core/models';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
|
|
||||||
@@ -48,8 +49,10 @@ export class SubscriptionListComponent implements OnInit {
|
|||||||
private apiService = inject(ApiService);
|
private apiService = inject(ApiService);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private notification = inject(NotificationService);
|
private notification = inject(NotificationService);
|
||||||
|
private userCacheService = inject(UserCacheService);
|
||||||
|
|
||||||
subscriptions = signal<Subscription[]>([]);
|
subscriptions = signal<Subscription[]>([]);
|
||||||
|
userNames = signal<Map<string, ResolvedUser>>(new Map());
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
direction: TabDirection = 'both';
|
direction: TabDirection = 'both';
|
||||||
|
|
||||||
@@ -78,6 +81,7 @@ export class SubscriptionListComponent implements OnInit {
|
|||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.subscriptions.set(response.subscriptions);
|
this.subscriptions.set(response.subscriptions);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
this.resolveUserNames(response.subscriptions);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
@@ -85,6 +89,24 @@ export class SubscriptionListComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveUserNames(subscriptions: Subscription[]): void {
|
||||||
|
const userIds = new Set<string>();
|
||||||
|
for (const sub of subscriptions) {
|
||||||
|
userIds.add(sub.subscriber_user_id);
|
||||||
|
userIds.add(sub.channel_owner_user_id);
|
||||||
|
}
|
||||||
|
for (const id of userIds) {
|
||||||
|
this.userCacheService.resolveUser(id).subscribe(resolved => {
|
||||||
|
this.userNames.update(map => new Map(map).set(id, resolved));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserDisplayName(userId: string): string {
|
||||||
|
const resolved = this.userNames().get(userId);
|
||||||
|
return resolved?.displayName || userId;
|
||||||
|
}
|
||||||
|
|
||||||
onTabChange(index: number): void {
|
onTabChange(index: number): void {
|
||||||
const directions: TabDirection[] = ['both', 'outgoing', 'incoming'];
|
const directions: TabDirection[] = ['both', 'outgoing', 'incoming'];
|
||||||
this.direction = directions[index];
|
this.direction = directions[index];
|
||||||
|
|||||||
@@ -134,3 +134,19 @@ body {
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nz-card {
|
||||||
|
border: 1px solid #CCC !important;
|
||||||
|
box-shadow: 0 0 6px #CCC;
|
||||||
|
|
||||||
|
> .ant-card-head {
|
||||||
|
border-bottom: 1px solid #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .ant-card-body {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user