Simple Managment webapp [LLM]
This commit is contained in:
@@ -18,10 +18,10 @@ export interface Message {
|
|||||||
export interface MessageListParams {
|
export interface MessageListParams {
|
||||||
after?: string;
|
after?: string;
|
||||||
before?: string;
|
before?: string;
|
||||||
channel?: string;
|
channel_id?: string[];
|
||||||
priority?: number;
|
priority?: number[];
|
||||||
search?: string;
|
search?: string;
|
||||||
sender?: string;
|
sender?: string[];
|
||||||
subscription_status?: 'all' | 'confirmed' | 'unconfirmed';
|
subscription_status?: 'all' | 'confirmed' | 'unconfirmed';
|
||||||
trimmed?: boolean;
|
trimmed?: boolean;
|
||||||
page_size?: number;
|
page_size?: number;
|
||||||
|
|||||||
@@ -135,10 +135,22 @@ export class ApiService {
|
|||||||
if (params) {
|
if (params) {
|
||||||
if (params.after) httpParams = httpParams.set('after', params.after);
|
if (params.after) httpParams = httpParams.set('after', params.after);
|
||||||
if (params.before) httpParams = httpParams.set('before', params.before);
|
if (params.before) httpParams = httpParams.set('before', params.before);
|
||||||
if (params.channel) httpParams = httpParams.set('channel', params.channel);
|
if (params.channel_id) {
|
||||||
if (params.priority !== undefined) httpParams = httpParams.set('priority', params.priority);
|
for (const c of params.channel_id) {
|
||||||
|
httpParams = httpParams.append('channel_id', c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (params.priority) {
|
||||||
|
for (const p of params.priority) {
|
||||||
|
httpParams = httpParams.append('priority', p);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (params.search) httpParams = httpParams.set('search', params.search);
|
if (params.search) httpParams = httpParams.set('search', params.search);
|
||||||
if (params.sender) httpParams = httpParams.set('sender', params.sender);
|
if (params.sender) {
|
||||||
|
for (const s of params.sender) {
|
||||||
|
httpParams = httpParams.append('sender', s);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (params.subscription_status) httpParams = httpParams.set('subscription_status', params.subscription_status);
|
if (params.subscription_status) httpParams = httpParams.set('subscription_status', params.subscription_status);
|
||||||
if (params.trimmed !== undefined) httpParams = httpParams.set('trimmed', params.trimmed);
|
if (params.trimmed !== undefined) httpParams = httpParams.set('trimmed', params.trimmed);
|
||||||
if (params.page_size) httpParams = httpParams.set('page_size', params.page_size);
|
if (params.page_size) httpParams = httpParams.set('page_size', params.page_size);
|
||||||
|
|||||||
88
webapp/src/app/core/services/channel-cache.service.ts
Normal file
88
webapp/src/app/core/services/channel-cache.service.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable, of, map, shareReplay, catchError } from 'rxjs';
|
||||||
|
import { ApiService } from './api.service';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { ChannelWithSubscription } from '../models';
|
||||||
|
|
||||||
|
export interface ResolvedChannel {
|
||||||
|
channelId: string;
|
||||||
|
displayName: string;
|
||||||
|
internalName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ChannelCacheService {
|
||||||
|
private apiService = inject(ApiService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
|
||||||
|
private channelsCache$: Observable<Map<string, ChannelWithSubscription>> | null = null;
|
||||||
|
|
||||||
|
getAllChannels(): Observable<ChannelWithSubscription[]> {
|
||||||
|
const userId = this.authService.getUserId();
|
||||||
|
if (!userId) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
return this.apiService.getChannels(userId, 'owned').pipe(
|
||||||
|
map(response => response.channels),
|
||||||
|
catchError(() => of([]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveChannel(channelId: string): Observable<ResolvedChannel> {
|
||||||
|
return this.getChannelsMap().pipe(
|
||||||
|
map(channelsMap => {
|
||||||
|
const channel = channelsMap.get(channelId);
|
||||||
|
return {
|
||||||
|
channelId,
|
||||||
|
displayName: channel?.display_name || channel?.internal_name || channelId,
|
||||||
|
internalName: channel?.internal_name || channelId
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveChannels(channelIds: string[]): Observable<Map<string, ResolvedChannel>> {
|
||||||
|
return this.getChannelsMap().pipe(
|
||||||
|
map(channelsMap => {
|
||||||
|
const resolved = new Map<string, ResolvedChannel>();
|
||||||
|
for (const channelId of channelIds) {
|
||||||
|
const channel = channelsMap.get(channelId);
|
||||||
|
resolved.set(channelId, {
|
||||||
|
channelId,
|
||||||
|
displayName: channel?.display_name || channel?.internal_name || channelId,
|
||||||
|
internalName: channel?.internal_name || channelId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getChannelsMap(): Observable<Map<string, ChannelWithSubscription>> {
|
||||||
|
const userId = this.authService.getUserId();
|
||||||
|
if (!userId) {
|
||||||
|
return of(new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.channelsCache$) {
|
||||||
|
this.channelsCache$ = this.apiService.getChannels(userId, 'owned').pipe(
|
||||||
|
map(response => {
|
||||||
|
const map = new Map<string, ChannelWithSubscription>();
|
||||||
|
for (const channel of response.channels) {
|
||||||
|
map.set(channel.channel_id, channel);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}),
|
||||||
|
catchError(() => of(new Map())),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.channelsCache$;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache(): void {
|
||||||
|
this.channelsCache$ = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="login-footer">
|
<div class="login-footer">
|
||||||
<p>You need an admin key to access the dashboard.</p>
|
<p>You need an admin key to access.</p>
|
||||||
</div>
|
</div>
|
||||||
</nz-card>
|
</nz-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,6 +59,12 @@
|
|||||||
<nz-tag nzColor="default" nz-tooltip nzTooltipTitle="Access to all channels">
|
<nz-tag nzColor="default" nz-tooltip nzTooltipTitle="Access to all channels">
|
||||||
All Channels
|
All Channels
|
||||||
</nz-tag>
|
</nz-tag>
|
||||||
|
} @else if (key.channels && key.channels.length > 0) {
|
||||||
|
@for (channelId of key.channels; track channelId) {
|
||||||
|
<nz-tag nzColor="orange" nz-tooltip [nzTooltipTitle]="channelId">
|
||||||
|
{{ getChannelDisplayName(channelId) }}
|
||||||
|
</nz-tag>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -73,22 +79,29 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (!isCurrentKey(key)) {
|
<div class="action-buttons">
|
||||||
<button
|
<button
|
||||||
nz-button
|
nz-button
|
||||||
nzSize="small"
|
nzSize="small"
|
||||||
nzDanger
|
nz-tooltip
|
||||||
nz-popconfirm
|
nzTooltipTitle="Edit key"
|
||||||
nzPopconfirmTitle="Are you sure you want to delete this key?"
|
(click)="openEditModal(key)"
|
||||||
(nzOnConfirm)="deleteKey(key)"
|
|
||||||
>
|
>
|
||||||
<span nz-icon nzType="delete"></span>
|
<span nz-icon nzType="edit"></span>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
@if (!isCurrentKey(key)) {
|
||||||
<span class="text-muted" nz-tooltip nzTooltipTitle="Cannot delete the key you're currently using">
|
<button
|
||||||
-
|
nz-button
|
||||||
</span>
|
nzSize="small"
|
||||||
}
|
nzDanger
|
||||||
|
nz-popconfirm
|
||||||
|
nzPopconfirmTitle="Are you sure you want to delete this key?"
|
||||||
|
(nzOnConfirm)="deleteKey(key)"
|
||||||
|
>
|
||||||
|
<span nz-icon nzType="delete"></span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
@@ -180,11 +193,33 @@
|
|||||||
</nz-form-control>
|
</nz-form-control>
|
||||||
</nz-form-item>
|
</nz-form-item>
|
||||||
|
|
||||||
<nz-form-item class="mb-0">
|
<nz-form-item>
|
||||||
<label nz-checkbox [(ngModel)]="newKeyAllChannels">
|
<label nz-checkbox [(ngModel)]="newKeyAllChannels">
|
||||||
Access to all channels
|
Access to all channels
|
||||||
</label>
|
</label>
|
||||||
</nz-form-item>
|
</nz-form-item>
|
||||||
|
|
||||||
|
@if (!newKeyAllChannels) {
|
||||||
|
<nz-form-item class="mb-0">
|
||||||
|
<nz-form-label>Channels</nz-form-label>
|
||||||
|
<nz-form-control>
|
||||||
|
<nz-select
|
||||||
|
[(ngModel)]="newKeyChannels"
|
||||||
|
nzMode="multiple"
|
||||||
|
nzPlaceHolder="Select channels"
|
||||||
|
nzShowSearch
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
@for (channel of availableChannels(); track channel.channel_id) {
|
||||||
|
<nz-option
|
||||||
|
[nzValue]="channel.channel_id"
|
||||||
|
[nzLabel]="getChannelLabel(channel)"
|
||||||
|
></nz-option>
|
||||||
|
}
|
||||||
|
</nz-select>
|
||||||
|
</nz-form-control>
|
||||||
|
</nz-form-item>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</nz-modal>
|
</nz-modal>
|
||||||
@@ -205,3 +240,87 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Edit Key Modal -->
|
||||||
|
<nz-modal
|
||||||
|
[(nzVisible)]="showEditModal"
|
||||||
|
nzTitle="Edit Key"
|
||||||
|
(nzOnCancel)="closeEditModal()"
|
||||||
|
[nzFooter]="editModalFooter"
|
||||||
|
nzWidth="500px"
|
||||||
|
>
|
||||||
|
<ng-container *nzModalContent>
|
||||||
|
<nz-form-item>
|
||||||
|
<nz-form-label>Name</nz-form-label>
|
||||||
|
<nz-form-control>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
nz-input
|
||||||
|
placeholder="Enter a name for this key"
|
||||||
|
[(ngModel)]="editKeyName"
|
||||||
|
/>
|
||||||
|
</nz-form-control>
|
||||||
|
</nz-form-item>
|
||||||
|
|
||||||
|
<nz-form-item>
|
||||||
|
<nz-form-label>Permissions</nz-form-label>
|
||||||
|
<nz-form-control>
|
||||||
|
<div class="permission-checkboxes">
|
||||||
|
@for (opt of permissionOptions; track opt.value) {
|
||||||
|
<label
|
||||||
|
nz-checkbox
|
||||||
|
[nzChecked]="isEditPermissionChecked(opt.value)"
|
||||||
|
[nzDisabled]="opt.value !== 'A' && isEditPermissionChecked('A')"
|
||||||
|
(nzCheckedChange)="onEditPermissionChange(opt.value, $event)"
|
||||||
|
>
|
||||||
|
<nz-tag [nzColor]="getPermissionColor(opt.value)">{{ opt.value }}</nz-tag>
|
||||||
|
<span class="perm-label">{{ opt.label }}</span>
|
||||||
|
<span class="perm-desc">- {{ opt.description }}</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</nz-form-control>
|
||||||
|
</nz-form-item>
|
||||||
|
|
||||||
|
<nz-form-item>
|
||||||
|
<label nz-checkbox [(ngModel)]="editKeyAllChannels">
|
||||||
|
Access to all channels
|
||||||
|
</label>
|
||||||
|
</nz-form-item>
|
||||||
|
|
||||||
|
@if (!editKeyAllChannels) {
|
||||||
|
<nz-form-item class="mb-0">
|
||||||
|
<nz-form-label>Channels</nz-form-label>
|
||||||
|
<nz-form-control>
|
||||||
|
<nz-select
|
||||||
|
[(ngModel)]="editKeyChannels"
|
||||||
|
nzMode="multiple"
|
||||||
|
nzPlaceHolder="Select channels"
|
||||||
|
nzShowSearch
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
@for (channel of availableChannels(); track channel.channel_id) {
|
||||||
|
<nz-option
|
||||||
|
[nzValue]="channel.channel_id"
|
||||||
|
[nzLabel]="getChannelLabel(channel)"
|
||||||
|
></nz-option>
|
||||||
|
}
|
||||||
|
</nz-select>
|
||||||
|
</nz-form-control>
|
||||||
|
</nz-form-item>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
</nz-modal>
|
||||||
|
|
||||||
|
<ng-template #editModalFooter>
|
||||||
|
<button nz-button (click)="closeEditModal()">Cancel</button>
|
||||||
|
<button
|
||||||
|
nz-button
|
||||||
|
nzType="primary"
|
||||||
|
[nzLoading]="updating()"
|
||||||
|
[disabled]="!editKeyName.trim() || editKeyPermissions.length === 0"
|
||||||
|
(click)="updateKey()"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
|
|||||||
@@ -42,6 +42,11 @@
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.copy-icon {
|
.copy-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import { NzInputModule } from 'ng-zorro-antd/input';
|
|||||||
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
|
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
|
||||||
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||||
import { NzAlertModule } from 'ng-zorro-antd/alert';
|
import { NzAlertModule } from 'ng-zorro-antd/alert';
|
||||||
|
import { NzSelectModule } from 'ng-zorro-antd/select';
|
||||||
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 { KeyToken, parsePermissions, TokenPermission } from '../../../core/models';
|
import { ChannelCacheService, ResolvedChannel } from '../../../core/services/channel-cache.service';
|
||||||
|
import { KeyToken, parsePermissions, TokenPermission, ChannelWithSubscription } from '../../../core/models';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
|
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ interface PermissionOption {
|
|||||||
NzCheckboxModule,
|
NzCheckboxModule,
|
||||||
NzToolTipModule,
|
NzToolTipModule,
|
||||||
NzAlertModule,
|
NzAlertModule,
|
||||||
|
NzSelectModule,
|
||||||
RelativeTimePipe,
|
RelativeTimePipe,
|
||||||
CopyToClipboardDirective,
|
CopyToClipboardDirective,
|
||||||
],
|
],
|
||||||
@@ -56,19 +59,32 @@ export class KeyListComponent 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 channelCacheService = inject(ChannelCacheService);
|
||||||
|
|
||||||
keys = signal<KeyToken[]>([]);
|
keys = signal<KeyToken[]>([]);
|
||||||
currentKeyId = signal<string | null>(null);
|
currentKeyId = signal<string | null>(null);
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
|
channelNames = signal<Map<string, ResolvedChannel>>(new Map());
|
||||||
|
availableChannels = signal<ChannelWithSubscription[]>([]);
|
||||||
|
|
||||||
// Create modal
|
// Create modal
|
||||||
showCreateModal = signal(false);
|
showCreateModal = signal(false);
|
||||||
newKeyName = '';
|
newKeyName = '';
|
||||||
newKeyPermissions: TokenPermission[] = ['CR'];
|
newKeyPermissions: TokenPermission[] = ['CR'];
|
||||||
newKeyAllChannels = true;
|
newKeyAllChannels = true;
|
||||||
|
newKeyChannels: string[] = [];
|
||||||
creating = signal(false);
|
creating = signal(false);
|
||||||
createdKey = signal<KeyToken | null>(null);
|
createdKey = signal<KeyToken | null>(null);
|
||||||
|
|
||||||
|
// Edit modal
|
||||||
|
showEditModal = signal(false);
|
||||||
|
editingKey = signal<KeyToken | null>(null);
|
||||||
|
editKeyName = '';
|
||||||
|
editKeyPermissions: TokenPermission[] = [];
|
||||||
|
editKeyAllChannels = true;
|
||||||
|
editKeyChannels: string[] = [];
|
||||||
|
updating = signal(false);
|
||||||
|
|
||||||
permissionOptions: PermissionOption[] = [
|
permissionOptions: PermissionOption[] = [
|
||||||
{ value: 'A', label: 'Admin', description: 'Full access to all operations' },
|
{ value: 'A', label: 'Admin', description: 'Full access to all operations' },
|
||||||
{ value: 'CR', label: 'Channel Read', description: 'Read messages from channels' },
|
{ value: 'CR', label: 'Channel Read', description: 'Read messages from channels' },
|
||||||
@@ -79,6 +95,17 @@ export class KeyListComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadKeys();
|
this.loadKeys();
|
||||||
this.loadCurrentKey();
|
this.loadCurrentKey();
|
||||||
|
this.loadAvailableChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAvailableChannels(): void {
|
||||||
|
this.channelCacheService.getAllChannels().subscribe(channels => {
|
||||||
|
this.availableChannels.set(channels);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getChannelLabel(channel: ChannelWithSubscription): string {
|
||||||
|
return channel.display_name || channel.internal_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadKeys(): void {
|
loadKeys(): void {
|
||||||
@@ -90,6 +117,7 @@ export class KeyListComponent implements OnInit {
|
|||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.keys.set(response.keys);
|
this.keys.set(response.keys);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
this.resolveChannelNames(response.keys);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
@@ -97,6 +125,28 @@ export class KeyListComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveChannelNames(keys: KeyToken[]): void {
|
||||||
|
const allChannelIds = new Set<string>();
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!key.all_channels && key.channels) {
|
||||||
|
for (const channelId of key.channels) {
|
||||||
|
allChannelIds.add(channelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChannelIds.size > 0) {
|
||||||
|
this.channelCacheService.resolveChannels([...allChannelIds]).subscribe(resolved => {
|
||||||
|
this.channelNames.set(resolved);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getChannelDisplayName(channelId: string): string {
|
||||||
|
const resolved = this.channelNames().get(channelId);
|
||||||
|
return resolved?.displayName || channelId;
|
||||||
|
}
|
||||||
|
|
||||||
loadCurrentKey(): void {
|
loadCurrentKey(): void {
|
||||||
const userId = this.authService.getUserId();
|
const userId = this.authService.getUserId();
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
@@ -134,6 +184,7 @@ export class KeyListComponent implements OnInit {
|
|||||||
this.newKeyName = '';
|
this.newKeyName = '';
|
||||||
this.newKeyPermissions = ['CR'];
|
this.newKeyPermissions = ['CR'];
|
||||||
this.newKeyAllChannels = true;
|
this.newKeyAllChannels = true;
|
||||||
|
this.newKeyChannels = [];
|
||||||
this.createdKey.set(null);
|
this.createdKey.set(null);
|
||||||
this.showCreateModal.set(true);
|
this.showCreateModal.set(true);
|
||||||
}
|
}
|
||||||
@@ -150,7 +201,8 @@ export class KeyListComponent implements OnInit {
|
|||||||
this.apiService.createKey(userId, {
|
this.apiService.createKey(userId, {
|
||||||
name: this.newKeyName.trim(),
|
name: this.newKeyName.trim(),
|
||||||
permissions: this.newKeyPermissions.join(';'),
|
permissions: this.newKeyPermissions.join(';'),
|
||||||
all_channels: this.newKeyAllChannels
|
all_channels: this.newKeyAllChannels,
|
||||||
|
channels: this.newKeyAllChannels ? undefined : this.newKeyChannels
|
||||||
}).subscribe({
|
}).subscribe({
|
||||||
next: (key) => {
|
next: (key) => {
|
||||||
this.createdKey.set(key);
|
this.createdKey.set(key);
|
||||||
@@ -198,4 +250,59 @@ export class KeyListComponent implements OnInit {
|
|||||||
isPermissionChecked(perm: TokenPermission): boolean {
|
isPermissionChecked(perm: TokenPermission): boolean {
|
||||||
return this.newKeyPermissions.includes(perm);
|
return this.newKeyPermissions.includes(perm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit key modal
|
||||||
|
openEditModal(key: KeyToken): void {
|
||||||
|
this.editingKey.set(key);
|
||||||
|
this.editKeyName = key.name;
|
||||||
|
this.editKeyPermissions = parsePermissions(key.permissions);
|
||||||
|
this.editKeyAllChannels = key.all_channels;
|
||||||
|
this.editKeyChannels = key.channels ? [...key.channels] : [];
|
||||||
|
this.showEditModal.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeEditModal(): void {
|
||||||
|
this.showEditModal.set(false);
|
||||||
|
this.editingKey.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateKey(): void {
|
||||||
|
const userId = this.authService.getUserId();
|
||||||
|
const key = this.editingKey();
|
||||||
|
if (!userId || !key || !this.editKeyName.trim() || this.editKeyPermissions.length === 0) return;
|
||||||
|
|
||||||
|
this.updating.set(true);
|
||||||
|
this.apiService.updateKey(userId, key.keytoken_id, {
|
||||||
|
name: this.editKeyName.trim(),
|
||||||
|
permissions: this.editKeyPermissions.join(';'),
|
||||||
|
all_channels: this.editKeyAllChannels,
|
||||||
|
channels: this.editKeyAllChannels ? undefined : this.editKeyChannels
|
||||||
|
}).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.notification.success('Key updated');
|
||||||
|
this.updating.set(false);
|
||||||
|
this.closeEditModal();
|
||||||
|
this.loadKeys();
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.updating.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditPermissionChange(perm: TokenPermission, checked: boolean): void {
|
||||||
|
if (checked) {
|
||||||
|
if (perm === 'A') {
|
||||||
|
this.editKeyPermissions = ['A'];
|
||||||
|
} else if (!this.editKeyPermissions.includes(perm)) {
|
||||||
|
this.editKeyPermissions = [...this.editKeyPermissions, perm];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.editKeyPermissions = this.editKeyPermissions.filter(p => p !== perm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditPermissionChecked(perm: TokenPermission): boolean {
|
||||||
|
return this.editKeyPermissions.includes(perm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,21 @@
|
|||||||
{{ getChannelDisplayName(channel) }}
|
{{ getChannelDisplayName(channel) }}
|
||||||
</nz-tag>
|
</nz-tag>
|
||||||
}
|
}
|
||||||
|
@for (sender of senderFilter; track sender) {
|
||||||
|
<nz-tag nzMode="closeable" (nzOnClose)="removeSenderFilter(sender)">
|
||||||
|
{{ sender }}
|
||||||
|
</nz-tag>
|
||||||
|
}
|
||||||
@if (priorityFilter.length > 0) {
|
@if (priorityFilter.length > 0) {
|
||||||
<nz-tag nzMode="closeable" (nzOnClose)="clearPriorityFilter()">
|
<nz-tag nzMode="closeable" (nzOnClose)="clearPriorityFilter()">
|
||||||
{{ getPriorityLabel(+priorityFilter[0]) }}
|
{{ getPriorityLabel(+priorityFilter[0]) }}
|
||||||
</nz-tag>
|
</nz-tag>
|
||||||
}
|
}
|
||||||
|
@if (dateRange) {
|
||||||
|
<nz-tag nzMode="closeable" (nzOnClose)="clearDateRange()">
|
||||||
|
{{ getDateRangeDisplay() }}
|
||||||
|
</nz-tag>
|
||||||
|
}
|
||||||
<a class="clear-all" (click)="clearAllFilters()">Clear all</a>
|
<a class="clear-all" (click)="clearAllFilters()">Clear all</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -64,14 +74,35 @@
|
|||||||
[nzFilterMultiple]="true"
|
[nzFilterMultiple]="true"
|
||||||
(nzFilterChange)="onChannelFilterChange($event)"
|
(nzFilterChange)="onChannelFilterChange($event)"
|
||||||
>Channel</th>
|
>Channel</th>
|
||||||
<th nzWidth="15%">Sender</th>
|
<th
|
||||||
|
nzWidth="15%"
|
||||||
|
[nzFilters]="senderFilters()"
|
||||||
|
[nzFilterMultiple]="true"
|
||||||
|
(nzFilterChange)="onSenderFilterChange($event)"
|
||||||
|
>Sender</th>
|
||||||
<th
|
<th
|
||||||
nzWidth="10%"
|
nzWidth="10%"
|
||||||
[nzFilters]="priorityFilters"
|
[nzFilters]="priorityFilters"
|
||||||
[nzFilterMultiple]="false"
|
[nzFilterMultiple]="false"
|
||||||
(nzFilterChange)="onPriorityFilterChange($event)"
|
(nzFilterChange)="onPriorityFilterChange($event)"
|
||||||
>Priority</th>
|
>Priority</th>
|
||||||
<th nzWidth="20%">Time</th>
|
<th nzWidth="20%" nzCustomFilter>
|
||||||
|
Time
|
||||||
|
<nz-filter-trigger [(nzVisible)]="dateFilterVisible" [nzActive]="!!dateRange" [nzDropdownMenu]="dateMenu">
|
||||||
|
<span nz-icon nzType="filter" nzTheme="fill"></span>
|
||||||
|
</nz-filter-trigger>
|
||||||
|
<nz-dropdown-menu #dateMenu="nzDropdownMenu">
|
||||||
|
<div class="date-filter-dropdown" (click)="$event.stopPropagation()">
|
||||||
|
<nz-range-picker
|
||||||
|
[ngModel]="dateRange"
|
||||||
|
(ngModelChange)="onDateRangeChange($event)"
|
||||||
|
[nzAllowClear]="true"
|
||||||
|
nzFormat="yyyy-MM-dd"
|
||||||
|
[nzInline]="true"
|
||||||
|
></nz-range-picker>
|
||||||
|
</div>
|
||||||
|
</nz-dropdown-menu>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -13,6 +13,11 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-filter-dropdown {
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.active-filters {
|
.active-filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { NzEmptyModule } from 'ng-zorro-antd/empty';
|
|||||||
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||||
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||||
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
|
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
|
||||||
|
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
|
||||||
|
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
|
||||||
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 { Message, MessageListParams } from '../../../core/models';
|
import { Message, MessageListParams } from '../../../core/models';
|
||||||
@@ -31,6 +33,8 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
|||||||
NzSpinModule,
|
NzSpinModule,
|
||||||
NzToolTipModule,
|
NzToolTipModule,
|
||||||
NzPaginationModule,
|
NzPaginationModule,
|
||||||
|
NzDatePickerModule,
|
||||||
|
NzDropDownModule,
|
||||||
RelativeTimePipe,
|
RelativeTimePipe,
|
||||||
],
|
],
|
||||||
templateUrl: './message-list.component.html',
|
templateUrl: './message-list.component.html',
|
||||||
@@ -54,6 +58,9 @@ export class MessageListComponent implements OnInit {
|
|||||||
appliedSearchText = '';
|
appliedSearchText = '';
|
||||||
priorityFilter: string[] = [];
|
priorityFilter: string[] = [];
|
||||||
channelFilter: string[] = [];
|
channelFilter: string[] = [];
|
||||||
|
senderFilter: string[] = [];
|
||||||
|
dateRange: [Date, Date] | null = null;
|
||||||
|
dateFilterVisible = false;
|
||||||
|
|
||||||
// Filter options
|
// Filter options
|
||||||
priorityFilters: NzTableFilterList = [
|
priorityFilters: NzTableFilterList = [
|
||||||
@@ -62,9 +69,11 @@ export class MessageListComponent implements OnInit {
|
|||||||
{ text: 'High', value: '2' },
|
{ text: 'High', value: '2' },
|
||||||
];
|
];
|
||||||
channelFilters = signal<NzTableFilterList>([]);
|
channelFilters = signal<NzTableFilterList>([]);
|
||||||
|
senderFilters = signal<NzTableFilterList>([]);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadChannels();
|
this.loadChannels();
|
||||||
|
this.loadSenders();
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +86,20 @@ export class MessageListComponent implements OnInit {
|
|||||||
this.channelFilters.set(
|
this.channelFilters.set(
|
||||||
response.channels.map(ch => ({
|
response.channels.map(ch => ({
|
||||||
text: ch.display_name,
|
text: ch.display_name,
|
||||||
value: ch.internal_name,
|
value: ch.channel_id,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSenders(): void {
|
||||||
|
this.apiService.getSenderNames().subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.senderFilters.set(
|
||||||
|
response.sender_names.map(s => ({
|
||||||
|
text: s.name,
|
||||||
|
value: s.name,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -95,11 +117,18 @@ export class MessageListComponent implements OnInit {
|
|||||||
if (this.appliedSearchText) {
|
if (this.appliedSearchText) {
|
||||||
params.search = this.appliedSearchText;
|
params.search = this.appliedSearchText;
|
||||||
}
|
}
|
||||||
if (this.priorityFilter.length === 1) {
|
if (this.priorityFilter.length > 0) {
|
||||||
params.priority = parseInt(this.priorityFilter[0], 10);
|
params.priority = this.priorityFilter.map(p => parseInt(p, 10));
|
||||||
}
|
}
|
||||||
if (this.channelFilter.length > 0) {
|
if (this.channelFilter.length > 0) {
|
||||||
params.channel = this.channelFilter.join(',');
|
params.channel_id = this.channelFilter;
|
||||||
|
}
|
||||||
|
if (this.senderFilter.length > 0) {
|
||||||
|
params.sender = this.senderFilter;
|
||||||
|
}
|
||||||
|
if (this.dateRange) {
|
||||||
|
params.after = this.dateRange[0].toISOString();
|
||||||
|
params.before = this.dateRange[1].toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use page-index based pagination: $1 = page 1, $2 = page 2, etc.
|
// Use page-index based pagination: $1 = page 1, $2 = page 2, etc.
|
||||||
@@ -138,6 +167,12 @@ export class MessageListComponent implements OnInit {
|
|||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSenderFilterChange(filters: string[] | null): void {
|
||||||
|
this.senderFilter = filters ?? [];
|
||||||
|
this.currentPage.set(1);
|
||||||
|
this.loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
clearSearch(): void {
|
clearSearch(): void {
|
||||||
this.searchText = '';
|
this.searchText = '';
|
||||||
this.appliedSearchText = '';
|
this.appliedSearchText = '';
|
||||||
@@ -157,29 +192,64 @@ export class MessageListComponent implements OnInit {
|
|||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearSenderFilter(): void {
|
||||||
|
this.senderFilter = [];
|
||||||
|
this.currentPage.set(1);
|
||||||
|
this.loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSenderFilter(sender: string): void {
|
||||||
|
this.senderFilter = this.senderFilter.filter(s => s !== sender);
|
||||||
|
this.currentPage.set(1);
|
||||||
|
this.loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
clearPriorityFilter(): void {
|
clearPriorityFilter(): void {
|
||||||
this.priorityFilter = [];
|
this.priorityFilter = [];
|
||||||
this.currentPage.set(1);
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDateRangeChange(dates: [Date, Date] | null): void {
|
||||||
|
this.dateRange = dates;
|
||||||
|
if (dates) {
|
||||||
|
this.dateFilterVisible = false;
|
||||||
|
}
|
||||||
|
this.currentPage.set(1);
|
||||||
|
this.loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDateRange(): void {
|
||||||
|
this.dateRange = null;
|
||||||
|
this.currentPage.set(1);
|
||||||
|
this.loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
clearAllFilters(): void {
|
clearAllFilters(): void {
|
||||||
this.searchText = '';
|
this.searchText = '';
|
||||||
this.appliedSearchText = '';
|
this.appliedSearchText = '';
|
||||||
this.channelFilter = [];
|
this.channelFilter = [];
|
||||||
|
this.senderFilter = [];
|
||||||
this.priorityFilter = [];
|
this.priorityFilter = [];
|
||||||
|
this.dateRange = null;
|
||||||
this.currentPage.set(1);
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
hasActiveFilters(): boolean {
|
hasActiveFilters(): boolean {
|
||||||
return !!this.appliedSearchText || this.channelFilter.length > 0 || this.priorityFilter.length > 0;
|
return !!this.appliedSearchText || this.channelFilter.length > 0 || this.senderFilter.length > 0 || this.priorityFilter.length > 0 || !!this.dateRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelDisplayName(internalName: string): string {
|
getChannelDisplayName(channelId: string): string {
|
||||||
const filters = this.channelFilters();
|
const filters = this.channelFilters();
|
||||||
const channel = filters.find(f => f.value === internalName);
|
const channel = filters.find(f => f.value === channelId);
|
||||||
return channel?.text?.toString() ?? internalName;
|
return channel?.text?.toString() ?? channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDateRangeDisplay(): string {
|
||||||
|
if (!this.dateRange) return '';
|
||||||
|
const format = (d: Date) => d.toLocaleDateString();
|
||||||
|
return `${format(this.dateRange[0])} - ${format(this.dateRange[1])}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPage(page: number): void {
|
goToPage(page: number): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user