|
|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
import { LitElement, html, css, property, state, customElement } from './plugins.js';
|
|
|
|
|
import { LitElement, html, css, property, state, customElement, DeesContextmenu } from './plugins.js';
|
|
|
|
|
import type { CSSResult, TemplateResult } from './plugins.js';
|
|
|
|
|
import { sharedStyles, panelStyles, tableStyles, buttonStyles } from './sw-dash-styles.js';
|
|
|
|
|
|
|
|
|
|
@@ -21,6 +21,19 @@ export interface ITypedRequestStats {
|
|
|
|
|
avgDurationMs: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Grouped request/response pair by correlationId
|
|
|
|
|
*/
|
|
|
|
|
export interface IGroupedRequest {
|
|
|
|
|
correlationId: string;
|
|
|
|
|
method: string;
|
|
|
|
|
request?: ITypedRequestLogEntry;
|
|
|
|
|
response?: ITypedRequestLogEntry;
|
|
|
|
|
timestamp: number;
|
|
|
|
|
durationMs?: number;
|
|
|
|
|
hasError: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TRequestFilter = 'all' | 'outgoing' | 'incoming';
|
|
|
|
|
type TPhaseFilter = 'all' | 'request' | 'response';
|
|
|
|
|
|
|
|
|
|
@@ -159,20 +172,6 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
color: var(--accent-error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.request-payload {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
padding: var(--space-2);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
max-height: 150px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
margin-top: var(--space-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.request-error {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--accent-error);
|
|
|
|
|
@@ -238,6 +237,17 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
padding: var(--space-2);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.method-stat-card:hover {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.method-stat-card.active {
|
|
|
|
|
background: rgba(99, 102, 241, 0.15);
|
|
|
|
|
border: 1px solid var(--accent-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.method-stat-name {
|
|
|
|
|
@@ -288,22 +298,188 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toggle-payload {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--accent-primary);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
margin-top: var(--space-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toggle-payload:hover {
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.correlation-id {
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Grouped request card */
|
|
|
|
|
.request-card .request-response-badges {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: var(--space-2);
|
|
|
|
|
margin-top: var(--space-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.request-card .status-badge {
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-badge.has-request {
|
|
|
|
|
background: rgba(251, 191, 36, 0.15);
|
|
|
|
|
color: var(--accent-warning);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-badge.has-response {
|
|
|
|
|
background: rgba(99, 102, 241, 0.15);
|
|
|
|
|
color: var(--accent-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-badge.pending {
|
|
|
|
|
background: rgba(156, 163, 175, 0.15);
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-show-payload {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border: 1px solid var(--border-default);
|
|
|
|
|
color: var(--accent-primary);
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
padding: var(--space-1) var(--space-2);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
margin-top: var(--space-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-show-payload:hover {
|
|
|
|
|
background: var(--accent-primary);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Modal styles */
|
|
|
|
|
.payload-modal-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
z-index: 10000;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: var(--space-4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payload-modal {
|
|
|
|
|
background: var(--bg-primary);
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
border: 1px solid var(--border-default);
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
height: 90vh;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: var(--space-3) var(--space-4);
|
|
|
|
|
border-bottom: 1px solid var(--border-default);
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-title {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-subtitle {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
margin-top: var(--space-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-close {
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: var(--space-1);
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-close:hover {
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-body {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 1px;
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: var(--border-default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payload-panel {
|
|
|
|
|
background: var(--bg-primary);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payload-panel-header {
|
|
|
|
|
padding: var(--space-2) var(--space-3);
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-bottom: 1px solid var(--border-default);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--space-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payload-panel-header .badge {
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payload-panel-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
padding: var(--space-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payload-json {
|
|
|
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payload-empty {
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
font-style: italic;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
padding: var(--space-4);
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payload-meta {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
padding: var(--space-2) var(--space-3);
|
|
|
|
|
border-top: 1px solid var(--border-default);
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.payload-error {
|
|
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
|
|
color: var(--accent-error);
|
|
|
|
|
padding: var(--space-2) var(--space-3);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
border-bottom: 1px solid rgba(239, 68, 68, 0.2);
|
|
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
@@ -318,9 +494,12 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
@state() accessor phaseFilter: TPhaseFilter = 'all';
|
|
|
|
|
@state() accessor methodFilter = '';
|
|
|
|
|
@state() accessor searchText = '';
|
|
|
|
|
@state() accessor expandedPayloads: Set<string> = new Set();
|
|
|
|
|
@state() accessor isLoadingMore = false;
|
|
|
|
|
|
|
|
|
|
// Modal state
|
|
|
|
|
@state() accessor modalOpen = false;
|
|
|
|
|
@state() accessor selectedGroup: IGroupedRequest | null = null;
|
|
|
|
|
|
|
|
|
|
private handleDirectionFilterChange(e: Event): void {
|
|
|
|
|
this.directionFilter = (e.target as HTMLSelectElement).value as TRequestFilter;
|
|
|
|
|
// Local filtering - no HTTP request
|
|
|
|
|
@@ -336,6 +515,11 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
// Local filtering - no HTTP request
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setMethodFilter(method: string): void {
|
|
|
|
|
// Toggle: clicking the same method clears the filter
|
|
|
|
|
this.methodFilter = this.methodFilter === method ? '' : method;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleSearch(e: Event): void {
|
|
|
|
|
this.searchText = (e.target as HTMLInputElement).value.toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
@@ -373,14 +557,120 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
}, 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private togglePayload(correlationId: string): void {
|
|
|
|
|
const newSet = new Set(this.expandedPayloads);
|
|
|
|
|
if (newSet.has(correlationId)) {
|
|
|
|
|
newSet.delete(correlationId);
|
|
|
|
|
} else {
|
|
|
|
|
newSet.add(correlationId);
|
|
|
|
|
private openPayloadModal(group: IGroupedRequest): void {
|
|
|
|
|
this.selectedGroup = group;
|
|
|
|
|
this.modalOpen = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleContextMenu(event: MouseEvent, group: IGroupedRequest): void {
|
|
|
|
|
// Build full message object for copying
|
|
|
|
|
const fullMessage = {
|
|
|
|
|
correlationId: group.correlationId,
|
|
|
|
|
method: group.method,
|
|
|
|
|
timestamp: group.timestamp,
|
|
|
|
|
durationMs: group.durationMs,
|
|
|
|
|
request: group.request ? {
|
|
|
|
|
direction: group.request.direction,
|
|
|
|
|
phase: group.request.phase,
|
|
|
|
|
timestamp: group.request.timestamp,
|
|
|
|
|
payload: group.request.payload,
|
|
|
|
|
} : null,
|
|
|
|
|
response: group.response ? {
|
|
|
|
|
direction: group.response.direction,
|
|
|
|
|
phase: group.response.phase,
|
|
|
|
|
timestamp: group.response.timestamp,
|
|
|
|
|
durationMs: group.response.durationMs,
|
|
|
|
|
payload: group.response.payload,
|
|
|
|
|
error: group.response.error,
|
|
|
|
|
} : null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DeesContextmenu.openContextMenuWithOptions(event, [
|
|
|
|
|
{
|
|
|
|
|
name: 'Copy Full Message',
|
|
|
|
|
iconName: 'copy',
|
|
|
|
|
action: async () => {
|
|
|
|
|
await navigator.clipboard.writeText(JSON.stringify(fullMessage, null, 2));
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Copy Request Payload',
|
|
|
|
|
iconName: 'upload',
|
|
|
|
|
disabled: !group.request,
|
|
|
|
|
action: async () => {
|
|
|
|
|
if (group.request) {
|
|
|
|
|
await navigator.clipboard.writeText(JSON.stringify(group.request.payload, null, 2));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Copy Response Payload',
|
|
|
|
|
iconName: 'download',
|
|
|
|
|
disabled: !group.response,
|
|
|
|
|
action: async () => {
|
|
|
|
|
if (group.response) {
|
|
|
|
|
await navigator.clipboard.writeText(JSON.stringify(group.response.payload, null, 2));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{ divider: true },
|
|
|
|
|
{
|
|
|
|
|
name: 'Copy Correlation ID',
|
|
|
|
|
iconName: 'hash',
|
|
|
|
|
action: async () => {
|
|
|
|
|
await navigator.clipboard.writeText(group.correlationId);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Copy Method Name',
|
|
|
|
|
iconName: 'tag',
|
|
|
|
|
action: async () => {
|
|
|
|
|
await navigator.clipboard.writeText(group.method);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{ divider: true },
|
|
|
|
|
{
|
|
|
|
|
name: 'Filter by Method',
|
|
|
|
|
iconName: 'filter',
|
|
|
|
|
action: async () => {
|
|
|
|
|
this.setMethodFilter(group.method);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Show Payload',
|
|
|
|
|
iconName: 'eye',
|
|
|
|
|
action: async () => {
|
|
|
|
|
this.openPayloadModal(group);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private closeModal(): void {
|
|
|
|
|
this.modalOpen = false;
|
|
|
|
|
this.selectedGroup = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleModalOverlayClick(e: Event): void {
|
|
|
|
|
if ((e.target as HTMLElement).classList.contains('payload-modal-overlay')) {
|
|
|
|
|
this.closeModal();
|
|
|
|
|
}
|
|
|
|
|
this.expandedPayloads = newSet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleKeydown = (e: KeyboardEvent): void => {
|
|
|
|
|
if (e.key === 'Escape' && this.modalOpen) {
|
|
|
|
|
this.closeModal();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
connectedCallback(): void {
|
|
|
|
|
super.connectedCallback();
|
|
|
|
|
document.addEventListener('keydown', this.handleKeydown);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnectedCallback(): void {
|
|
|
|
|
super.disconnectedCallback();
|
|
|
|
|
document.removeEventListener('keydown', this.handleKeydown);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private formatTimestamp(ts: number): string {
|
|
|
|
|
@@ -440,10 +730,127 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
|
|
|
|
const filteredLogs = this.getFilteredLogs();
|
|
|
|
|
/**
|
|
|
|
|
* Group filtered logs by correlationId to show request/response pairs together
|
|
|
|
|
*/
|
|
|
|
|
private getGroupedLogs(): IGroupedRequest[] {
|
|
|
|
|
const filtered = this.getFilteredLogs();
|
|
|
|
|
const groups = new Map<string, IGroupedRequest>();
|
|
|
|
|
|
|
|
|
|
for (const log of filtered) {
|
|
|
|
|
let group = groups.get(log.correlationId);
|
|
|
|
|
|
|
|
|
|
if (!group) {
|
|
|
|
|
group = {
|
|
|
|
|
correlationId: log.correlationId,
|
|
|
|
|
method: log.method,
|
|
|
|
|
timestamp: log.timestamp,
|
|
|
|
|
hasError: false,
|
|
|
|
|
};
|
|
|
|
|
groups.set(log.correlationId, group);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (log.phase === 'request') {
|
|
|
|
|
group.request = log;
|
|
|
|
|
// Update timestamp to the earliest (request time)
|
|
|
|
|
if (log.timestamp < group.timestamp) {
|
|
|
|
|
group.timestamp = log.timestamp;
|
|
|
|
|
}
|
|
|
|
|
} else if (log.phase === 'response') {
|
|
|
|
|
group.response = log;
|
|
|
|
|
if (log.durationMs !== undefined) {
|
|
|
|
|
group.durationMs = log.durationMs;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (log.error) {
|
|
|
|
|
group.hasError = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert to array and sort by timestamp (newest first)
|
|
|
|
|
return Array.from(groups.values()).sort((a, b) => b.timestamp - a.timestamp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render the payload modal
|
|
|
|
|
*/
|
|
|
|
|
private renderModal(): TemplateResult | null {
|
|
|
|
|
if (!this.modalOpen || !this.selectedGroup) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const group = this.selectedGroup;
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<div class="payload-modal-overlay" @click="${this.handleModalOverlayClick}">
|
|
|
|
|
<div class="payload-modal">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="modal-title">${group.method}</div>
|
|
|
|
|
<div class="modal-subtitle">
|
|
|
|
|
Correlation ID: ${group.correlationId}
|
|
|
|
|
${group.durationMs !== undefined ? html` | Duration: ${this.formatDuration(group.durationMs)}` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="modal-close" @click="${this.closeModal}">×</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<!-- Request Panel (Left) -->
|
|
|
|
|
<div class="payload-panel">
|
|
|
|
|
<div class="payload-panel-header">
|
|
|
|
|
<span class="badge phase-request">REQUEST</span>
|
|
|
|
|
${group.request ? html`
|
|
|
|
|
<span class="badge direction-${group.request.direction}">${group.request.direction}</span>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
${group.request ? html`
|
|
|
|
|
<div class="payload-meta">
|
|
|
|
|
Timestamp: ${this.formatTimestamp(group.request.timestamp)}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="payload-panel-content">
|
|
|
|
|
<pre class="payload-json">${JSON.stringify(group.request.payload, null, 2)}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
` : html`
|
|
|
|
|
<div class="payload-empty">No request data captured</div>
|
|
|
|
|
`}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Response Panel (Right) -->
|
|
|
|
|
<div class="payload-panel">
|
|
|
|
|
<div class="payload-panel-header">
|
|
|
|
|
<span class="badge phase-response">RESPONSE</span>
|
|
|
|
|
${group.response ? html`
|
|
|
|
|
<span class="badge direction-${group.response.direction}">${group.response.direction}</span>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
${group.response?.error ? html`
|
|
|
|
|
<div class="payload-error">Error: ${group.response.error}</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${group.response ? html`
|
|
|
|
|
<div class="payload-meta">
|
|
|
|
|
Timestamp: ${this.formatTimestamp(group.response.timestamp)}
|
|
|
|
|
${group.response.durationMs !== undefined ? html` | Duration: ${this.formatDuration(group.response.durationMs)}` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="payload-panel-content">
|
|
|
|
|
<pre class="payload-json">${JSON.stringify(group.response.payload, null, 2)}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
` : html`
|
|
|
|
|
<div class="payload-empty">No response yet (pending)</div>
|
|
|
|
|
`}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
|
|
|
|
const groupedLogs = this.getGroupedLogs();
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
${this.renderModal()}
|
|
|
|
|
|
|
|
|
|
<!-- Stats Bar -->
|
|
|
|
|
<div class="stats-bar">
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
@@ -463,7 +870,7 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
<span class="stat-label">Avg Duration</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="stat-value">${filteredLogs.length}</span>
|
|
|
|
|
<span class="stat-value">${groupedLogs.length}</span>
|
|
|
|
|
<span class="stat-label">Showing</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -474,7 +881,10 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
<div class="method-stats-title">Methods</div>
|
|
|
|
|
<div class="method-stats-grid">
|
|
|
|
|
${Object.entries(this.stats.methodCounts).slice(0, 8).map(([method, data]) => html`
|
|
|
|
|
<div class="method-stat-card">
|
|
|
|
|
<div
|
|
|
|
|
class="method-stat-card ${this.methodFilter === method ? 'active' : ''}"
|
|
|
|
|
@click="${() => this.setMethodFilter(method)}"
|
|
|
|
|
>
|
|
|
|
|
<div class="method-stat-name" title="${method}">${method}</div>
|
|
|
|
|
<div class="method-stat-details">
|
|
|
|
|
<span>${data.requests} req</span>
|
|
|
|
|
@@ -506,9 +916,9 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
<span class="filter-label">Method:</span>
|
|
|
|
|
<select class="filter-select" @change="${this.handleMethodFilterChange}">
|
|
|
|
|
<select class="filter-select" .value="${this.methodFilter}" @change="${this.handleMethodFilterChange}">
|
|
|
|
|
<option value="">All Methods</option>
|
|
|
|
|
${this.methods.map(m => html`<option value="${m}">${m}</option>`)}
|
|
|
|
|
${this.methods.map(m => html`<option value="${m}" ?selected="${this.methodFilter === m}">${m}</option>`)}
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
@@ -523,46 +933,54 @@ export class SwDashRequests extends LitElement {
|
|
|
|
|
<button class="btn clear-btn" @click="${this.handleClear}">Clear Logs</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Request List -->
|
|
|
|
|
<!-- Request List (Grouped by correlationId) -->
|
|
|
|
|
${this.logs.length === 0 ? html`
|
|
|
|
|
<div class="empty-state">No request logs found. Traffic will appear here as TypedRequests are made.</div>
|
|
|
|
|
` : filteredLogs.length === 0 ? html`
|
|
|
|
|
` : groupedLogs.length === 0 ? html`
|
|
|
|
|
<div class="empty-state">No logs match filter</div>
|
|
|
|
|
` : html`
|
|
|
|
|
<div class="requests-list">
|
|
|
|
|
${filteredLogs.map(log => html`
|
|
|
|
|
<div class="request-card ${log.error ? 'has-error' : ''}">
|
|
|
|
|
${groupedLogs.map(group => html`
|
|
|
|
|
<div
|
|
|
|
|
class="request-card ${group.hasError ? 'has-error' : ''}"
|
|
|
|
|
@contextmenu="${(e: MouseEvent) => this.handleContextMenu(e, group)}"
|
|
|
|
|
>
|
|
|
|
|
<div class="request-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="request-badges">
|
|
|
|
|
<span class="badge direction-${log.direction}">${log.direction}</span>
|
|
|
|
|
<span class="badge phase-${log.phase}">${log.phase}</span>
|
|
|
|
|
${log.error ? html`<span class="badge error">error</span>` : ''}
|
|
|
|
|
${group.request ? html`
|
|
|
|
|
<span class="badge direction-${group.request.direction}">${group.request.direction}</span>
|
|
|
|
|
` : ''}
|
|
|
|
|
${group.hasError ? html`<span class="badge error">error</span>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="method-name">${group.method}</div>
|
|
|
|
|
<div class="correlation-id">${group.correlationId}</div>
|
|
|
|
|
<div class="request-response-badges">
|
|
|
|
|
<span class="status-badge ${group.request ? 'has-request' : 'pending'}">
|
|
|
|
|
${group.request ? 'REQ' : 'REQ pending'}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="status-badge ${group.response ? 'has-response' : 'pending'}">
|
|
|
|
|
${group.response ? 'RES' : 'RES pending'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="method-name">${log.method}</div>
|
|
|
|
|
<div class="correlation-id">${log.correlationId}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="request-meta">
|
|
|
|
|
<span class="request-time">${this.formatTimestamp(log.timestamp)}</span>
|
|
|
|
|
${log.durationMs !== undefined ? html`
|
|
|
|
|
<span class="request-duration ${this.getDurationClass(log.durationMs)}">
|
|
|
|
|
${this.formatDuration(log.durationMs)}
|
|
|
|
|
<span class="request-time">${this.formatTimestamp(group.timestamp)}</span>
|
|
|
|
|
${group.durationMs !== undefined ? html`
|
|
|
|
|
<span class="request-duration ${this.getDurationClass(group.durationMs)}">
|
|
|
|
|
${this.formatDuration(group.durationMs)}
|
|
|
|
|
</span>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${log.error ? html`
|
|
|
|
|
<div class="request-error">${log.error}</div>
|
|
|
|
|
${group.response?.error ? html`
|
|
|
|
|
<div class="request-error">${group.response.error}</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
<div class="toggle-payload" @click="${() => this.togglePayload(log.correlationId)}">
|
|
|
|
|
${this.expandedPayloads.has(log.correlationId) ? 'Hide payload' : 'Show payload'}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${this.expandedPayloads.has(log.correlationId) ? html`
|
|
|
|
|
<div class="request-payload">${JSON.stringify(log.payload, null, 2)}</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
<button class="btn-show-payload" @click="${() => this.openPayloadModal(group)}">
|
|
|
|
|
Show Payload
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
`)}
|
|
|
|
|
</div>
|
|
|
|
|
|