Simple Managment webapp [LLM]

This commit is contained in:
2025-12-03 19:38:15 +01:00
parent 3ed323e056
commit 6090319b5f
10 changed files with 468 additions and 31 deletions

View File

@@ -59,6 +59,12 @@
<nz-tag nzColor="default" nz-tooltip nzTooltipTitle="Access to all channels">
All Channels
</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>
</td>
@@ -73,22 +79,29 @@
}
</td>
<td>
@if (!isCurrentKey(key)) {
<div class="action-buttons">
<button
nz-button
nzSize="small"
nzDanger
nz-popconfirm
nzPopconfirmTitle="Are you sure you want to delete this key?"
(nzOnConfirm)="deleteKey(key)"
nz-tooltip
nzTooltipTitle="Edit key"
(click)="openEditModal(key)"
>
<span nz-icon nzType="delete"></span>
<span nz-icon nzType="edit"></span>
</button>
} @else {
<span class="text-muted" nz-tooltip nzTooltipTitle="Cannot delete the key you're currently using">
-
</span>
}
@if (!isCurrentKey(key)) {
<button
nz-button
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>
</tr>
} @empty {
@@ -180,11 +193,33 @@
</nz-form-control>
</nz-form-item>
<nz-form-item class="mb-0">
<nz-form-item>
<label nz-checkbox [(ngModel)]="newKeyAllChannels">
Access to all channels
</label>
</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>
</nz-modal>
@@ -205,3 +240,87 @@
</button>
}
</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>

View File

@@ -42,6 +42,11 @@
color: #999;
}
.action-buttons {
display: flex;
gap: 8px;
}
.copy-icon {
cursor: pointer;
color: #999;

View File

@@ -14,10 +14,12 @@ import { NzInputModule } from 'ng-zorro-antd/input';
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { NzAlertModule } from 'ng-zorro-antd/alert';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { ApiService } from '../../../core/services/api.service';
import { AuthService } from '../../../core/services/auth.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 { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
@@ -46,6 +48,7 @@ interface PermissionOption {
NzCheckboxModule,
NzToolTipModule,
NzAlertModule,
NzSelectModule,
RelativeTimePipe,
CopyToClipboardDirective,
],
@@ -56,19 +59,32 @@ export class KeyListComponent implements OnInit {
private apiService = inject(ApiService);
private authService = inject(AuthService);
private notification = inject(NotificationService);
private channelCacheService = inject(ChannelCacheService);
keys = signal<KeyToken[]>([]);
currentKeyId = signal<string | null>(null);
loading = signal(false);
channelNames = signal<Map<string, ResolvedChannel>>(new Map());
availableChannels = signal<ChannelWithSubscription[]>([]);
// Create modal
showCreateModal = signal(false);
newKeyName = '';
newKeyPermissions: TokenPermission[] = ['CR'];
newKeyAllChannels = true;
newKeyChannels: string[] = [];
creating = signal(false);
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[] = [
{ value: 'A', label: 'Admin', description: 'Full access to all operations' },
{ value: 'CR', label: 'Channel Read', description: 'Read messages from channels' },
@@ -79,6 +95,17 @@ export class KeyListComponent implements OnInit {
ngOnInit(): void {
this.loadKeys();
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 {
@@ -90,6 +117,7 @@ export class KeyListComponent implements OnInit {
next: (response) => {
this.keys.set(response.keys);
this.loading.set(false);
this.resolveChannelNames(response.keys);
},
error: () => {
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 {
const userId = this.authService.getUserId();
if (!userId) return;
@@ -134,6 +184,7 @@ export class KeyListComponent implements OnInit {
this.newKeyName = '';
this.newKeyPermissions = ['CR'];
this.newKeyAllChannels = true;
this.newKeyChannels = [];
this.createdKey.set(null);
this.showCreateModal.set(true);
}
@@ -150,7 +201,8 @@ export class KeyListComponent implements OnInit {
this.apiService.createKey(userId, {
name: this.newKeyName.trim(),
permissions: this.newKeyPermissions.join(';'),
all_channels: this.newKeyAllChannels
all_channels: this.newKeyAllChannels,
channels: this.newKeyAllChannels ? undefined : this.newKeyChannels
}).subscribe({
next: (key) => {
this.createdKey.set(key);
@@ -198,4 +250,59 @@ export class KeyListComponent implements OnInit {
isPermissionChecked(perm: TokenPermission): boolean {
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);
}
}