Simple Managment webapp [LLM]
This commit is contained in:
@@ -48,10 +48,7 @@
|
|||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{"input": "src/assets", "output": ".", "glob": "**/*" }
|
||||||
"glob": "**/*",
|
|
||||||
"input": "public"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
@@ -96,30 +93,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
|
||||||
"extract-i18n": {
|
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
|
||||||
"options": {
|
|
||||||
"polyfills": [
|
|
||||||
"zone.js",
|
|
||||||
"zone.js/testing"
|
|
||||||
],
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
|
||||||
"inlineStyleLanguage": "scss",
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"glob": "**/*",
|
|
||||||
"input": "public"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"src/styles.scss"
|
|
||||||
],
|
|
||||||
"scripts": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,4 +32,5 @@ export interface MessageListResponse {
|
|||||||
messages: Message[];
|
messages: Message[];
|
||||||
next_page_token: string;
|
next_page_token: string;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
|
total_count: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
export interface SenderNameStatistics {
|
export interface SenderNameStatistics {
|
||||||
name: string;
|
name: string;
|
||||||
|
first_timestamp: string;
|
||||||
last_timestamp: string;
|
last_timestamp: string;
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SenderNameListResponse {
|
export interface SenderNameListResponse {
|
||||||
senders: SenderNameStatistics[];
|
sender_names: SenderNameStatistics[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<nz-card class="login-card">
|
<nz-card class="login-card">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<img src="assets/logo.png" alt="SimpleCloudNotifier" class="login-logo" />
|
<img src="/logo.png" alt="SimpleCloudNotifier" class="login-logo" />
|
||||||
<h1>SimpleCloudNotifier</h1>
|
<h1>SimpleCloudNotifier</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -92,26 +92,22 @@
|
|||||||
nzTooltipTitle="Copy"
|
nzTooltipTitle="Copy"
|
||||||
[appCopyToClipboard]="channel()!.subscribe_key!"
|
[appCopyToClipboard]="channel()!.subscribe_key!"
|
||||||
></span>
|
></span>
|
||||||
<span
|
|
||||||
nz-icon
|
|
||||||
nzType="qrcode"
|
|
||||||
class="action-icon"
|
|
||||||
nz-tooltip
|
|
||||||
nzTooltipTitle="Show QR Code"
|
|
||||||
(click)="showQrCode()"
|
|
||||||
></span>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="key-actions">
|
<div class="key-actions">
|
||||||
<button
|
<button
|
||||||
nz-button
|
nz-button
|
||||||
nzSize="small"
|
nzSize="small"
|
||||||
nz-popconfirm
|
nz-popconfirm
|
||||||
nzPopconfirmTitle="Regenerate subscribe key? Existing subscribers will need the new key."
|
nzPopconfirmTitle="Regenerate subscribe key? The existing key will no longer be valid."
|
||||||
(nzOnConfirm)="regenerateSubscribeKey()"
|
(nzOnConfirm)="regenerateSubscribeKey()"
|
||||||
>
|
>
|
||||||
Regenerate
|
Invalidate & Regenerate
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="qr-section">
|
||||||
|
<app-qr-code-display [data]="qrCodeData()"></app-qr-code-display>
|
||||||
|
<p class="qr-hint">Scan this QR code with the SimpleCloudNotifier app to subscribe to this channel.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,15 +234,3 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</nz-modal>
|
</nz-modal>
|
||||||
|
|
||||||
<!-- QR Code Modal -->
|
|
||||||
<nz-modal
|
|
||||||
[(nzVisible)]="showQrModal"
|
|
||||||
nzTitle="Subscribe QR Code"
|
|
||||||
(nzOnCancel)="closeQrModal()"
|
|
||||||
[nzFooter]="null"
|
|
||||||
>
|
|
||||||
<ng-container *nzModalContent>
|
|
||||||
<app-qr-code-display [data]="qrCodeData()"></app-qr-code-display>
|
|
||||||
<p class="qr-hint">Scan this QR code with the SimpleCloudNotifier app to subscribe to this channel.</p>
|
|
||||||
</ng-container>
|
|
||||||
</nz-modal>
|
|
||||||
|
|||||||
@@ -44,9 +44,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qr-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-qr-code-display {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.qr-hint {
|
.qr-hint {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-top: 16px;
|
margin-top: 12px;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, inject, signal, OnInit } from '@angular/core';
|
import { Component, inject, signal, computed, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
@@ -70,9 +70,20 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
editDescription = '';
|
editDescription = '';
|
||||||
saving = signal(false);
|
saving = signal(false);
|
||||||
|
|
||||||
// QR modal
|
// QR code data (computed from channel)
|
||||||
showQrModal = signal(false);
|
qrCodeData = computed(() => {
|
||||||
qrCodeData = signal('');
|
const channel = this.channel();
|
||||||
|
if (!channel || !channel.subscribe_key) return '';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'@scn.channel.subscribe',
|
||||||
|
'v1',
|
||||||
|
channel.display_name,
|
||||||
|
channel.owner_user_id,
|
||||||
|
channel.channel_id,
|
||||||
|
channel.subscribe_key
|
||||||
|
].join('\n');
|
||||||
|
});
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const channelId = this.route.snapshot.paramMap.get('id');
|
const channelId = this.route.snapshot.paramMap.get('id');
|
||||||
@@ -211,28 +222,6 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// QR Code
|
|
||||||
showQrCode(): void {
|
|
||||||
const channel = this.channel();
|
|
||||||
if (!channel || !channel.subscribe_key) return;
|
|
||||||
|
|
||||||
const qrText = [
|
|
||||||
'@scn.channel.subscribe',
|
|
||||||
'v1',
|
|
||||||
channel.display_name,
|
|
||||||
channel.owner_user_id,
|
|
||||||
channel.channel_id,
|
|
||||||
channel.subscribe_key
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
this.qrCodeData.set(qrText);
|
|
||||||
this.showQrModal.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeQrModal(): void {
|
|
||||||
this.showQrModal.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubscriptionStatus(): { label: string; color: string } {
|
getSubscriptionStatus(): { label: string; color: string } {
|
||||||
const channel = this.channel();
|
const channel = this.channel();
|
||||||
if (!channel) return { label: 'Unknown', color: 'default' };
|
if (!channel) return { label: 'Unknown', color: 'default' };
|
||||||
|
|||||||
@@ -110,12 +110,14 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</nz-table>
|
</nz-table>
|
||||||
|
|
||||||
@if (nextPageToken()) {
|
<div class="pagination-controls">
|
||||||
<div class="load-more">
|
<nz-pagination
|
||||||
<button nz-button nzType="default" (click)="loadMore()" [nzLoading]="loading()">
|
[nzPageIndex]="currentPage()"
|
||||||
Load More
|
[nzPageSize]="pageSize"
|
||||||
</button>
|
[nzTotal]="totalCount()"
|
||||||
</div>
|
[nzDisabled]="loading()"
|
||||||
}
|
(nzPageIndexChange)="goToPage($event)"
|
||||||
|
></nz-pagination>
|
||||||
|
</div>
|
||||||
</nz-card>
|
</nz-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,8 +42,17 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-more {
|
.pagination-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
|
|
||||||
|
.page-indicator {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { NzEmptyModule } from 'ng-zorro-antd/empty';
|
|||||||
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||||
import { NzCardModule } from 'ng-zorro-antd/card';
|
import { NzCardModule } from 'ng-zorro-antd/card';
|
||||||
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||||
|
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
|
||||||
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 +32,7 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
|||||||
NzSpinModule,
|
NzSpinModule,
|
||||||
NzCardModule,
|
NzCardModule,
|
||||||
NzToolTipModule,
|
NzToolTipModule,
|
||||||
|
NzPaginationModule,
|
||||||
RelativeTimePipe,
|
RelativeTimePipe,
|
||||||
],
|
],
|
||||||
templateUrl: './message-list.component.html',
|
templateUrl: './message-list.component.html',
|
||||||
@@ -43,7 +45,11 @@ export class MessageListComponent implements OnInit {
|
|||||||
|
|
||||||
messages = signal<Message[]>([]);
|
messages = signal<Message[]>([]);
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
nextPageToken = signal<string | null>(null);
|
|
||||||
|
// Pagination
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = 50;
|
||||||
|
totalCount = signal(0);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
searchText = '';
|
searchText = '';
|
||||||
@@ -80,11 +86,11 @@ export class MessageListComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMessages(append = false): void {
|
loadMessages(): void {
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
|
|
||||||
const params: MessageListParams = {
|
const params: MessageListParams = {
|
||||||
page_size: 50,
|
page_size: this.pageSize,
|
||||||
trimmed: true,
|
trimmed: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,22 +103,17 @@ export class MessageListComponent implements OnInit {
|
|||||||
if (this.channelFilter.length > 0) {
|
if (this.channelFilter.length > 0) {
|
||||||
params.channel = this.channelFilter.join(',');
|
params.channel = this.channelFilter.join(',');
|
||||||
}
|
}
|
||||||
if (append && this.nextPageToken()) {
|
|
||||||
params.next_page_token = this.nextPageToken()!;
|
// Use page-index based pagination: $1 = page 1, $2 = page 2, etc.
|
||||||
|
const page = this.currentPage();
|
||||||
|
if (page > 1) {
|
||||||
|
params.next_page_token = `$${page}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.apiService.getMessages(params).subscribe({
|
this.apiService.getMessages(params).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
if (append) {
|
this.messages.set(response.messages);
|
||||||
this.messages.update(msgs => [...msgs, ...response.messages]);
|
this.totalCount.set(response.total_count);
|
||||||
} else {
|
|
||||||
this.messages.set(response.messages);
|
|
||||||
}
|
|
||||||
this.nextPageToken.set(
|
|
||||||
response.next_page_token && response.next_page_token !== '@end'
|
|
||||||
? response.next_page_token
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@@ -123,37 +124,44 @@ export class MessageListComponent implements OnInit {
|
|||||||
|
|
||||||
applyFilters(): void {
|
applyFilters(): void {
|
||||||
this.appliedSearchText = this.searchText;
|
this.appliedSearchText = this.searchText;
|
||||||
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
onPriorityFilterChange(filters: string[] | null): void {
|
onPriorityFilterChange(filters: string[] | null): void {
|
||||||
this.priorityFilter = filters ?? [];
|
this.priorityFilter = filters ?? [];
|
||||||
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChannelFilterChange(filters: string[] | null): void {
|
onChannelFilterChange(filters: string[] | null): void {
|
||||||
this.channelFilter = filters ?? [];
|
this.channelFilter = filters ?? [];
|
||||||
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSearch(): void {
|
clearSearch(): void {
|
||||||
this.searchText = '';
|
this.searchText = '';
|
||||||
this.appliedSearchText = '';
|
this.appliedSearchText = '';
|
||||||
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearChannelFilter(): void {
|
clearChannelFilter(): void {
|
||||||
this.channelFilter = [];
|
this.channelFilter = [];
|
||||||
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeChannelFilter(channel: string): void {
|
removeChannelFilter(channel: string): void {
|
||||||
this.channelFilter = this.channelFilter.filter(c => c !== channel);
|
this.channelFilter = this.channelFilter.filter(c => c !== channel);
|
||||||
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPriorityFilter(): void {
|
clearPriorityFilter(): void {
|
||||||
this.priorityFilter = [];
|
this.priorityFilter = [];
|
||||||
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +170,7 @@ export class MessageListComponent implements OnInit {
|
|||||||
this.appliedSearchText = '';
|
this.appliedSearchText = '';
|
||||||
this.channelFilter = [];
|
this.channelFilter = [];
|
||||||
this.priorityFilter = [];
|
this.priorityFilter = [];
|
||||||
|
this.currentPage.set(1);
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,10 +184,9 @@ export class MessageListComponent implements OnInit {
|
|||||||
return channel?.text?.toString() ?? internalName;
|
return channel?.text?.toString() ?? internalName;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMore(): void {
|
goToPage(page: number): void {
|
||||||
if (this.nextPageToken()) {
|
this.currentPage.set(page);
|
||||||
this.loadMessages(true);
|
this.loadMessages();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewMessage(message: Message): void {
|
viewMessage(message: Message): void {
|
||||||
|
|||||||
@@ -1,50 +1,95 @@
|
|||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Senders</h2>
|
<h2>Senders</h2>
|
||||||
<button nz-button (click)="loadSenders()">
|
<button nz-button (click)="refresh()">
|
||||||
<span nz-icon nzType="reload"></span>
|
<span nz-icon nzType="reload"></span>
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nz-card>
|
<nz-card>
|
||||||
<nz-table
|
<nz-tabset (nzSelectedIndexChange)="onTabChange($event)">
|
||||||
#senderTable
|
<nz-tab nzTitle="My Senders">
|
||||||
[nzData]="senders()"
|
<nz-table
|
||||||
[nzLoading]="loading()"
|
#mySenderTable
|
||||||
[nzShowPagination]="false"
|
[nzData]="mySenders()"
|
||||||
[nzNoResult]="noResultTpl"
|
[nzLoading]="loadingMy()"
|
||||||
nzSize="middle"
|
[nzShowPagination]="false"
|
||||||
>
|
[nzNoResult]="noResultTpl"
|
||||||
<ng-template #noResultTpl></ng-template>
|
nzSize="middle"
|
||||||
<thead>
|
>
|
||||||
<tr>
|
<ng-template #noResultTpl></ng-template>
|
||||||
<th nzWidth="40%">Sender Name</th>
|
<thead>
|
||||||
<th nzWidth="20%">Message Count</th>
|
<tr>
|
||||||
<th nzWidth="40%">Last Used</th>
|
<th nzWidth="40%">Sender Name</th>
|
||||||
</tr>
|
<th nzWidth="20%">Message Count</th>
|
||||||
</thead>
|
<th nzWidth="40%">Last Used</th>
|
||||||
<tbody>
|
</tr>
|
||||||
@for (sender of senders(); track sender.name) {
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td>
|
@for (sender of mySenders(); track sender.name) {
|
||||||
<span class="sender-name">{{ sender.name || '(No name)' }}</span>
|
<tr>
|
||||||
</td>
|
<td>
|
||||||
<td>{{ sender.count }}</td>
|
<span class="sender-name">{{ sender.name || '(No name)' }}</span>
|
||||||
<td>
|
</td>
|
||||||
<span nz-tooltip [nzTooltipTitle]="sender.last_timestamp">
|
<td>{{ sender.count }}</td>
|
||||||
{{ sender.last_timestamp | relativeTime }}
|
<td>
|
||||||
</span>
|
<span nz-tooltip [nzTooltipTitle]="sender.last_timestamp">
|
||||||
</td>
|
{{ sender.last_timestamp | relativeTime }}
|
||||||
</tr>
|
</span>
|
||||||
} @empty {
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td colspan="3">
|
} @empty {
|
||||||
<nz-empty nzNotFoundContent="No senders found"></nz-empty>
|
<tr>
|
||||||
</td>
|
<td colspan="3">
|
||||||
</tr>
|
<nz-empty nzNotFoundContent="No senders found"></nz-empty>
|
||||||
}
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</nz-table>
|
}
|
||||||
|
</tbody>
|
||||||
|
</nz-table>
|
||||||
|
</nz-tab>
|
||||||
|
|
||||||
|
<nz-tab nzTitle="All Senders">
|
||||||
|
<nz-table
|
||||||
|
#allSenderTable
|
||||||
|
[nzData]="allSenders()"
|
||||||
|
[nzLoading]="loadingAll()"
|
||||||
|
[nzShowPagination]="false"
|
||||||
|
[nzNoResult]="noResultTpl2"
|
||||||
|
nzSize="middle"
|
||||||
|
>
|
||||||
|
<ng-template #noResultTpl2></ng-template>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th nzWidth="40%">Sender Name</th>
|
||||||
|
<th nzWidth="20%">Message Count</th>
|
||||||
|
<th nzWidth="40%">Last Used</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (sender of allSenders(); track sender.name) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="sender-name">{{ sender.name || '(No name)' }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ sender.count }}</td>
|
||||||
|
<td>
|
||||||
|
<span nz-tooltip [nzTooltipTitle]="sender.last_timestamp">
|
||||||
|
{{ sender.last_timestamp | relativeTime }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="3">
|
||||||
|
<nz-empty nzNotFoundContent="No senders found"></nz-empty>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</nz-table>
|
||||||
|
</nz-tab>
|
||||||
|
</nz-tabset>
|
||||||
</nz-card>
|
</nz-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { NzIconModule } from 'ng-zorro-antd/icon';
|
|||||||
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 { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||||
|
import { NzTabsModule } from 'ng-zorro-antd/tabs';
|
||||||
import { ApiService } from '../../../core/services/api.service';
|
import { ApiService } from '../../../core/services/api.service';
|
||||||
|
import { AuthService } from '../../../core/services/auth.service';
|
||||||
import { SenderNameStatistics } from '../../../core/models';
|
import { SenderNameStatistics } from '../../../core/models';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
|||||||
NzEmptyModule,
|
NzEmptyModule,
|
||||||
NzCardModule,
|
NzCardModule,
|
||||||
NzToolTipModule,
|
NzToolTipModule,
|
||||||
|
NzTabsModule,
|
||||||
RelativeTimePipe,
|
RelativeTimePipe,
|
||||||
],
|
],
|
||||||
templateUrl: './sender-list.component.html',
|
templateUrl: './sender-list.component.html',
|
||||||
@@ -28,24 +31,61 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
|||||||
})
|
})
|
||||||
export class SenderListComponent implements OnInit {
|
export class SenderListComponent implements OnInit {
|
||||||
private apiService = inject(ApiService);
|
private apiService = inject(ApiService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
|
||||||
senders = signal<SenderNameStatistics[]>([]);
|
mySenders = signal<SenderNameStatistics[]>([]);
|
||||||
loading = signal(false);
|
allSenders = signal<SenderNameStatistics[]>([]);
|
||||||
|
loadingMy = signal(false);
|
||||||
|
loadingAll = signal(false);
|
||||||
|
activeTab = signal(0);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadSenders();
|
this.loadMySenders();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSenders(): void {
|
onTabChange(index: number): void {
|
||||||
this.loading.set(true);
|
this.activeTab.set(index);
|
||||||
this.apiService.getSenderNames().subscribe({
|
if (index === 0 && this.mySenders().length === 0) {
|
||||||
|
this.loadMySenders();
|
||||||
|
} else if (index === 1 && this.allSenders().length === 0) {
|
||||||
|
this.loadAllSenders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMySenders(): void {
|
||||||
|
const userId = this.authService.getUserId();
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
this.loadingMy.set(true);
|
||||||
|
this.apiService.getUserSenderNames(userId).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.senders.set(response.senders);
|
this.mySenders.set(response.sender_names);
|
||||||
this.loading.set(false);
|
this.loadingMy.set(false);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loading.set(false);
|
this.loadingMy.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadAllSenders(): void {
|
||||||
|
this.loadingAll.set(true);
|
||||||
|
this.apiService.getSenderNames().subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.allSenders.set(response.sender_names);
|
||||||
|
this.loadingAll.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loadingAll.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(): void {
|
||||||
|
if (this.activeTab() === 0) {
|
||||||
|
this.loadMySenders();
|
||||||
|
} else {
|
||||||
|
this.loadAllSenders();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,9 @@
|
|||||||
[nzCollapsedWidth]="80"
|
[nzCollapsedWidth]="80"
|
||||||
>
|
>
|
||||||
<div class="sidebar-logo">
|
<div class="sidebar-logo">
|
||||||
|
<img src="/logo.png" alt="SCN" class="sidebar-logo-img" />
|
||||||
@if (!isCollapsed()) {
|
@if (!isCollapsed()) {
|
||||||
<span>SimpleCloudNotifier</span>
|
<span>SimpleCloudNotifier</span>
|
||||||
} @else {
|
|
||||||
<span>SCN</span>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<ul nz-menu nzTheme="dark" nzMode="inline" [nzInlineCollapsed]="isCollapsed()">
|
<ul nz-menu nzTheme="dark" nzMode="inline" [nzInlineCollapsed]="isCollapsed()">
|
||||||
@@ -62,7 +61,6 @@
|
|||||||
<span nz-icon nzType="logout"></span>
|
<span nz-icon nzType="logout"></span>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
<img src="assets/logo.png" alt="SCN" class="header-logo" />
|
|
||||||
</div>
|
</div>
|
||||||
</nz-header>
|
</nz-header>
|
||||||
<nz-content class="content-area">
|
<nz-content class="content-area">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
background: #001529;
|
background: #001529;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -25,6 +26,12 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.sidebar-logo-img {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header {
|
.app-header {
|
||||||
@@ -64,11 +71,6 @@
|
|||||||
color: #666;
|
color: #666;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-logo {
|
|
||||||
height: 36px;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-area {
|
.content-area {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
Reference in New Issue
Block a user