<!-- htmlhint doctype-first:false -->
<!-- Observability Dashboard Partial -->
<div class="observability-container" x-data="{
viewMode: 'traces',
selectedTrace: null,
timeRange: '24h',
statusFilter: 'all',
minDuration: '',
maxDuration: '',
httpMethod: '',
userEmail: '',
nameSearch: '',
attributeSearch: '',
toolName: '',
showAdvancedFilters: false,
loading: false,
stats: {},
tracesInterval: null,
statsInterval: null,
savedQueries: [],
selectedQueryId: '',
showSaveQueryModal: false,
saveQueryName: '',
saveQueryDescription: '',
saveQueryIsShared: false,
async loadMetricsView() {
const response = await fetch('{{ root_path }}/admin/observability/metrics/partial');
if (response.ok) {
const html = await response.text();
const container = document.getElementById('metrics-container');
container.innerHTML = html;
// Execute any script tags (innerHTML doesn't execute them)
const scripts = container.querySelectorAll('script');
scripts.forEach(script => {
const newScript = document.createElement('script');
if (script.src) {
newScript.src = script.src;
} else {
newScript.textContent = script.textContent;
}
document.head.appendChild(newScript);
});
// Wait for scripts to execute, then re-initialize Alpine.js
setTimeout(() => {
if (window.Alpine) {
window.Alpine.initTree(container);
}
}, 10);
}
},
async loadToolsView() {
const response = await fetch('{{ root_path }}/admin/observability/tools/partial');
if (response.ok) {
const html = await response.text();
const container = document.getElementById('tools-container');
container.innerHTML = html;
// Execute any script tags (innerHTML doesn't execute them)
const scripts = container.querySelectorAll('script');
scripts.forEach(script => {
const newScript = document.createElement('script');
if (script.src) {
newScript.src = script.src;
} else {
newScript.textContent = script.textContent;
}
document.head.appendChild(newScript);
});
// Wait for scripts to execute, then re-initialize Alpine.js
setTimeout(() => {
if (window.Alpine) {
window.Alpine.initTree(container);
}
}, 10);
}
},
async loadPromptsView() {
const response = await fetch('{{ root_path }}/admin/observability/prompts/partial');
if (response.ok) {
const html = await response.text();
const container = document.getElementById('prompts-container');
container.innerHTML = html;
// Execute any script tags (innerHTML doesn't execute them)
const scripts = container.querySelectorAll('script');
scripts.forEach(script => {
const newScript = document.createElement('script');
if (script.src) {
newScript.src = script.src;
} else {
newScript.textContent = script.textContent;
}
document.head.appendChild(newScript);
});
// Wait for scripts to execute, then re-initialize Alpine.js
setTimeout(() => {
if (window.Alpine) {
window.Alpine.initTree(container);
}
}, 10);
}
},
async loadResourcesView() {
const response = await fetch('{{ root_path }}/admin/observability/resources/partial');
if (response.ok) {
const html = await response.text();
const container = document.getElementById('resources-container');
container.innerHTML = html;
// Execute any script tags (innerHTML doesn't execute them)
const scripts = container.querySelectorAll('script');
scripts.forEach(script => {
const newScript = document.createElement('script');
if (script.src) {
newScript.src = script.src;
} else {
newScript.textContent = script.textContent;
}
document.head.appendChild(newScript);
});
// Wait for scripts to execute, then re-initialize Alpine.js
setTimeout(() => {
if (window.Alpine) {
window.Alpine.initTree(container);
}
}, 10);
}
},
refreshTraces() {
let url = `{{ root_path }}/admin/observability/traces?time_range=${this.timeRange}&status_filter=${this.statusFilter}&limit=50`;
if (this.minDuration) url += `&min_duration=${this.minDuration}`;
if (this.maxDuration) url += `&max_duration=${this.maxDuration}`;
if (this.httpMethod) url += `&http_method=${this.httpMethod}`;
if (this.userEmail) url += `&user_email=${encodeURIComponent(this.userEmail)}`;
if (this.nameSearch) url += `&name_search=${encodeURIComponent(this.nameSearch)}`;
if (this.attributeSearch) url += `&attribute_search=${encodeURIComponent(this.attributeSearch)}`;
if (this.toolName) url += `&tool_name=${encodeURIComponent(this.toolName)}`;
htmx.ajax('GET', url, {target: '#traces-list', swap: 'innerHTML'});
},
refreshStats() {
htmx.ajax('GET', '{{ root_path }}/admin/observability/stats', {target: '#stats-container', swap: 'innerHTML'});
},
startPolling() {
this.refreshTraces();
this.refreshStats();
this.tracesInterval = setInterval(() => this.refreshTraces(), 5000);
this.statsInterval = setInterval(() => this.refreshStats(), 30000);
},
stopPolling() {
if (this.tracesInterval) clearInterval(this.tracesInterval);
if (this.statsInterval) clearInterval(this.statsInterval);
},
applyFilters() {
this.stopPolling();
this.refreshTraces();
this.startPolling();
},
async loadSavedQueries() {
try {
const response = await fetch('{{ root_path }}/admin/observability/queries');
if (response.ok) {
this.savedQueries = await response.json();
}
} catch (e) {
console.error('Failed to load saved queries:', e);
}
},
async applySavedQuery() {
if (!this.selectedQueryId) return;
try {
const response = await fetch(`{{ root_path }}/admin/observability/queries/${this.selectedQueryId}`);
if (response.ok) {
const query = await response.json();
const config = query.filter_config;
this.timeRange = config.timeRange || '24h';
this.statusFilter = config.statusFilter || 'all';
this.minDuration = config.minDuration || '';
this.maxDuration = config.maxDuration || '';
this.toolName = config.toolName || '';
this.httpMethod = config.httpMethod || '';
this.userEmail = config.userEmail || '';
this.nameSearch = config.nameSearch || '';
this.attributeSearch = config.attributeSearch || '';
this.applyFilters();
// Track usage
await fetch(`{{ root_path }}/admin/observability/queries/${this.selectedQueryId}/use`, { method: 'POST' });
}
} catch (e) {
console.error('Failed to apply saved query:', e);
}
},
getCurrentFilterConfig() {
return {
timeRange: this.timeRange,
statusFilter: this.statusFilter,
minDuration: this.minDuration,
maxDuration: this.maxDuration,
httpMethod: this.httpMethod,
userEmail: this.userEmail,
nameSearch: this.nameSearch,
attributeSearch: this.attributeSearch,
toolName: this.toolName
};
},
async saveCurrentQuery() {
if (!this.saveQueryName) {
alert('Please enter a name for the query');
return;
}
try {
const response = await fetch('{{ root_path }}/admin/observability/queries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: this.saveQueryName,
description: this.saveQueryDescription,
filter_config: this.getCurrentFilterConfig(),
is_shared: this.saveQueryIsShared
})
});
if (response.ok) {
this.showSaveQueryModal = false;
this.saveQueryName = '';
this.saveQueryDescription = '';
this.saveQueryIsShared = false;
await this.loadSavedQueries();
alert('Query saved successfully!');
} else {
alert('Failed to save query');
}
} catch (e) {
console.error('Failed to save query:', e);
alert('Failed to save query');
}
},
async deleteSavedQuery(queryId) {
if (!confirm('Are you sure you want to delete this saved query?')) return;
try {
const response = await fetch(`{{ root_path }}/admin/observability/queries/${queryId}`, { method: 'DELETE' });
if (response.ok) {
await this.loadSavedQueries();
if (this.selectedQueryId == queryId) {
this.selectedQueryId = '';
}
}
} catch (e) {
console.error('Failed to delete query:', e);
}
}
}" x-init="
startPolling();
loadSavedQueries();
$watch('timeRange', () => applyFilters());
$watch('statusFilter', () => applyFilters());
$watch('minDuration', () => { if(minDuration) applyFilters(); });
$watch('maxDuration', () => { if(maxDuration) applyFilters(); });
$watch('httpMethod', () => applyFilters());
$watch('userEmail', () => { if(userEmail) applyFilters(); });
$watch('nameSearch', () => { if(nameSearch) applyFilters(); });
$watch('attributeSearch', () => { if(attributeSearch) applyFilters(); });
$watch('toolName', () => { if(toolName) applyFilters(); });
$watch('selectedQueryId', () => { if(selectedQueryId) applySavedQuery(); });
">
<style>
.observability-container {
/* No extra padding - parent already has p-4 lg:p-6 */
}
.view-toggle-btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
border: 1px solid #d1d5db;
background: white;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
}
.view-toggle-btn:hover {
background: #f3f4f6;
}
.view-toggle-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.obs-header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e5e7eb;
}
.obs-header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.obs-tabs-row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.obs-title {
font-size: 1.875rem;
font-weight: 700;
color: #1f2937;
display: flex;
align-items: center;
gap: 0.5rem;
}
.obs-filters {
display: flex;
gap: 1rem;
align-items: center;
}
.obs-filter-btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
border: 1px solid #d1d5db;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.obs-filter-btn:hover {
background: #f3f4f6;
}
.obs-filter-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border-left: 4px solid #3b82f6;
}
.stat-card.success {
border-left-color: #10b981;
}
.stat-card.error {
border-left-color: #ef4444;
}
.stat-card.warning {
border-left-color: #f59e0b;
}
.stat-label {
font-size: 0.875rem;
color: #6b7280;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #1f2937;
}
.stat-unit {
font-size: 1rem;
color: #6b7280;
margin-left: 0.25rem;
}
.traces-table {
width: 100%;
background: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
overflow: hidden;
}
.traces-table table {
width: 100%;
border-collapse: collapse;
}
.traces-table th {
background: #f9fafb;
padding: 0.75rem 1rem;
text-align: left;
font-size: 0.875rem;
font-weight: 600;
color: #6b7280;
border-bottom: 1px solid #e5e7eb;
}
.traces-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #e5e7eb;
}
.traces-table tr:hover {
background: #f9fafb;
cursor: pointer;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.status-ok {
background: #d1fae5;
color: #065f46;
}
.status-error {
background: #fee2e2;
color: #991b1b;
}
.duration-badge {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-family: monospace;
}
.duration-fast {
background: #d1fae5;
color: #065f46;
}
.duration-medium {
background: #fef3c7;
color: #92400e;
}
.duration-slow {
background: #fee2e2;
color: #991b1b;
}
.trace-detail-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.trace-detail-content {
background: white;
border-radius: 0.5rem;
max-width: 90%;
max-height: 90%;
overflow: auto;
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
}
.trace-detail-header {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.trace-detail-body {
padding: 1.5rem;
}
.waterfall {
margin-top: 1rem;
}
.span-row {
display: flex;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
.span-name {
width: 200px;
font-size: 0.875rem;
color: #374151;
}
.span-timeline {
flex: 1;
height: 24px;
position: relative;
background: #f3f4f6;
border-radius: 0.25rem;
}
.span-bar {
position: absolute;
height: 100%;
background: #3b82f6;
border-radius: 0.25rem;
display: flex;
align-items: center;
padding: 0 0.5rem;
font-size: 0.75rem;
color: white;
}
.span-bar.error {
background: #ef4444;
}
.span-duration {
width: 80px;
text-align: right;
font-size: 0.875rem;
color: #6b7280;
font-family: monospace;
}
.refresh-btn {
padding: 0.5rem 1rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
}
.refresh-btn:hover {
background: #2563eb;
}
.empty-state {
text-align: center;
padding: 3rem;
color: #6b7280;
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
/* Gantt Chart Styles */
.gantt-container {
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1rem;
min-height: 400px;
}
.gantt-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
.gantt-info {
font-size: 0.875rem;
color: #374151;
}
.gantt-controls {
display: flex;
gap: 0.5rem;
}
.gantt-btn {
padding: 0.375rem 0.75rem;
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.gantt-btn:hover {
background: #e5e7eb;
}
.gantt-timescale {
position: relative;
height: 40px;
border-bottom: 2px solid #d1d5db;
margin-bottom: 0.5rem;
}
.time-marker {
position: absolute;
top: 0;
display: flex;
flex-direction: column;
align-items: center;
}
.time-tick {
width: 1px;
height: 10px;
background: #9ca3af;
}
.time-label {
font-size: 0.75rem;
color: #6b7280;
margin-top: 0.25rem;
}
.gantt-spans {
max-height: 600px;
overflow-y: auto;
}
.span-row {
display: flex;
align-items: center;
padding: 0.25rem 0;
border-bottom: 1px solid #f3f4f6;
transition: background 0.1s;
}
.span-row:hover {
background: #f9fafb;
}
.critical-path-row {
background: #fef3c7 !important;
}
.span-name {
width: 250px;
font-size: 0.875rem;
color: #374151;
display: flex;
align-items: center;
gap: 0.25rem;
}
.span-toggle {
background: none;
border: none;
cursor: pointer;
font-size: 0.75rem;
padding: 0.125rem;
color: #6b7280;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.span-spacer {
width: 16px;
display: inline-block;
}
.span-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.span-timeline {
flex: 1;
height: 24px;
position: relative;
background: #f9fafb;
border-radius: 0.25rem;
margin: 0 0.5rem;
}
.span-bar {
position: absolute;
height: 100%;
background: #3b82f6;
border-radius: 0.25rem;
cursor: pointer;
transition: opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.span-bar:hover {
opacity: 0.8;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.critical-path-bar {
box-shadow: 0 0 0 2px #f59e0b;
}
.span-bar-label {
font-size: 0.7rem;
color: white;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.span-duration {
width: 100px;
text-align: right;
font-size: 0.75rem;
color: #6b7280;
font-family: monospace;
}
.gantt-legend {
display: flex;
gap: 1rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
font-size: 0.875rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 0.25rem;
}
.legend-color.critical-path {
background: #fbbf24;
border: 2px solid #f59e0b;
}
.view-toggle-btn {
padding: 0.5rem 1rem;
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.view-toggle-btn:hover {
background: #e5e7eb;
}
.view-toggle-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
</style>
<!-- Header -->
<div class="obs-header">
<!-- Title and Filters Row -->
<div class="obs-header-top">
<h1 class="obs-title">
<span>🔍</span>
Observability Dashboard
</h1>
<div class="obs-filters" x-show="viewMode === 'traces'" style="margin: 0;">
<select
x-model="selectedQueryId"
class="obs-filter-btn"
style="min-width: 200px;"
>
<option value="">📋 Saved Queries</option>
<template x-for="query in savedQueries" :key="query.id">
<option :value="query.id" x-text="query.name + (query.is_shared ? ' (shared)' : '')"></option>
</template>
</select>
<button
class="obs-filter-btn"
@click="showSaveQueryModal = true"
title="Save current filters as a query"
>
💾 Save Query
</button>
<select
x-model="timeRange"
class="obs-filter-btn"
>
<option value="1h">Last Hour</option>
<option value="6h">Last 6 Hours</option>
<option value="24h" selected>Last 24 Hours</option>
<option value="7d">Last 7 Days</option>
</select>
<select
x-model="statusFilter"
class="obs-filter-btn"
>
<option value="all" selected>All Status</option>
<option value="ok">Success Only</option>
<option value="error">Errors Only</option>
</select>
<button
class="obs-filter-btn"
@click="showAdvancedFilters = !showAdvancedFilters"
:class="{'active': showAdvancedFilters}"
>
⚙️ Advanced Filters
</button>
<button
class="refresh-btn"
@click="refreshStats(); refreshTraces();"
>
🔄 Refresh
</button>
</div>
</div>
<!-- View Mode Tabs Row -->
<div class="obs-tabs-row">
<button
@click="viewMode = 'traces'"
:class="{'active': viewMode === 'traces'}"
class="view-toggle-btn"
>
📋 Traces
</button>
<button
@click="viewMode = 'metrics'; loadMetricsView()"
:class="{'active': viewMode === 'metrics'}"
class="view-toggle-btn"
>
📊 Advanced Metrics
</button>
<button
@click="viewMode = 'tools'; loadToolsView()"
:class="{'active': viewMode === 'tools'}"
class="view-toggle-btn"
>
🔧 MCP Tools
</button>
<button
@click="viewMode = 'prompts'; loadPromptsView()"
:class="{'active': viewMode === 'prompts'}"
class="view-toggle-btn"
>
💬 Prompts
</button>
<button
@click="viewMode = 'resources'; loadResourcesView()"
:class="{'active': viewMode === 'resources'}"
class="view-toggle-btn"
>
📦 Resources
</button>
</div>
</div>
<!-- Traces View -->
<div x-show="viewMode === 'traces'">
<!-- Advanced Filters Panel -->
<div x-show="showAdvancedFilters"
x-transition
style="background: white; border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 1rem 0; font-size: 1.125rem; font-weight: 600; color: #374151;">
🔧 Advanced Filters
</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
<!-- Duration Range -->
<div>
<label style="display: block; font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.25rem;">
Min Duration (ms)
</label>
<input
type="number"
x-model="minDuration"
placeholder="e.g., 100"
class="obs-filter-btn"
style="width: 100%;"
@change="refreshTraces()"
/>
</div>
<div>
<label style="display: block; font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.25rem;">
Max Duration (ms)
</label>
<input
type="number"
x-model="maxDuration"
placeholder="e.g., 5000"
class="obs-filter-btn"
style="width: 100%;"
@change="refreshTraces()"
/>
</div>
<!-- HTTP Method -->
<div>
<label style="display: block; font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.25rem;">
HTTP Method
</label>
<select
x-model="httpMethod"
class="obs-filter-btn"
style="width: 100%;"
@change="refreshTraces()"
>
<option value="">All Methods</option>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<!-- User Email -->
<div>
<label style="display: block; font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.25rem;">
User Email
</label>
<input
type="text"
x-model="userEmail"
placeholder="admin@example.com"
class="obs-filter-btn"
style="width: 100%;"
@input.debounce.500ms="refreshTraces()"
/>
</div>
<!-- Name Search -->
<div>
<label style="display: block; font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.25rem;">
Trace Name
</label>
<input
type="text"
x-model="nameSearch"
placeholder="e.g., GET /api/tools"
class="obs-filter-btn"
style="width: 100%;"
@input.debounce.500ms="refreshTraces()"
/>
</div>
<!-- Attribute Search -->
<div>
<label style="display: block; font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.25rem;">
Attribute Search
</label>
<input
type="text"
x-model="attributeSearch"
placeholder="Search in attributes..."
class="obs-filter-btn"
style="width: 100%;"
@input.debounce.500ms="refreshTraces()"
/>
</div>
<!-- Tool Name Filter -->
<div>
<label style="display: block; font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.25rem;">
🔧 MCP Tool Name
</label>
<input
type="text"
x-model="toolName"
placeholder="Filter by tool name..."
class="obs-filter-btn"
style="width: 100%;"
@input.debounce.500ms="refreshTraces()"
/>
</div>
</div>
<!-- Clear Filters Button -->
<div style="margin-top: 1rem; text-align: right;">
<button
class="obs-filter-btn"
@click="minDuration=''; maxDuration=''; httpMethod=''; userEmail=''; nameSearch=''; attributeSearch=''; refreshTraces();"
style="padding: 0.5rem 1.5rem;"
>
🗑️ Clear All Filters
</button>
</div>
</div>
<!-- Statistics Cards -->
<div id="stats-container">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Loading statistics...</div>
</div>
</div>
</div>
<!-- Traces Table -->
<div class="traces-table">
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Method</th>
<th>Endpoint</th>
<th>Status</th>
<th>Duration</th>
<th>User</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="traces-list">
<tr>
<td colspan="7" style="text-align: center; padding: 2rem;">
Loading traces...
</td>
</tr>
</tbody>
</table>
</div>
<!-- Trace Detail Modal (shown when trace is selected) -->
<div x-show="selectedTrace"
x-cloak
class="trace-detail-modal"
@click.self="selectedTrace = null">
<div class="trace-detail-content" id="trace-detail-content">
<!-- Loaded via HTMX -->
</div>
</div>
<!-- Save Query Modal -->
<div x-show="showSaveQueryModal"
x-cloak
class="trace-detail-modal"
@click.self="showSaveQueryModal = false">
<div class="trace-detail-content" style="max-width: 600px; padding: 2rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 1rem;">
<h2 style="font-size: 1.5rem; font-weight: 700; color: #1f2937; margin: 0;">💾 Save Query</h2>
<button @click="showSaveQueryModal = false" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #6b7280;">×</button>
</div>
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
<!-- Query Name -->
<div>
<label style="display: block; font-size: 0.875rem; font-weight: 600; color: #374151; margin-bottom: 0.5rem;">
Query Name <span style="color: #ef4444;">*</span>
</label>
<input
x-model="saveQueryName"
type="text"
placeholder="e.g., Slow Requests Last Hour"
style="width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem; font-size: 0.875rem;"
/>
</div>
<!-- Query Description -->
<div>
<label style="display: block; font-size: 0.875rem; font-weight: 600; color: #374151; margin-bottom: 0.5rem;">
Description (optional)
</label>
<textarea
x-model="saveQueryDescription"
placeholder="What does this query help you find?"
rows="3"
style="width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem; font-size: 0.875rem; resize: vertical;"
></textarea>
</div>
<!-- Share with Team -->
<div style="display: flex; align-items: center; gap: 0.5rem;">
<input
x-model="saveQueryIsShared"
type="checkbox"
id="save-query-shared"
style="width: 1.125rem; height: 1.125rem;"
/>
<label for="save-query-shared" style="font-size: 0.875rem; font-weight: 500; color: #374151; cursor: pointer;">
Share with team (visible to all users)
</label>
</div>
<!-- Current Filters Preview -->
<div style="background: #f9fafb; border-radius: 0.5rem; padding: 1rem;">
<p style="font-size: 0.75rem; font-weight: 600; color: #6b7280; margin: 0 0 0.5rem 0; text-transform: uppercase;">Current Filters</p>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; font-size: 0.875rem; color: #374151;">
<div x-show="timeRange"><strong>Time:</strong> <span x-text="timeRange"></span></div>
<div x-show="statusFilter"><strong>Status:</strong> <span x-text="statusFilter"></span></div>
<div x-show="minDuration"><strong>Min Duration:</strong> <span x-text="minDuration + 'ms'"></span></div>
<div x-show="maxDuration"><strong>Max Duration:</strong> <span x-text="maxDuration + 'ms'"></span></div>
<div x-show="httpMethod"><strong>HTTP Method:</strong> <span x-text="httpMethod"></span></div>
<div x-show="userEmail"><strong>User:</strong> <span x-text="userEmail"></span></div>
<div x-show="nameSearch" style="grid-column: span 2;"><strong>Name:</strong> <span x-text="nameSearch"></span></div>
<div x-show="attributeSearch" style="grid-column: span 2;"><strong>Attributes:</strong> <span x-text="attributeSearch"></span></div>
</div>
</div>
<!-- Action Buttons -->
<div style="display: flex; justify-content: flex-end; gap: 1rem; padding-top: 1rem; border-top: 1px solid #e5e7eb;">
<button
@click="showSaveQueryModal = false"
style="padding: 0.5rem 1.5rem; border: 1px solid #d1d5db; background: white; color: #374151; border-radius: 0.375rem; cursor: pointer; font-weight: 500;"
>
Cancel
</button>
<button
@click="saveCurrentQuery()"
class="refresh-btn"
style="padding: 0.5rem 1.5rem;"
>
Save Query
</button>
</div>
</div>
</div>
</div>
</div> <!-- End Traces View -->
<!-- Metrics View -->
<div x-show="viewMode === 'metrics'" id="metrics-container">
<div style="padding: 2rem; text-align: center; color: #6b7280;">
Loading metrics dashboard...
</div>
</div>
<!-- Tools View -->
<div x-show="viewMode === 'tools'" id="tools-container">
<div style="padding: 2rem; text-align: center; color: #6b7280;">
Loading tool metrics dashboard...
</div>
</div>
<!-- Prompts View -->
<div x-show="viewMode === 'prompts'" id="prompts-container">
<div style="padding: 2rem; text-align: center; color: #6b7280;">
Loading prompt metrics dashboard...
</div>
</div>
<!-- Resources View -->
<div x-show="viewMode === 'resources'" id="resources-container">
<div style="padding: 2rem; text-align: center; color: #6b7280;">
Loading resource metrics dashboard...
</div>
</div>
</div>