Show more data in webapp deliveries-table
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
"blackforestbytes.com/simplecloudnotifier/models"
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
|
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetUserPreview swaggerdoc
|
// GetUserPreview swaggerdoc
|
||||||
@@ -52,7 +53,7 @@ func (h APIHandler) GetUserPreview(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return finishSuccess(ginext.JSON(http.StatusOK, user.JSONPreview()))
|
return finishSuccess(ginext.JSON(http.StatusOK, user.Preview()))
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -175,3 +176,65 @@ func (h APIHandler) GetUserKeyPreview(pctx ginext.PreContext) ginext.HTTPRespons
|
|||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetClientPreview swaggerdoc
|
||||||
|
//
|
||||||
|
// @Summary Get a client (similar to api-clients-get, but can be called from anyone and only returns a subset of fields)
|
||||||
|
// @ID api-clients-get-preview
|
||||||
|
// @Tags API-v2
|
||||||
|
//
|
||||||
|
// @Param cid path string true "ClientID"
|
||||||
|
//
|
||||||
|
// @Success 200 {object} handler.GetClientPreview.response
|
||||||
|
//
|
||||||
|
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||||
|
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||||
|
// @Failure 404 {object} ginresp.apiError "client not found"
|
||||||
|
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||||
|
//
|
||||||
|
// @Router /api/v2/preview/clients/{cid} [GET]
|
||||||
|
func (h APIHandler) GetClientPreview(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||||
|
type uri struct {
|
||||||
|
ClientID models.ClientID `uri:"cid" binding:"entityid"`
|
||||||
|
}
|
||||||
|
type response struct {
|
||||||
|
Client models.ClientPreview `json:"client"`
|
||||||
|
User models.UserPreview `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var u uri
|
||||||
|
ctx, g, errResp := pctx.URI(&u).Start()
|
||||||
|
if errResp != nil {
|
||||||
|
return *errResp
|
||||||
|
}
|
||||||
|
defer ctx.Cancel()
|
||||||
|
|
||||||
|
return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
|
||||||
|
|
||||||
|
if permResp := ctx.CheckPermissionAny(); permResp != nil {
|
||||||
|
return *permResp
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := h.database.GetClientByID(ctx, u.ClientID)
|
||||||
|
if client == nil {
|
||||||
|
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.database.GetUser(ctx, client.UserID)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishSuccess(ginext.JSON(http.StatusOK, response{
|
||||||
|
Client: client.Preview(),
|
||||||
|
User: user.Preview(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ func (r *Router) Init(e *ginext.GinWrapper) error {
|
|||||||
apiv2.GET("/preview/users/:uid").Handle(r.apiHandler.GetUserPreview)
|
apiv2.GET("/preview/users/:uid").Handle(r.apiHandler.GetUserPreview)
|
||||||
apiv2.GET("/preview/keys/:kid").Handle(r.apiHandler.GetUserKeyPreview)
|
apiv2.GET("/preview/keys/:kid").Handle(r.apiHandler.GetUserKeyPreview)
|
||||||
apiv2.GET("/preview/channels/:cid").Handle(r.apiHandler.GetChannelPreview)
|
apiv2.GET("/preview/channels/:cid").Handle(r.apiHandler.GetChannelPreview)
|
||||||
|
apiv2.GET("/preview/clients/:cid").Handle(r.apiHandler.GetClientPreview)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================ Send API (unversioned) ================
|
// ================ Send API (unversioned) ================
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ func (db *Database) GetClient(ctx db.TxContext, userid models.UserID, clientid m
|
|||||||
}, sq.SModeExtended, sq.Safe)
|
}, sq.SModeExtended, sq.Safe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetClientByID(ctx db.TxContext, clientid models.ClientID) (*models.Client, error) {
|
||||||
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sq.QuerySingleOpt[models.Client](ctx, tx, "SELECT * FROM clients WHERE deleted=0 AND client_id = :cid LIMIT 1", sq.PP{"cid": clientid}, sq.SModeExtended, sq.Safe)
|
||||||
|
}
|
||||||
|
|
||||||
func (db *Database) GetClientOpt(ctx db.TxContext, userid models.UserID, clientid models.ClientID) (*models.Client, error) {
|
func (db *Database) GetClientOpt(ctx db.TxContext, userid models.UserID, clientid models.ClientID) (*models.Client, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -21,3 +21,25 @@ type Client struct {
|
|||||||
Name *string `db:"name" json:"name"`
|
Name *string `db:"name" json:"name"`
|
||||||
Deleted bool `db:"deleted" json:"-"`
|
Deleted bool `db:"deleted" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClientPreview struct {
|
||||||
|
ClientID ClientID `json:"client_id"`
|
||||||
|
UserID UserID `json:"user_id"`
|
||||||
|
Type ClientType `json:"type"`
|
||||||
|
TimestampCreated SCNTime `json:"timestamp_created"`
|
||||||
|
AgentModel string `json:"agent_model"`
|
||||||
|
AgentVersion string `json:"agent_version"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) Preview() ClientPreview {
|
||||||
|
return ClientPreview{
|
||||||
|
ClientID: c.ClientID,
|
||||||
|
UserID: c.UserID,
|
||||||
|
Type: c.Type,
|
||||||
|
TimestampCreated: c.TimestampCreated,
|
||||||
|
AgentModel: c.AgentModel,
|
||||||
|
AgentVersion: c.AgentVersion,
|
||||||
|
Name: c.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func (u User) MaxTimestampDiffHours() int {
|
|||||||
return 24
|
return 24
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) JSONPreview() UserPreview {
|
func (u User) Preview() UserPreview {
|
||||||
return UserPreview{
|
return UserPreview{
|
||||||
UserID: u.UserID,
|
UserID: u.UserID,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { UserPreview } from "./user.model";
|
||||||
|
|
||||||
export type ClientType = 'ANDROID' | 'IOS' | 'LINUX' | 'MACOS' | 'WINDOWS';
|
export type ClientType = 'ANDROID' | 'IOS' | 'LINUX' | 'MACOS' | 'WINDOWS';
|
||||||
|
|
||||||
export interface Client {
|
export interface Client {
|
||||||
@@ -15,6 +17,19 @@ export interface ClientListResponse {
|
|||||||
clients: Client[];
|
clients: Client[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClientPreview {
|
||||||
|
client_id: string;
|
||||||
|
name: string | null;
|
||||||
|
type: ClientType;
|
||||||
|
agent_model: string;
|
||||||
|
agent_version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientPreviewResponse {
|
||||||
|
user: UserPreview;
|
||||||
|
client: ClientPreview;
|
||||||
|
}
|
||||||
|
|
||||||
export function getClientTypeIcon(type: ClientType): string {
|
export function getClientTypeIcon(type: ClientType): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'ANDROID':
|
case 'ANDROID':
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
UpdateKeyRequest,
|
UpdateKeyRequest,
|
||||||
Client,
|
Client,
|
||||||
ClientListResponse,
|
ClientListResponse,
|
||||||
|
ClientPreviewResponse,
|
||||||
SenderNameStatistics,
|
SenderNameStatistics,
|
||||||
SenderNameListResponse,
|
SenderNameListResponse,
|
||||||
DeliveryListResponse,
|
DeliveryListResponse,
|
||||||
@@ -93,6 +94,10 @@ export class ApiService {
|
|||||||
return this.http.delete<Client>(`${this.baseUrl}/users/${userId}/clients/${clientId}`);
|
return this.http.delete<Client>(`${this.baseUrl}/users/${userId}/clients/${clientId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getClientPreview(clientId: string): Observable<ClientPreviewResponse> {
|
||||||
|
return this.http.get<ClientPreviewResponse>(`${this.baseUrl}/preview/clients/${clientId}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
|
|||||||
44
webapp/src/app/core/services/client-cache.service.ts
Normal file
44
webapp/src/app/core/services/client-cache.service.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable, of, catchError, map, shareReplay } from 'rxjs';
|
||||||
|
import { ApiService } from './api.service';
|
||||||
|
|
||||||
|
export interface ResolvedClient {
|
||||||
|
clientId: string;
|
||||||
|
clientName: string | null;
|
||||||
|
userId: string;
|
||||||
|
userName: string | null;
|
||||||
|
agentModel: string;
|
||||||
|
agentVersion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ClientCacheService {
|
||||||
|
private apiService = inject(ApiService);
|
||||||
|
|
||||||
|
private cache = new Map<string, Observable<ResolvedClient | null>>();
|
||||||
|
|
||||||
|
resolveClient(clientId: string): Observable<ResolvedClient | null> {
|
||||||
|
if (!this.cache.has(clientId)) {
|
||||||
|
const request$ = this.apiService.getClientPreview(clientId).pipe(
|
||||||
|
map(response => ({
|
||||||
|
clientId: response.client.client_id,
|
||||||
|
clientName: response.client.name,
|
||||||
|
userId: response.user.user_id,
|
||||||
|
userName: response.user.username,
|
||||||
|
agentModel: response.client.agent_model,
|
||||||
|
agentVersion: response.client.agent_version,
|
||||||
|
})),
|
||||||
|
catchError(() => of(null)),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
this.cache.set(clientId, request$);
|
||||||
|
}
|
||||||
|
return this.cache.get(clientId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,18 +90,33 @@
|
|||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Client ID</th>
|
<th>Client</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Agent</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Retries</th>
|
<th>Retries</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
<th>Finalized</th>
|
<th>Finalized</th>
|
||||||
|
<th>FCM-ID</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (delivery of deliveriesTable.data; track delivery.delivery_id) {
|
@for (delivery of deliveriesTable.data; track delivery.delivery_id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="mono">{{ delivery.receiver_client_id }}</span>
|
<div class="cell-name">{{ getResolvedClient(delivery.receiver_client_id)?.clientName || '-' }}</div>
|
||||||
|
<div class="cell-id mono">{{ delivery.receiver_client_id }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-name">{{ getResolvedClient(delivery.receiver_client_id)?.userName || '-' }}</div>
|
||||||
|
<div class="cell-id mono">{{ delivery.receiver_user_id }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (getResolvedClient(delivery.receiver_client_id); as client) {
|
||||||
|
{{ client.agentModel }} {{ client.agentVersion }}
|
||||||
|
} @else {
|
||||||
|
-
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<nz-tag [nzColor]="getStatusColor(delivery.status)">
|
<nz-tag [nzColor]="getStatusColor(delivery.status)">
|
||||||
@@ -111,6 +126,20 @@
|
|||||||
<td>{{ delivery.retry_count }}</td>
|
<td>{{ delivery.retry_count }}</td>
|
||||||
<td>{{ delivery.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</td>
|
<td>{{ delivery.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</td>
|
||||||
<td>{{ delivery.timestamp_finalized ? (delivery.timestamp_finalized | date:'yyyy-MM-dd HH:mm:ss') : '-' }}</td>
|
<td>{{ delivery.timestamp_finalized ? (delivery.timestamp_finalized | date:'yyyy-MM-dd HH:mm:ss') : '-' }}</td>
|
||||||
|
<td>
|
||||||
|
@if (delivery.fcm_message_id) {
|
||||||
|
<span
|
||||||
|
nz-icon
|
||||||
|
nzType="copy"
|
||||||
|
class="action-icon"
|
||||||
|
nz-tooltip
|
||||||
|
nzTooltipTitle="Copy FCM ID"
|
||||||
|
[appCopyToClipboard]="delivery.fcm_message_id"
|
||||||
|
></span>
|
||||||
|
} @else {
|
||||||
|
-
|
||||||
|
}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ import { NzTagModule } from 'ng-zorro-antd/tag';
|
|||||||
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||||
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
|
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
|
||||||
import { NzTableModule } from 'ng-zorro-antd/table';
|
import { NzTableModule } from 'ng-zorro-antd/table';
|
||||||
|
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||||
import { ApiService } from '../../../core/services/api.service';
|
import { ApiService } from '../../../core/services/api.service';
|
||||||
import { AuthService } from '../../../core/services/auth.service';
|
import { AuthService } from '../../../core/services/auth.service';
|
||||||
import { NotificationService } from '../../../core/services/notification.service';
|
import { NotificationService } from '../../../core/services/notification.service';
|
||||||
import { SettingsService } from '../../../core/services/settings.service';
|
import { SettingsService } from '../../../core/services/settings.service';
|
||||||
import { KeyCacheService, ResolvedKey } from '../../../core/services/key-cache.service';
|
import { KeyCacheService, ResolvedKey } from '../../../core/services/key-cache.service';
|
||||||
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
||||||
|
import { ClientCacheService, ResolvedClient } from '../../../core/services/client-cache.service';
|
||||||
import { Message, Delivery } from '../../../core/models';
|
import { Message, Delivery } from '../../../core/models';
|
||||||
|
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
|
||||||
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';
|
||||||
|
|
||||||
@@ -31,10 +34,12 @@ import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/c
|
|||||||
NzSpinModule,
|
NzSpinModule,
|
||||||
NzPopconfirmModule,
|
NzPopconfirmModule,
|
||||||
NzTableModule,
|
NzTableModule,
|
||||||
|
NzToolTipModule,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
RelativeTimePipe,
|
RelativeTimePipe,
|
||||||
MetadataGridComponent,
|
MetadataGridComponent,
|
||||||
MetadataValueComponent,
|
MetadataValueComponent,
|
||||||
|
CopyToClipboardDirective,
|
||||||
],
|
],
|
||||||
templateUrl: './message-detail.component.html',
|
templateUrl: './message-detail.component.html',
|
||||||
styleUrl: './message-detail.component.scss'
|
styleUrl: './message-detail.component.scss'
|
||||||
@@ -48,11 +53,13 @@ export class MessageDetailComponent implements OnInit {
|
|||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private keyCacheService = inject(KeyCacheService);
|
private keyCacheService = inject(KeyCacheService);
|
||||||
private userCacheService = inject(UserCacheService);
|
private userCacheService = inject(UserCacheService);
|
||||||
|
private clientCacheService = inject(ClientCacheService);
|
||||||
|
|
||||||
message = signal<Message | null>(null);
|
message = signal<Message | null>(null);
|
||||||
resolvedKey = signal<ResolvedKey | null>(null);
|
resolvedKey = signal<ResolvedKey | null>(null);
|
||||||
resolvedChannelOwner = signal<ResolvedUser | null>(null);
|
resolvedChannelOwner = signal<ResolvedUser | null>(null);
|
||||||
deliveries = signal<Delivery[]>([]);
|
deliveries = signal<Delivery[]>([]);
|
||||||
|
resolvedClients = signal<Map<string, ResolvedClient>>(new Map());
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
deleting = signal(false);
|
deleting = signal(false);
|
||||||
loadingDeliveries = signal(false);
|
loadingDeliveries = signal(false);
|
||||||
@@ -107,6 +114,7 @@ export class MessageDetailComponent implements OnInit {
|
|||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.deliveries.set(response.deliveries);
|
this.deliveries.set(response.deliveries);
|
||||||
this.loadingDeliveries.set(false);
|
this.loadingDeliveries.set(false);
|
||||||
|
this.resolveDeliveryClients(response.deliveries);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loadingDeliveries.set(false);
|
this.loadingDeliveries.set(false);
|
||||||
@@ -114,6 +122,27 @@ export class MessageDetailComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveDeliveryClients(deliveries: Delivery[]): void {
|
||||||
|
const uniqueClientIds = [...new Set(deliveries.map(d => d.receiver_client_id))];
|
||||||
|
for (const clientId of uniqueClientIds) {
|
||||||
|
this.clientCacheService.resolveClient(clientId).subscribe({
|
||||||
|
next: (resolved) => {
|
||||||
|
if (resolved) {
|
||||||
|
this.resolvedClients.update(map => {
|
||||||
|
const newMap = new Map(map);
|
||||||
|
newMap.set(clientId, resolved);
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getResolvedClient(clientId: string): ResolvedClient | undefined {
|
||||||
|
return this.resolvedClients().get(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
private resolveKey(keyId: string): void {
|
private resolveKey(keyId: string): void {
|
||||||
this.keyCacheService.resolveKey(keyId).subscribe({
|
this.keyCacheService.resolveKey(keyId).subscribe({
|
||||||
next: (resolved) => this.resolvedKey.set(resolved)
|
next: (resolved) => this.resolvedKey.set(resolved)
|
||||||
|
|||||||
Reference in New Issue
Block a user