WebApp: Fix channel-detail page for non-owned channels
This commit is contained in:
@@ -21,6 +21,9 @@ export interface ChannelPreview {
|
|||||||
owner_user_id: string;
|
owner_user_id: string;
|
||||||
internal_name: string;
|
internal_name: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
|
description_name: string | null;
|
||||||
|
messages_sent: number;
|
||||||
|
subscription: Subscription | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChannelSelector = 'owned' | 'subscribed' | 'all' | 'subscribed_any' | 'all_any';
|
export type ChannelSelector = 'owned' | 'subscribed' | 'all' | 'subscribed_any' | 'all_any';
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ export interface ClientListResponse {
|
|||||||
|
|
||||||
export interface ClientPreview {
|
export interface ClientPreview {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
|
user_id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
type: ClientType;
|
type: ClientType;
|
||||||
|
timestamp_created: string;
|
||||||
agent_model: string;
|
agent_model: string;
|
||||||
agent_version: string;
|
agent_version: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export interface KeyToken {
|
|||||||
export interface KeyTokenPreview {
|
export interface KeyTokenPreview {
|
||||||
keytoken_id: string;
|
keytoken_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
owner_user_id: string;
|
||||||
|
all_channels: boolean;
|
||||||
|
channels: string[];
|
||||||
|
permissions: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TokenPermission = 'A' | 'CR' | 'CS' | 'UR';
|
export type TokenPermission = 'A' | 'CR' | 'CS' | 'UR';
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
User,
|
User,
|
||||||
UserWithExtra,
|
UserWithExtra,
|
||||||
UserPreview,
|
UserPreview,
|
||||||
|
ChannelPreview,
|
||||||
|
KeyTokenPreview,
|
||||||
Message,
|
Message,
|
||||||
MessageListParams,
|
MessageListParams,
|
||||||
MessageListResponse,
|
MessageListResponse,
|
||||||
@@ -98,6 +100,14 @@ export class ApiService {
|
|||||||
return this.http.get<ClientPreviewResponse>(`${this.baseUrl}/preview/clients/${clientId}`);
|
return this.http.get<ClientPreviewResponse>(`${this.baseUrl}/preview/clients/${clientId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChannelPreview(channelId: string): Observable<ChannelPreview> {
|
||||||
|
return this.http.get<ChannelPreview>(`${this.baseUrl}/preview/channels/${channelId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeyPreview(keyId: string): Observable<KeyTokenPreview> {
|
||||||
|
return this.http.get<KeyTokenPreview>(`${this.baseUrl}/preview/keys/${keyId}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Channel endpoints
|
// Channel endpoints
|
||||||
getChannels(userId: string, selector?: ChannelSelector): Observable<ChannelListResponse> {
|
getChannels(userId: string, selector?: ChannelSelector): Observable<ChannelListResponse> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="loading-container">
|
<div class="loading-container">
|
||||||
<nz-spin nzSimple nzSize="large"></nz-spin>
|
<nz-spin nzSimple nzSize="large"></nz-spin>
|
||||||
</div>
|
</div>
|
||||||
} @else if (channel()) {
|
} @else if (channelData()) {
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<button nz-button (click)="goBack()">
|
<button nz-button (click)="goBack()">
|
||||||
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
||||||
@@ -32,13 +32,13 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nz-card [nzTitle]="channel()!.display_name">
|
<nz-card [nzTitle]="channelData()!.display_name">
|
||||||
<scn-metadata-grid>
|
<scn-metadata-grid>
|
||||||
<scn-metadata-value label="Channel ID">
|
<scn-metadata-value label="Channel ID">
|
||||||
<span class="mono">{{ channel()!.channel_id }}</span>
|
<span class="mono">{{ channelData()!.channel_id }}</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Internal Name">
|
<scn-metadata-value label="Internal Name">
|
||||||
<span class="mono">{{ channel()!.internal_name }}</span>
|
<span class="mono">{{ channelData()!.internal_name }}</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Status">
|
<scn-metadata-value label="Status">
|
||||||
<nz-tag [nzColor]="getSubscriptionStatus().color">
|
<nz-tag [nzColor]="getSubscriptionStatus().color">
|
||||||
@@ -46,16 +46,22 @@
|
|||||||
</nz-tag>
|
</nz-tag>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Owner">
|
<scn-metadata-value label="Owner">
|
||||||
<span class="mono">{{ channel()!.owner_user_id }}</span>
|
@if (resolvedOwner()) {
|
||||||
|
<div class="owner-name">{{ resolvedOwner()!.displayName }}</div>
|
||||||
|
<div class="owner-id mono">{{ channelData()!.owner_user_id }}</div>
|
||||||
|
} @else {
|
||||||
|
<span class="mono">{{ channelData()!.owner_user_id }}</span>
|
||||||
|
}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
@if (channel()!.description_name) {
|
@if (channelData()!.description_name) {
|
||||||
<scn-metadata-value label="Description">
|
<scn-metadata-value label="Description">
|
||||||
{{ channel()!.description_name }}
|
{{ channelData()!.description_name }}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
}
|
}
|
||||||
<scn-metadata-value label="Messages Sent">
|
<scn-metadata-value label="Messages Sent">
|
||||||
{{ channel()!.messages_sent }}
|
{{ channelData()!.messages_sent }}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
@if (channel()) {
|
||||||
<scn-metadata-value label="Last Sent">
|
<scn-metadata-value label="Last Sent">
|
||||||
@if (channel()!.timestamp_lastsent) {
|
@if (channel()!.timestamp_lastsent) {
|
||||||
<div class="timestamp-absolute">{{ channel()!.timestamp_lastsent | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
<div class="timestamp-absolute">{{ channel()!.timestamp_lastsent | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
||||||
@@ -68,7 +74,8 @@
|
|||||||
<div class="timestamp-absolute">{{ channel()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
<div class="timestamp-absolute">{{ channel()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
||||||
<div class="timestamp-relative">{{ channel()!.timestamp_created | relativeTime }}</div>
|
<div class="timestamp-relative">{{ channel()!.timestamp_created | relativeTime }}</div>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
@if (isOwner() && channel()!.subscribe_key) {
|
}
|
||||||
|
@if (isOwner() && channel()?.subscribe_key) {
|
||||||
<scn-metadata-value label="Subscribe Key">
|
<scn-metadata-value label="Subscribe Key">
|
||||||
<div class="key-field">
|
<div class="key-field">
|
||||||
<nz-input-group [nzSuffix]="subscribeKeySuffix">
|
<nz-input-group [nzSuffix]="subscribeKeySuffix">
|
||||||
|
|||||||
@@ -109,6 +109,17 @@
|
|||||||
overflow-y: clip;
|
overflow-y: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.owner-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-id {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.text-muted {
|
.text-muted {
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { AuthService } from '../../../core/services/auth.service';
|
|||||||
import { NotificationService } from '../../../core/services/notification.service';
|
import { NotificationService } from '../../../core/services/notification.service';
|
||||||
import { SettingsService } from '../../../core/services/settings.service';
|
import { SettingsService } from '../../../core/services/settings.service';
|
||||||
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
||||||
import { ChannelWithSubscription, Subscription, Message } from '../../../core/models';
|
import { ChannelWithSubscription, ChannelPreview, Subscription, Message } 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';
|
||||||
import { QrCodeDisplayComponent } from '../../../shared/components/qr-code-display/qr-code-display.component';
|
import { QrCodeDisplayComponent } from '../../../shared/components/qr-code-display/qr-code-display.component';
|
||||||
@@ -68,9 +68,11 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
private userCacheService = inject(UserCacheService);
|
private userCacheService = inject(UserCacheService);
|
||||||
|
|
||||||
channel = signal<ChannelWithSubscription | null>(null);
|
channel = signal<ChannelWithSubscription | null>(null);
|
||||||
|
channelPreview = signal<ChannelPreview | null>(null);
|
||||||
subscriptions = signal<Subscription[]>([]);
|
subscriptions = signal<Subscription[]>([]);
|
||||||
messages = signal<Message[]>([]);
|
messages = signal<Message[]>([]);
|
||||||
userNames = signal<Map<string, ResolvedUser>>(new Map());
|
userNames = signal<Map<string, ResolvedUser>>(new Map());
|
||||||
|
resolvedOwner = signal<ResolvedUser | null>(null);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
loadingSubscriptions = signal(false);
|
loadingSubscriptions = signal(false);
|
||||||
loadingMessages = signal(false);
|
loadingMessages = signal(false);
|
||||||
@@ -115,19 +117,31 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
|
this.apiService.getChannelPreview(channelId).subscribe({
|
||||||
|
next: (preview) => {
|
||||||
|
this.channelPreview.set(preview);
|
||||||
|
this.resolveOwner(preview.owner_user_id);
|
||||||
|
if (preview.owner_user_id === userId) {
|
||||||
this.apiService.getChannel(userId, channelId).subscribe({
|
this.apiService.getChannel(userId, channelId).subscribe({
|
||||||
next: (channel) => {
|
next: (channel) => {
|
||||||
this.channel.set(channel);
|
this.channel.set(channel);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
if (this.isOwner()) {
|
|
||||||
this.loadSubscriptions(channelId);
|
this.loadSubscriptions(channelId);
|
||||||
}
|
|
||||||
this.loadMessages(channelId);
|
this.loadMessages(channelId);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.loading.set(false);
|
||||||
|
this.loadMessages(channelId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSubscriptions(channelId: string): void {
|
loadSubscriptions(channelId: string): void {
|
||||||
@@ -148,14 +162,13 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadMessages(channelId: string, nextPageToken?: string): void {
|
loadMessages(channelId: string, nextPageToken?: string): void {
|
||||||
const userId = this.authService.getUserId();
|
|
||||||
if (!userId) return;
|
|
||||||
|
|
||||||
this.loadingMessages.set(true);
|
this.loadingMessages.set(true);
|
||||||
this.apiService.getChannelMessages(userId, channelId, {
|
this.apiService.getMessages({
|
||||||
|
channel_id: [channelId],
|
||||||
page_size: this.messagesPageSize,
|
page_size: this.messagesPageSize,
|
||||||
next_page_token: nextPageToken,
|
next_page_token: nextPageToken,
|
||||||
trimmed: true
|
trimmed: true,
|
||||||
|
subscription_status: 'all'
|
||||||
}).subscribe({
|
}).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.messages.set(response.messages);
|
this.messages.set(response.messages);
|
||||||
@@ -210,6 +223,12 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveOwner(ownerId: string): void {
|
||||||
|
this.userCacheService.resolveUser(ownerId).subscribe(resolved => {
|
||||||
|
this.resolvedOwner.set(resolved);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private resolveUserNames(subscriptions: Subscription[]): void {
|
private resolveUserNames(subscriptions: Subscription[]): void {
|
||||||
const userIds = new Set<string>();
|
const userIds = new Set<string>();
|
||||||
for (const sub of subscriptions) {
|
for (const sub of subscriptions) {
|
||||||
@@ -232,9 +251,16 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isOwner(): boolean {
|
isOwner(): boolean {
|
||||||
const channel = this.channel();
|
|
||||||
const userId = this.authService.getUserId();
|
const userId = this.authService.getUserId();
|
||||||
return channel?.owner_user_id === userId;
|
const channel = this.channel();
|
||||||
|
if (channel) return channel.owner_user_id === userId;
|
||||||
|
const preview = this.channelPreview();
|
||||||
|
if (preview) return preview.owner_user_id === userId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelData() {
|
||||||
|
return this.channel() ?? this.channelPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit methods
|
// Edit methods
|
||||||
@@ -290,18 +316,20 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSubscriptionStatus(): { label: string; color: string } {
|
getSubscriptionStatus(): { label: string; color: string } {
|
||||||
const channel = this.channel();
|
const data = this.channelData();
|
||||||
if (!channel) return { label: 'Unknown', color: 'default' };
|
if (!data) return { label: 'Unknown', color: 'default' };
|
||||||
|
|
||||||
|
const subscription = 'subscribe_key' in data ? data.subscription : data.subscription;
|
||||||
|
|
||||||
if (this.isOwner()) {
|
if (this.isOwner()) {
|
||||||
if (channel.subscription) {
|
if (subscription) {
|
||||||
return { label: 'Owned & Subscribed', color: 'green' };
|
return { label: 'Owned & Subscribed', color: 'green' };
|
||||||
}
|
}
|
||||||
return { label: 'Owned', color: 'blue' };
|
return { label: 'Owned', color: 'blue' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel.subscription) {
|
if (subscription) {
|
||||||
if (channel.subscription.confirmed) {
|
if (subscription.confirmed) {
|
||||||
return { label: 'Subscribed', color: 'green' };
|
return { label: 'Subscribed', color: 'green' };
|
||||||
}
|
}
|
||||||
return { label: 'Pending', color: 'orange' };
|
return { label: 'Pending', color: 'orange' };
|
||||||
@@ -377,7 +405,7 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isUserSubscribed(): boolean {
|
isUserSubscribed(): boolean {
|
||||||
return this.channel()?.subscription !== null;
|
return this.channelData()?.subscription !== null && this.channelData()?.subscription !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSelfSubscription(): void {
|
toggleSelfSubscription(): void {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<div class="loading-container">
|
<div class="loading-container">
|
||||||
<nz-spin nzSimple nzSize="large"></nz-spin>
|
<nz-spin nzSimple nzSize="large"></nz-spin>
|
||||||
</div>
|
</div>
|
||||||
} @else if (client()) {
|
} @else if (clientData()) {
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<button nz-button (click)="goBack()">
|
<button nz-button (click)="goBack()">
|
||||||
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
||||||
Back to Clients
|
Back to Clients
|
||||||
</button>
|
</button>
|
||||||
@if (expertMode()) {
|
@if (isOwner() && expertMode()) {
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button
|
<button
|
||||||
nz-button
|
nz-button
|
||||||
@@ -29,36 +29,38 @@
|
|||||||
<div class="client-header">
|
<div class="client-header">
|
||||||
<span
|
<span
|
||||||
nz-icon
|
nz-icon
|
||||||
[nzType]="getClientIcon(client()!.type)"
|
[nzType]="getClientIcon(clientData()!.type)"
|
||||||
nzTheme="outline"
|
nzTheme="outline"
|
||||||
class="client-type-icon"
|
class="client-type-icon"
|
||||||
></span>
|
></span>
|
||||||
<h2 class="client-title">{{ client()!.name || 'Unnamed Client' }}</h2>
|
<h2 class="client-title">{{ clientData()!.name || 'Unnamed Client' }}</h2>
|
||||||
<nz-tag>{{ getClientTypeLabel(client()!.type) }}</nz-tag>
|
<nz-tag>{{ getClientTypeLabel(clientData()!.type) }}</nz-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<scn-metadata-grid>
|
<scn-metadata-grid>
|
||||||
<scn-metadata-value label="Client ID">
|
<scn-metadata-value label="Client ID">
|
||||||
<span class="mono">{{ client()!.client_id }}</span>
|
<span class="mono">{{ clientData()!.client_id }}</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Type">
|
<scn-metadata-value label="Type">
|
||||||
<nz-tag>{{ getClientTypeLabel(client()!.type) }}</nz-tag>
|
<nz-tag>{{ getClientTypeLabel(clientData()!.type) }}</nz-tag>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Agent">
|
<scn-metadata-value label="Agent">
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<span>{{ client()!.agent_model }}</span>
|
<span>{{ clientData()!.agent_model }}</span>
|
||||||
<span class="agent-version">v{{ client()!.agent_version }}</span>
|
<span class="agent-version">v{{ clientData()!.agent_version }}</span>
|
||||||
</div>
|
</div>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Created">
|
<scn-metadata-value label="Created">
|
||||||
<div class="timestamp-absolute">{{ client()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
<div class="timestamp-absolute">{{ clientData()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
||||||
<div class="timestamp-relative">{{ client()!.timestamp_created | relativeTime }}</div>
|
<div class="timestamp-relative">{{ clientData()!.timestamp_created | relativeTime }}</div>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
@if (client()) {
|
||||||
<scn-metadata-value label="FCM Token">
|
<scn-metadata-value label="FCM Token">
|
||||||
<span class="mono fcm-token" nz-tooltip [nzTooltipTitle]="client()!.fcm_token">
|
<span class="mono fcm-token" nz-tooltip [nzTooltipTitle]="client()!.fcm_token">
|
||||||
{{ client()!.fcm_token }}
|
{{ client()!.fcm_token }}
|
||||||
</span>
|
</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
}
|
||||||
</scn-metadata-grid>
|
</scn-metadata-grid>
|
||||||
</nz-card>
|
</nz-card>
|
||||||
} @else {
|
} @else {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ 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 { SettingsService } from '../../../core/services/settings.service';
|
import { SettingsService } from '../../../core/services/settings.service';
|
||||||
import { Client, ClientType, getClientTypeIcon } from '../../../core/models';
|
import { Client, ClientPreview, ClientType, getClientTypeIcon } from '../../../core/models';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
||||||
|
|
||||||
@@ -45,6 +45,7 @@ export class ClientDetailComponent implements OnInit {
|
|||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
|
|
||||||
client = signal<Client | null>(null);
|
client = signal<Client | null>(null);
|
||||||
|
clientPreview = signal<ClientPreview | null>(null);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
expertMode = this.settingsService.expertMode;
|
expertMode = this.settingsService.expertMode;
|
||||||
|
|
||||||
@@ -60,6 +61,10 @@ export class ClientDetailComponent implements OnInit {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
|
this.apiService.getClientPreview(clientId).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.clientPreview.set(response.client);
|
||||||
|
if (response.client.user_id === userId) {
|
||||||
this.apiService.getClient(userId, clientId).subscribe({
|
this.apiService.getClient(userId, clientId).subscribe({
|
||||||
next: (client) => {
|
next: (client) => {
|
||||||
this.client.set(client);
|
this.client.set(client);
|
||||||
@@ -69,6 +74,27 @@ export class ClientDetailComponent implements OnInit {
|
|||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clientData() {
|
||||||
|
return this.client() ?? this.clientPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwner(): boolean {
|
||||||
|
const userId = this.authService.getUserId();
|
||||||
|
const client = this.client();
|
||||||
|
if (client) return client.user_id === userId;
|
||||||
|
const preview = this.clientPreview();
|
||||||
|
if (preview) return preview.user_id === userId;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
goBack(): void {
|
goBack(): void {
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
<div class="loading-container">
|
<div class="loading-container">
|
||||||
<nz-spin nzSimple nzSize="large"></nz-spin>
|
<nz-spin nzSimple nzSize="large"></nz-spin>
|
||||||
</div>
|
</div>
|
||||||
} @else if (key()) {
|
} @else if (keyData()) {
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<button nz-button (click)="goBack()">
|
<button nz-button (click)="goBack()">
|
||||||
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
||||||
Back to Keys
|
Back to Keys
|
||||||
</button>
|
</button>
|
||||||
|
@if (isOwner()) {
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button nz-button (click)="openEditModal()">
|
<button nz-button (click)="openEditModal()">
|
||||||
<span nz-icon nzType="edit"></span>
|
<span nz-icon nzType="edit"></span>
|
||||||
@@ -27,11 +28,12 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nz-card>
|
<nz-card>
|
||||||
<div class="key-header">
|
<div class="key-header">
|
||||||
<h2 class="key-title">{{ key()!.name }}</h2>
|
<h2 class="key-title">{{ keyData()!.name }}</h2>
|
||||||
@if (isCurrentKey()) {
|
@if (isCurrentKey()) {
|
||||||
<nz-tag nzColor="cyan">Current</nz-tag>
|
<nz-tag nzColor="cyan">Current</nz-tag>
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
|
|
||||||
<scn-metadata-grid>
|
<scn-metadata-grid>
|
||||||
<scn-metadata-value label="Key ID">
|
<scn-metadata-value label="Key ID">
|
||||||
<span class="mono">{{ key()!.keytoken_id }}</span>
|
<span class="mono">{{ keyData()!.keytoken_id }}</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Permissions">
|
<scn-metadata-value label="Permissions">
|
||||||
<div class="permissions">
|
<div class="permissions">
|
||||||
@@ -55,11 +57,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Channel Access">
|
<scn-metadata-value label="Channel Access">
|
||||||
@if (key()!.all_channels) {
|
@if (keyData()!.all_channels) {
|
||||||
<nz-tag nzColor="default">All Channels</nz-tag>
|
<nz-tag nzColor="default">All Channels</nz-tag>
|
||||||
} @else if (key()!.channels && key()!.channels.length > 0) {
|
} @else if (keyData()!.channels && keyData()!.channels.length > 0) {
|
||||||
<div class="channel-list">
|
<div class="channel-list">
|
||||||
@for (channelId of key()!.channels; track channelId) {
|
@for (channelId of keyData()!.channels; track channelId) {
|
||||||
<nz-tag nzColor="orange" nz-tooltip [nzTooltipTitle]="channelId">
|
<nz-tag nzColor="orange" nz-tooltip [nzTooltipTitle]="channelId">
|
||||||
{{ getChannelDisplayName(channelId) }}
|
{{ getChannelDisplayName(channelId) }}
|
||||||
</nz-tag>
|
</nz-tag>
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
<span class="text-muted">No channels</span>
|
<span class="text-muted">No channels</span>
|
||||||
}
|
}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
@if (key()) {
|
||||||
<scn-metadata-value label="Messages Sent">
|
<scn-metadata-value label="Messages Sent">
|
||||||
{{ key()!.messages_sent }}
|
{{ key()!.messages_sent }}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
@@ -84,12 +87,13 @@
|
|||||||
<span class="text-muted">Never</span>
|
<span class="text-muted">Never</span>
|
||||||
}
|
}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
}
|
||||||
<scn-metadata-value label="Owner">
|
<scn-metadata-value label="Owner">
|
||||||
@if (resolvedOwner()) {
|
@if (resolvedOwner()) {
|
||||||
<div class="owner-name">{{ resolvedOwner()!.displayName }}</div>
|
<div class="owner-name">{{ resolvedOwner()!.displayName }}</div>
|
||||||
<div class="owner-id mono">{{ key()!.owner_user_id }}</div>
|
<div class="owner-id mono">{{ keyData()!.owner_user_id }}</div>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="mono">{{ key()!.owner_user_id }}</span>
|
<span class="mono">{{ keyData()!.owner_user_id }}</span>
|
||||||
}
|
}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
</scn-metadata-grid>
|
</scn-metadata-grid>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { AuthService } from '../../../core/services/auth.service';
|
|||||||
import { NotificationService } from '../../../core/services/notification.service';
|
import { NotificationService } from '../../../core/services/notification.service';
|
||||||
import { ChannelCacheService, ResolvedChannel } from '../../../core/services/channel-cache.service';
|
import { ChannelCacheService, ResolvedChannel } from '../../../core/services/channel-cache.service';
|
||||||
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
||||||
import { KeyToken, parsePermissions, TokenPermission, ChannelWithSubscription, Message } from '../../../core/models';
|
import { KeyToken, KeyTokenPreview, parsePermissions, TokenPermission, ChannelWithSubscription, Message } from '../../../core/models';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
||||||
|
|
||||||
@@ -72,6 +72,7 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
private userCacheService = inject(UserCacheService);
|
private userCacheService = inject(UserCacheService);
|
||||||
|
|
||||||
key = signal<KeyToken | null>(null);
|
key = signal<KeyToken | null>(null);
|
||||||
|
keyPreview = signal<KeyTokenPreview | null>(null);
|
||||||
currentKeyId = signal<string | null>(null);
|
currentKeyId = signal<string | null>(null);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
channelNames = signal<Map<string, ResolvedChannel>>(new Map());
|
channelNames = signal<Map<string, ResolvedChannel>>(new Map());
|
||||||
@@ -105,8 +106,6 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
const keyId = this.route.snapshot.paramMap.get('id');
|
const keyId = this.route.snapshot.paramMap.get('id');
|
||||||
if (keyId) {
|
if (keyId) {
|
||||||
this.loadKey(keyId);
|
this.loadKey(keyId);
|
||||||
this.loadCurrentKey();
|
|
||||||
this.loadAvailableChannels();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,18 +114,34 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
|
this.apiService.getKeyPreview(keyId).subscribe({
|
||||||
|
next: (preview) => {
|
||||||
|
this.keyPreview.set(preview);
|
||||||
|
this.resolveOwner(preview.owner_user_id);
|
||||||
|
this.resolveChannelNamesFromPreview(preview);
|
||||||
|
if (preview.owner_user_id === userId) {
|
||||||
|
this.loadCurrentKey();
|
||||||
|
this.loadAvailableChannels();
|
||||||
this.apiService.getKey(userId, keyId).subscribe({
|
this.apiService.getKey(userId, keyId).subscribe({
|
||||||
next: (key) => {
|
next: (key) => {
|
||||||
this.key.set(key);
|
this.key.set(key);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.resolveChannelNames(key);
|
this.resolveChannelNames(key);
|
||||||
this.resolveOwner(key.owner_user_id);
|
|
||||||
this.loadMessages(keyId);
|
this.loadMessages(keyId);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.loading.set(false);
|
||||||
|
this.loadMessages(keyId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMessages(keyId: string, nextPageToken?: string): void {
|
loadMessages(keyId: string, nextPageToken?: string): void {
|
||||||
@@ -217,6 +232,27 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveChannelNamesFromPreview(preview: KeyTokenPreview): void {
|
||||||
|
if (!preview.all_channels && preview.channels && preview.channels.length > 0) {
|
||||||
|
this.channelCacheService.resolveChannels(preview.channels).subscribe(resolved => {
|
||||||
|
this.channelNames.set(resolved);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyData() {
|
||||||
|
return this.key() ?? this.keyPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwner(): boolean {
|
||||||
|
const userId = this.authService.getUserId();
|
||||||
|
const key = this.key();
|
||||||
|
if (key) return key.owner_user_id === userId;
|
||||||
|
const preview = this.keyPreview();
|
||||||
|
if (preview) return preview.owner_user_id === userId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
goBack(): void {
|
goBack(): void {
|
||||||
this.router.navigate(['/keys']);
|
this.router.navigate(['/keys']);
|
||||||
}
|
}
|
||||||
@@ -227,8 +263,8 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPermissions(): TokenPermission[] {
|
getPermissions(): TokenPermission[] {
|
||||||
const key = this.key();
|
const data = this.keyData();
|
||||||
return key ? parsePermissions(key.permissions) : [];
|
return data ? parsePermissions(data.permissions) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getPermissionColor(perm: TokenPermission): string {
|
getPermissionColor(perm: TokenPermission): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user