refactor: migrate the frontend from AngularJS to Vue 3

This commit is contained in:
tdurieux
2026-09-09 13:19:53 +02:00
parent dc0ef022fb
commit 4a18f94631
65 changed files with 6164 additions and 8425 deletions
+71 -71
View File
@@ -4,8 +4,8 @@
<header class="errors-header">
<h1 class="paper-page-title">Errors</h1>
<div class="errors-actions">
<button class="btn btn-sm" type="button" ng-click="exportCsv()"><i class="fas fa-file-export"></i> Export CSV</button>
<button class="btn btn-sm btn-danger" type="button" ng-click="clearAll()"><i class="fas fa-trash"></i> Clear all</button>
<button class="btn btn-sm" type="button" @click="exportCsv()"><i class="fas fa-file-export"></i> Export CSV</button>
<button class="btn btn-sm btn-danger" type="button" @click="clearAll()"><i class="fas fa-trash"></i> Clear all</button>
</div>
</header>
@@ -21,33 +21,33 @@
<section class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">Last 24h</div>
<div class="kpi-value">{{stats.last24h}}</div>
<div class="kpi-sub" ng-class="{up: stats.delta > 0, down: stats.delta < 0}">
<span ng-if="stats.prev24h">{{stats.delta > 0 ? '+' : ''}}{{stats.delta}}% vs yesterday</span>
<span ng-if="!stats.prev24h">no prior baseline</span>
<div class="kpi-value">{{ stats?.last24h }}</div>
<div class="kpi-sub" :class="{up: stats?.delta &gt; 0, down: stats?.delta &lt; 0}">
<span v-if="stats?.prev24h">{{ stats?.delta > 0 ? '+' : '' }}{{ stats?.delta }}% vs yesterday</span>
<span v-if="!stats?.prev24h">no prior baseline</span>
</div>
</div>
<div class="kpi-card kpi-error">
<div class="kpi-label">Errors (5xx)</div>
<div class="kpi-value">{{stats.severity.error}}</div>
<div class="kpi-sub">{{stats.unique.error}} unique</div>
<div class="kpi-value">{{ stats?.severity?.error }}</div>
<div class="kpi-sub">{{ stats?.unique?.error }} unique</div>
</div>
<div class="kpi-card kpi-warn">
<div class="kpi-label">Warnings (4xx)</div>
<div class="kpi-value">{{stats.severity.warn}}</div>
<div class="kpi-sub">{{stats.unique.warn}} unique</div>
<div class="kpi-value">{{ stats?.severity?.warn }}</div>
<div class="kpi-sub">{{ stats?.unique?.warn }} unique</div>
</div>
<div class="kpi-card kpi-info">
<div class="kpi-label">Info (auth, 404)</div>
<div class="kpi-value">{{stats.severity.info}}</div>
<div class="kpi-sub">{{stats.unique.info}} unique</div>
<div class="kpi-value">{{ stats?.severity?.info }}</div>
<div class="kpi-sub">{{ stats?.unique?.info }} unique</div>
</div>
<div class="kpi-card" ng-class="{'kpi-error': stats.dropped > 0}">
<div class="kpi-card" :class="{&#x27;kpi-error&#x27;: stats?.dropped &gt; 0}">
<div class="kpi-label">Captured</div>
<div class="kpi-value">{{total}}</div>
<div class="kpi-value">{{ total }}</div>
<div class="kpi-sub">
cap {{cap}} · {{available ? 'live' : 'redis off'}}
<span ng-if="stats.dropped > 0" class="dropped-warn"> · {{stats.dropped}} dropped</span>
cap {{ cap }} · {{ available ? 'live' : 'redis off' }}
<span class="dropped-warn" v-if="stats?.dropped &gt; 0"> · {{ stats?.dropped }} dropped</span>
</div>
</div>
</section>
@@ -62,55 +62,55 @@
</span>
</div>
<div class="volume-bars">
<div class="volume-bar" ng-repeat="b in stats.buckets track by $index" title="{{bucketTitle(b)}}">
<span class="seg seg-error" ng-style="{height: barPx(b, 'error') + 'px'}"></span>
<span class="seg seg-warn" ng-style="{height: barPx(b, 'warn') + 'px'}"></span>
<span class="seg seg-info" ng-style="{height: barPx(b, 'info') + 'px'}"></span>
<div class="volume-bar" v-for="(b, index) in stats?.buckets" :key="index" :title="(bucketTitle(b))">
<span class="seg seg-error" :style="{height: barPx(b, &#x27;error&#x27;) + &#x27;px&#x27;}"></span>
<span class="seg seg-warn" :style="{height: barPx(b, &#x27;warn&#x27;) + &#x27;px&#x27;}"></span>
<span class="seg seg-info" :style="{height: barPx(b, &#x27;info&#x27;) + &#x27;px&#x27;}"></span>
</div>
</div>
</section>
<form class="errors-toolbar" aria-label="Error filters">
<form class="errors-toolbar" aria-label="Error filters" v-form="viewState">
<div class="seg-tabs">
<button type="button" ng-class="{active: query.bucket === ''}" ng-click="setBucket('')">All</button>
<button type="button" ng-class="{active: query.bucket === 'error'}" ng-click="setBucket('error')">5xx</button>
<button type="button" ng-class="{active: query.bucket === 'warn'}" ng-click="setBucket('warn')">4xx</button>
<button type="button" ng-class="{active: query.bucket === 'info'}" ng-click="setBucket('info')">Info</button>
<button type="button" :class="{active: query?.bucket === &#x27;&#x27;}" @click="setBucket(&#x27;&#x27;)">All</button>
<button type="button" :class="{active: query?.bucket === &#x27;error&#x27;}" @click="setBucket(&#x27;error&#x27;)">5xx</button>
<button type="button" :class="{active: query?.bucket === &#x27;warn&#x27;}" @click="setBucket(&#x27;warn&#x27;)">4xx</button>
<button type="button" :class="{active: query?.bucket === &#x27;info&#x27;}" @click="setBucket(&#x27;info&#x27;)">Info</button>
</div>
<div class="search-wrap">
<i class="fas fa-search search-icon"></i>
<input type="search" class="form-control" placeholder="code:repo_not_found module:route status:>=400" ng-model="query.search" autocomplete="off" />
<span class="filter-count" ng-if="parsedFilterCount">{{parsedFilterCount}} filter{{parsedFilterCount > 1 ? 's' : ''}}</span>
<input type="search" class="form-control" placeholder="code:repo_not_found module:route status:&gt;=400" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { query.search = value }, value: query?.search, form: null, options: {} }">
<span class="filter-count" v-if="parsedFilterCount">{{ parsedFilterCount }} filter{{ parsedFilterCount > 1 ? 's' : '' }}</span>
</div>
<div class="select-wrap">
<label>Sort</label>
<select class="form-control form-control-sm" ng-model="query.sort">
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.sort = value }, value: query?.sort, form: null, options: {} }">
<option value="recent">Most recent</option>
<option value="count">Most frequent</option>
</select>
</div>
<div class="select-wrap">
<label>Group</label>
<select class="form-control form-control-sm" ng-model="query.group">
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.group = value }, value: query?.group, form: null, options: {} }">
<option value="">Off</option>
<option value="code">By code</option>
<option value="module">By module</option>
</select>
</div>
<label class="autoref">
<input type="checkbox" ng-model="query.autoRefresh" />
<input type="checkbox" v-field="{ state: viewState, set: value =&gt; { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
Auto-refresh
</label>
<button class="btn btn-sm btn-icon" type="button" ng-click="refreshNow()" title="Refresh now"><i class="fas fa-sync"></i></button>
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()"><i class="fas fa-sync"></i></button>
</form>
<div ng-if="!visible.length" class="admin-empty">No errors captured.</div>
<div ng-if="canLoadMore() && visible.length" class="errors-pager">
<span>Showing {{entries.length}} of {{total}} captured</span>
<button class="btn btn-sm" type="button" ng-click="loadMore()">Load older</button>
<div class="admin-empty" v-if="!visible?.length">No errors captured.</div>
<div class="errors-pager" v-if="canLoadMore() &amp;&amp; visible?.length">
<span>Showing {{ entries?.length }} of {{ total }} captured</span>
<button class="btn btn-sm" type="button" @click="loadMore()">Load older</button>
</div>
<div class="errors-list" ng-if="visible.length">
<div class="errors-list" v-if="visible?.length">
<div class="errors-list-head">
<span class="col-when">When</span>
<span class="col-sev">Severity</span>
@@ -120,75 +120,75 @@
<span class="col-status">Status</span>
</div>
<div class="errors-row" ng-repeat="row in visible track by row._key" ng-class="{open: expanded[row._key]}">
<div class="errors-row-main" ng-click="toggle(row)">
<div class="errors-row" v-for="(row, index) in visible" :key="row?._key" :class="{open: expanded[row?._key]}">
<div class="errors-row-main" @click="toggle(row)">
<div class="col-when">
<div class="when-rel">{{relTime(row.ts)}}</div>
<div class="when-abs">{{absTimeShort(row.ts)}}</div>
<div class="when-rel">{{ relTime(row?.ts) }}</div>
<div class="when-abs">{{ absTimeShort(row?.ts) }}</div>
</div>
<div class="col-sev">
<span class="sev-dot" ng-class="'sev-' + row._bucket"></span>
<span class="sev-label">{{row._bucket | uppercase}}</span>
<span class="sev-dot" :class="&#x27;sev-&#x27; + row?._bucket"></span>
<span class="sev-label">{{ fmt?.uppercase(row?._bucket) }}</span>
</div>
<div class="col-mod"><span class="pill pill-module">{{row.module}}</span></div>
<div class="col-mod"><span class="pill pill-module">{{ row?.module }}</span></div>
<div class="col-msg">
<strong class="msg-code">{{row.displayMessage}}</strong>
<span class="msg-context" ng-if="row.displayContext && row.displayContext !== row.displayMessage">{{row.displayContext}}</span>
<span class="msg-detail" ng-if="row._detail">{{row._detail}}</span>
<div class="msg-url" ng-if="row._url">{{row._url}}</div>
<strong class="msg-code">{{ row?.displayMessage }}</strong>
<span class="msg-context" v-if="row?.displayContext &amp;&amp; row?.displayContext !== row?.displayMessage">{{ row?.displayContext }}</span>
<span class="msg-detail" v-if="row?._detail">{{ row?._detail }}</span>
<div class="msg-url" v-if="row?._url">{{ row?._url }}</div>
</div>
<div class="col-count">
<span class="count-pill" ng-if="row.count > 1">×{{row.count}}</span>
<span class="count-pill count-pill-muted" ng-if="row.count === 1">×1</span>
<span class="count-pill" v-if="row?.count &gt; 1">×{{ row?.count }}</span>
<span class="count-pill count-pill-muted" v-if="row?.count === 1">×1</span>
</div>
<div class="col-status">
<span class="status-pill" ng-if="row._status" ng-class="'status-' + row._bucket">{{row._status}}</span>
<span class="status-pill" v-if="row?._status" :class="&#x27;status-&#x27; + row?._bucket">{{ row?._status }}</span>
</div>
</div>
<div class="errors-row-detail" ng-if="expanded[row._key]">
<div class="errors-row-detail" v-if="expanded[row?._key]">
<div class="detail-tabs">
<button type="button" ng-class="{active: detailTab[row._key] === 'raw' || !detailTab[row._key]}" ng-click="detailTab[row._key] = 'raw'">Raw</button>
<button type="button" ng-class="{active: detailTab[row._key] === 'stack'}" ng-click="detailTab[row._key] = 'stack'" ng-if="row._stack">Stack</button>
<button type="button" ng-class="{active: detailTab[row._key] === 'related'}" ng-click="detailTab[row._key] = 'related'" ng-if="row.count > 1">Related ({{row.count}})</button>
<button type="button" :class="{active: detailTab[row?._key] === &#x27;raw&#x27; || !detailTab[row?._key]}" @click="detailTab[row._key] = &#x27;raw&#x27;">Raw</button>
<button type="button" v-if="row?._stack" :class="{active: detailTab[row?._key] === &#x27;stack&#x27;}" @click="detailTab[row._key] = &#x27;stack&#x27;">Stack</button>
<button type="button" v-if="row?.count &gt; 1" :class="{active: detailTab[row?._key] === &#x27;related&#x27;}" @click="detailTab[row._key] = &#x27;related&#x27;">Related ({{ row?.count }})</button>
</div>
<div class="detail-body">
<div class="detail-main">
<pre ng-if="(detailTab[row._key] || 'raw') === 'raw'">{{row._detailJson}}</pre>
<pre ng-if="detailTab[row._key] === 'stack'" class="stack-pre">{{row._stack}}</pre>
<div ng-if="detailTab[row._key] === 'related'" class="related-list">
<div class="related-row" ng-repeat="r in row._related track by $index">
<span class="when-abs">{{absTimeShort(r.ts)}}</span>
<span class="msg-url">{{r._url}}</span>
<span class="status-pill" ng-if="r._status" ng-class="'status-' + r._bucket">{{r._status}}</span>
<pre v-if="(detailTab[row?._key] || &#x27;raw&#x27;) === &#x27;raw&#x27;">{{ row?._detailJson }}</pre>
<pre class="stack-pre" v-if="detailTab[row?._key] === &#x27;stack&#x27;">{{ row?._stack }}</pre>
<div class="related-list" v-if="detailTab[row?._key] === &#x27;related&#x27;">
<div class="related-row" v-for="(r, index) in row?._related" :key="index">
<span class="when-abs">{{ absTimeShort(r?.ts) }}</span>
<span class="msg-url">{{ r?._url }}</span>
<span class="status-pill" v-if="r?._status" :class="&#x27;status-&#x27; + r?._bucket">{{ r?._status }}</span>
</div>
</div>
<div class="detail-actions">
<button class="btn btn-sm" type="button" ng-click="copyCurl(row)" title="Copy a curl that reproduces the request"><i class="fas fa-terminal"></i> Copy curl</button>
<button class="btn btn-sm" type="button" ng-click="copyJson(row)"><i class="fas fa-clipboard"></i> Copy JSON</button>
<span class="copy-hint" ng-if="copyHint">{{copyHint}}</span>
<button class="btn btn-sm" type="button" title="Copy a curl that reproduces the request" @click="copyCurl(row)"><i class="fas fa-terminal"></i> Copy curl</button>
<button class="btn btn-sm" type="button" @click="copyJson(row)"><i class="fas fa-clipboard"></i> Copy JSON</button>
<span class="copy-hint" v-if="copyHint">{{ copyHint }}</span>
</div>
</div>
<aside class="detail-aside">
<div class="aside-block">
<div class="aside-label">First seen</div>
<div class="aside-value" title="{{absTime(row._firstSeen)}}">{{relTime(row._firstSeen)}}</div>
<div class="aside-value" :title="(absTime(row?._firstSeen))">{{ relTime(row?._firstSeen) }}</div>
</div>
<div class="aside-block">
<div class="aside-label">Last seen</div>
<div class="aside-value" title="{{absTime(row.ts)}}">{{relTime(row.ts)}}</div>
<div class="aside-value" :title="(absTime(row?.ts))">{{ relTime(row?.ts) }}</div>
</div>
<div class="aside-block">
<div class="aside-label">Occurrences</div>
<div class="aside-value">{{row.count}}<span class="aside-sub" ng-if="row._lastHourCount"> · {{row._lastHourCount}} this hour</span></div>
<div class="aside-value">{{ row?.count }}<span class="aside-sub" v-if="row?._lastHourCount"> · {{ row?._lastHourCount }} this hour</span></div>
</div>
<div class="aside-block" ng-if="row._repoId">
<div class="aside-block" v-if="row?._repoId">
<div class="aside-label">Repository</div>
<a class="aside-value" ng-href="/r/{{row._repoId}}">{{row._repoId}}</a>
<a class="aside-value" :href="safeUrl(&quot;/r/&quot; + (row?._repoId))">{{ row?._repoId }}</a>
</div>
<div class="aside-block" ng-if="row._url">
<div class="aside-block" v-if="row?._url">
<div class="aside-label">URL</div>
<div class="aside-value mono">{{row._url}}</div>
<div class="aside-value mono">{{ row?._url }}</div>
</div>
</aside>
</div>