Plain is headed towards 1.0! Subscribe for development updates →

  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4    <meta charset="UTF-8">
  5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6    <title>Querystats</title>
  7    {% tailwind_css %}
  8</head>
  9<body class="text-stone-300">
 10
 11    {% if querystats_enabled %}
 12    <div class="flex items-center justify-between border-b border-white/5 px-6 h-14 fixed top-0 left-0 right-0 bg-stone-950 z-10">
 13        <!-- <h1 class="text-lg font-semibold">Querystats</h1> -->
 14        <div></div>
 15        <div class="flex items-center space-x-2">
 16            <form method="get" action=".">
 17                {{ csrf_input }}
 18                <button type="submit" class="px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Reload</button>
 19            </form>
 20            <form method="post" action=".">
 21                {{ csrf_input }}
 22                <input type="hidden" name="querystats_action" value="clear">
 23                <button type="submit" class="px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Clear</button>
 24            </form>
 25            <form method="post" action=".">
 26                {{ csrf_input }}
 27                <input type="hidden" name="querystats_action" value="disable">
 28                <button type="submit" class="px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Disable</button>
 29            </form>
 30        </div>
 31    </div>
 32    {% endif %}
 33
 34    {% if querystats %}
 35    <div class="flex mt-2 h-full">
 36        <aside id="sidebar" class="fixed left-0 top-14 bottom-0 w-82 overflow-auto p-4">
 37            <ul class="space-y-2">
 38                {% for request_id, qs in querystats.items() %}
 39                <li>
 40                    <button data-request-id="{{ request_id }}" class="w-full text-left px-2 py-1 rounded hover:bg-stone-700 cursor-pointer">
 41                        <span class="text-sm">{{ qs.request.path }}</span>
 42                        <span class="font-semibold bg-white/5 rounded-sm px-1 py-0.5 text-xs">{{ qs.request.method }}</span>
 43                        <div class="text-xs text-stone-400">{{ qs.summary }}</div>
 44                        <div class="text-xs text-stone-500">{{ qs.timestamp|fromisoformat|timesince }} ago</div>
 45                    </button>
 46                </li>
 47                {% endfor %}
 48            </ul>
 49        </aside>
 50
 51        <main id="content" class="flex-1 p-6 overflow-auto ml-82 mt-14">
 52            {% for request_id, qs in querystats.items() %}
 53            <div class="request-detail" data-request-id="{{ request_id }}" style="display: none;">
 54                <div class="flex justify-between">
 55                    <div>
 56                        <h2 class="font-medium text-sm"><span class="font-semibold">{{ qs.request.method }}</span> {{ qs.request.path }}</h2>
 57                        <p class="text-sm text-white/70">{{ qs.summary }}</p>
 58                    </div>
 59                    <div class="text-right">
 60                        <div class="text-xs text-white/60">Request ID <code>{{ qs.request.unique_id }}</code></div>
 61                        <div class="text-xs text-white/60"><code>{{ qs.timestamp|fromisoformat }}</code></div>
 62                    </div>
 63                </div>
 64
 65                <div class="flex w-full mt-3 overflow-auto rounded-sm">
 66                    {% for query in qs.queries %}
 67                    <a href="#query-{{ loop.index }}"
 68                        {{ loop.cycle('class=\"h-2 bg-amber-400\"', 'class=\"h-2 bg-orange-400\"', 'class=\"h-2 bg-yellow-400\"', 'class=\"h-2 bg-amber-600\"')|safe }}
 69                        title="[{{ query.duration_display }}] {{ query.sql_display }}"
 70                        style="width: {{ query.duration / qs.total_time * 100 }}%">
 71                    </a>
 72                    {% endfor %}
 73                </div>
 74
 75                <div class="mt-4 space-y-3 text-xs">
 76                    {% for query in qs.queries %}
 77                    <details id="query-{{ loop.index }}" class="p-2 rounded bg-white/5">
 78                        <summary class="truncate">
 79                            <div class="float-right px-2 py-px mb-px ml-2 text-xs rounded-full bg-zinc-700">
 80                                <span>{{ query.duration_display }}</span>
 81                                {% if query.duplicate_count is defined %}
 82                                <span class="text-red-500">&nbsp; duplicated {{ query.duplicate_count }} times</span>
 83                                {% endif %}
 84                            </div>
 85                            <code class="font-mono">{{ query.sql }}</code>
 86                        </summary>
 87                        <div class="space-y-3 mt-3">
 88                            <div>
 89                                <pre><code class="font-mono whitespace-pre-wrap text-zinc-100">{{ query.sql_display }}</code></pre>
 90                            </div>
 91                            <div class="text-zinc-400">
 92                                <span class="font-medium">Parameters</span>
 93                                <pre><code class="font-mono">{{ query.params|pprint }}</code></pre>
 94                            </div>
 95                            {% if query.tb|default(false) %}
 96                            <details>
 97                                <summary>Traceback</summary>
 98                                <pre><code class="block overflow-x-auto font-mono text-xs">{{ query.tb }}</code></pre>
 99                            </details>
100                            {% endif %}
101                        </div>
102                    </details>
103                    {% else %}
104                    <div>No queries...</div>
105                    {% endfor %}
106                </div>
107            </div>
108            {% endfor %}
109        </main>
110    </div>
111    {% elif querystats_enabled %}
112    <div class="text-center text-white/30 py-8">Querystats are enabled but nothing has been recorded yet.</div>
113    {% else %}
114    <div class="text-center py-8">
115        <div class="text-white/30">Querystats are disabled.</div>
116        <form method="post" action=".">
117            {{ csrf_input }}
118            <input type="hidden" name="querystats_action" value="enable">
119            <button type="submit" class="mt-2 px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Enable</button>
120        </form>
121    </div>
122    {% endif %}
123
124    <script>
125        document.addEventListener('DOMContentLoaded', function() {
126            const buttons = document.querySelectorAll('#sidebar [data-request-id]');
127            const details = document.querySelectorAll('#content .request-detail');
128            buttons.forEach(function(btn) {
129                btn.addEventListener('click', function(e) {
130                    e.preventDefault();
131                    const id = this.getAttribute('data-request-id');
132                    details.forEach(div => div.style.display = 'none');
133                    const sel = document.querySelector('#content .request-detail[data-request-id="' + id + '"]');
134                    if (sel) sel.style.display = 'block';
135                    buttons.forEach(b => b.classList.remove('bg-stone-700', 'text-white'));
136                    this.classList.add('bg-stone-700', 'text-white');
137                });
138            });
139            if (buttons.length > 0) buttons[0].click();
140        });
141    </script>
142
143    </body>
144</html>