mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-15 00:37:33 +01:00
227 lines
17 KiB
HTML
227 lines
17 KiB
HTML
<div class="container paper-page admin-page">
|
|
<div class="paper-crumbs">Admin / <span class="here">Queues</span></div>
|
|
|
|
<div class="q-header">
|
|
<h1 class="paper-page-title">Queues</h1>
|
|
<div class="q-header-actions">
|
|
<button class="btn btn-sm" @click="pauseAll()">Pause all queues</button>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="admin-nav">
|
|
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
|
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
|
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
|
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
|
<a href="/admin/billing"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-card"></use></svg> Billing</a>
|
|
<a href="/admin/queues" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
|
<a href="/admin/emails"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Emails</a>
|
|
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
|
</nav>
|
|
|
|
<!-- Queue overview cards -->
|
|
<div class="q-cards">
|
|
<div class="q-card" role="button" tabindex="0" :aria-pressed="selectedQueue === q.key" @keydown.enter.prevent="selectQueue(q.key)" @keydown.space.prevent="selectQueue(q.key)" v-for="(q, index) in queueList" :class="{selected: selectedQueue == q?.key, paused: q?.paused}" @click="selectQueue(q.key)">
|
|
<div class="q-card-head">
|
|
<span class="q-dot" :title="q?.paused ? 'Paused' : q?.counts?.failed > 0 ? q.counts.failed + ' failed jobs' : (q?.counts?.active || q?.counts?.waiting) ? 'Running' : 'Idle'" :class="{'q-dot-red': q?.paused || q?.counts?.failed > 0, 'q-dot-idle': !q?.counts?.active && !q?.counts?.waiting}"></span>
|
|
<span class="q-card-name" v-text="q?.label"></span>
|
|
</div>
|
|
<div class="q-card-count" v-text="(q?.counts?.waiting || 0) + (q?.counts?.active || 0) + (q?.counts?.delayed || 0)"></div>
|
|
<div class="q-card-sub">
|
|
<span>waiting · {{ q?.counts?.active || 0 }} active<template v-if="q?.counts?.failed"> · <b class="q-card-failed">{{ q.counts.failed }} failed</b></template><template v-if="q?.paused"> · <b class="q-card-failed">paused</b></template></span>
|
|
<div class="q-card-bar" v-if="q?.counts?.active">
|
|
<div class="q-card-bar-fill" :style="{width: (q?.counts?.active / ((q?.counts?.waiting || 0) + (q?.counts?.active || 0) + (q?.counts?.delayed || 0) || 1) * 100) + '%'}"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail: throughput chart + stats panel -->
|
|
<div class="q-detail" v-if="selectedStats">
|
|
<div class="q-throughput">
|
|
<div class="q-range-btns">
|
|
<button class="btn btn-sm" :class="{active: range == '1h'}" @click="setRange('1h')">1h</button>
|
|
<button class="btn btn-sm" :class="{active: range == '6h'}" @click="setRange('6h')">6h</button>
|
|
<button class="btn btn-sm" :class="{active: range == '24h'}" @click="setRange('24h')">24h</button>
|
|
<button class="btn btn-sm" :class="{active: range == '7d'}" @click="setRange('7d')">7d</button>
|
|
</div>
|
|
<div class="q-section-label">Throughput · {{ selectedQueue }} <span class="q-section-right" v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) > 0"><span class="q-legend-completed">●</span> Completed <span class="q-legend-failed">●</span> Failed <template v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) > 0"><span class="q-legend-exec" aria-hidden="true"></span> Avg time · {{ range }}</template></span></div>
|
|
<div class="q-chart-wrap">
|
|
<canvas id="q-throughput-chart" height="180"></canvas>
|
|
<div class="q-chart-empty" v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) === 0"><strong>No jobs processed in this window</strong><span>Completed and failed jobs will chart here.</span></div>
|
|
<div id="q-chart-tooltip" class="q-chart-tooltip" style="display:none;"></div>
|
|
<div id="q-chart-crosshair" class="q-chart-crosshair" style="display:none;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="q-stats-panel">
|
|
<div class="q-section-label">Stats · {{ selectedQueue }}</div>
|
|
<div class="q-stats-grid">
|
|
<div class="q-stat">
|
|
<div class="q-stat-label">Waiting</div>
|
|
<div class="q-stat-value" v-text="selectedStats?.counts?.waiting || 0"></div>
|
|
</div>
|
|
<div class="q-stat">
|
|
<div class="q-stat-label">Active</div>
|
|
<div class="q-stat-value" v-text="selectedStats?.counts?.active || 0"></div>
|
|
</div>
|
|
<div class="q-stat">
|
|
<div class="q-stat-label">Completed (24h)</div>
|
|
<div class="q-stat-value" v-text="fmt?.number(selectedStats?.completed24h)"></div>
|
|
</div>
|
|
<div class="q-stat">
|
|
<div class="q-stat-label">Failed (24h)</div>
|
|
<div class="q-stat-value" v-text="selectedStats?.failed24h || 0"></div>
|
|
</div>
|
|
<div class="q-stat">
|
|
<div class="q-stat-label">Delayed</div>
|
|
<div class="q-stat-value" v-text="selectedStats?.counts?.delayed || 0"></div>
|
|
</div>
|
|
<div class="q-stat">
|
|
<div class="q-stat-label">Workers</div>
|
|
<div class="q-stat-value" v-text="selectedStats?.workers || 0"></div>
|
|
</div>
|
|
</div>
|
|
<div class="q-stats-actions">
|
|
<button class="btn btn-sm" @click="togglePause()">{{ selectedStats?.paused ? 'Resume ' + selectedQueue : 'Pause ' + selectedQueue }}</button>
|
|
<button class="btn btn-sm" @click="retryFailed()" :disabled="!selectedStats?.counts?.failed">Retry failed</button>
|
|
<span class="q-danger-group"><button class="btn btn-sm btn-outline-danger" @click="drainSelected()">Drain</button><button class="btn btn-sm btn-outline-danger" @click="emptyQueue()">Empty</button></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="q-toast-error" v-if="actionError" @click="actionError = null">
|
|
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> {{ actionError }}
|
|
</div>
|
|
|
|
<!-- Jobs table -->
|
|
<div class="q-jobs">
|
|
<div class="q-jobs-header">
|
|
<div class="q-section-label">All jobs · {{ selectedQueue }}</div>
|
|
<div class="seg-filter" role="radiogroup" aria-label="Filter jobs by state">
|
|
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'all' }" :aria-checked="segView() === 'all'" @click="setSegView('all')">All <span class="count">{{ fmt?.number(segCount('all')) }}</span></button>
|
|
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'active', 'is-empty': !segCount('active') }" :aria-checked="segView() === 'active'" @click="setSegView('active')"><span class="seg-dot ok" aria-hidden="true"></span>Active <span class="count">{{ fmt?.number(segCount('active')) }}</span></button>
|
|
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'waiting', 'is-empty': !segCount('waiting') }" :aria-checked="segView() === 'waiting'" @click="setSegView('waiting')"><span class="seg-dot warn" aria-hidden="true"></span>Waiting <span class="count">{{ fmt?.number(segCount('waiting')) }}</span></button>
|
|
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'delayed', 'is-empty': !segCount('delayed') }" :aria-checked="segView() === 'delayed'" @click="setSegView('delayed')"><span class="seg-dot warn" aria-hidden="true"></span>Delayed <span class="count">{{ fmt?.number(segCount('delayed')) }}</span></button>
|
|
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'failed', 'is-empty': !segCount('failed') }" :aria-checked="segView() === 'failed'" @click="setSegView('failed')"><span class="seg-dot bad" aria-hidden="true"></span>Failed <span class="count">{{ fmt?.number(segCount('failed')) }}</span></button>
|
|
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'completed', 'is-empty': !segCount('completed') }" :aria-checked="segView() === 'completed'" @click="setSegView('completed')"><span class="seg-dot done" aria-hidden="true"></span>Completed <span class="count">{{ fmt?.number(segCount('completed')) }}</span></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="q-search-row">
|
|
<input type="search" class="form-control" placeholder="Search by job/repo id…" autocomplete="off" v-field="{ state: viewState, set: value => { query.search = value }, value: query?.search, form: null, options: {} }">
|
|
<label class="q-auto-refresh">
|
|
<input type="checkbox" v-field="{ state: viewState, set: value => { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
|
|
<span class="auto-refresh-label">Auto-refresh</span>
|
|
</label>
|
|
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()" aria-label="Refresh"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
|
|
</div>
|
|
|
|
<table class="q-table" v-if="filteredJobs().length > 0">
|
|
<thead>
|
|
<tr>
|
|
<th>State</th>
|
|
<th>Job</th>
|
|
<th>Detail</th>
|
|
<th>Attempts</th>
|
|
<th class="q-th-num">Duration</th>
|
|
<th>Progress</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody v-for="(job, index) in filteredJobs()">
|
|
<tr style="cursor:pointer;" :class="{'q-row-failed': job?._state == 'failed', 'q-row-expanded': expanded[job?.id]}" @click="toggleJob(job)">
|
|
<td class="q-cell-state">
|
|
<span v-text="job?._state" :class=""q-state-badge q-state-" + (job?._state)"></span>
|
|
<span class="q-delay-hint" v-if="job?._state == 'delayed' && job?.delayUntil" v-text="delayCountdown(job?.delayUntil)"></span>
|
|
</td>
|
|
<td class="q-cell-id" :title="'job:' + job?.id + (job?.name && job?.name !== job?.id ? ' · ' + job.name : '') + (job?.data?.repoId && job?.data?.repoId !== job?.id ? ' · ' + job.data.repoId : '')">
|
|
<svg class="ui-icon q-chevron" :class="{'q-chevron-open': expanded[job?.id]}" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg>
|
|
<a target="_blank" class="q-job-link" v-text="job?.data?.repoId || job?.id" @click="$event.stopPropagation()" :href="safeUrl("/r/" + (job?.data?.repoId || job?.id))"></a>
|
|
</td>
|
|
<td class="q-cell-detail" :class="{'is-error': job?._state == 'failed'}" :title="job?._state == 'failed' ? job?.failedReason : ''" v-text="jobDetail(job)"></td>
|
|
<td class="q-cell-num" v-text="job?.attemptsMade || 1"></td>
|
|
<td class="q-cell-num q-cell-duration" v-text="jobDuration(job)"></td>
|
|
<td class="q-cell-progress">
|
|
<div class="q-progress-wrap" :class="{'is-indeterminate': jobProgressPct(job) === null}" :aria-label="jobProgressPct(job) === null ? 'Progress not reported' : jobProgressPct(job) + '% done'">
|
|
<div class="q-progress-bar" :style="{'--pct': (jobProgressPct(job) || 0) + '%'}"></div>
|
|
<span class="q-progress-label" v-text="jobProgressPct(job) === null ? '—' : jobProgressPct(job) + '%'"></span>
|
|
</div>
|
|
</td>
|
|
<td class="q-cell-actions">
|
|
<button class="btn btn-sm btn-icon" title="Retry" v-if="job?._state == 'failed'" @click="retryJob(job); $event.stopPropagation()" aria-label="Retry job"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
|
|
<button class="btn btn-sm btn-icon" title="Remove" aria-label="Remove job" @click="removeJob(job); $event.stopPropagation()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg></button>
|
|
</td>
|
|
</tr>
|
|
<tr class="q-detail-row" v-if="expanded[job?.id]">
|
|
<td colspan="7">
|
|
<div class="q-job-detail">
|
|
<div class="q-job-detail-grid">
|
|
<div class="q-job-detail-item">
|
|
<span class="q-job-detail-label">Job id</span>
|
|
<span class="q-job-detail-value"><a target="_blank" v-text="job?.id" :href="safeUrl("/r/" + (job?.id))"></a></span>
|
|
</div>
|
|
<div class="q-job-detail-item">
|
|
<span class="q-job-detail-label">State</span>
|
|
<span class="q-job-detail-value"><span v-text="job?._state" :class=""q-state-badge q-state-" + (job?._state)"></span></span>
|
|
</div>
|
|
<div class="q-job-detail-item" v-if="job?.data?.repoId">
|
|
<span class="q-job-detail-label">Repo ID</span>
|
|
<span class="q-job-detail-value" v-text="job?.data?.repoId"></span>
|
|
</div>
|
|
<div class="q-job-detail-item" v-if="job?.timestamp">
|
|
<span class="q-job-detail-label">Created</span>
|
|
<span class="q-job-detail-value" v-text="humanTime(job?.timestamp)"></span>
|
|
</div>
|
|
<div class="q-job-detail-item" v-if="job?._state == 'delayed' && job?.delayUntil">
|
|
<span class="q-job-detail-label">Retry at</span>
|
|
<span class="q-job-detail-value">{{ humanTime(job?.delayUntil) }} ({{ delayCountdown(job?.delayUntil) }})</span>
|
|
</div>
|
|
<div class="q-job-detail-item" v-if="job?.processedOn">
|
|
<span class="q-job-detail-label">Processed</span>
|
|
<span class="q-job-detail-value" v-text="humanTime(job?.processedOn)"></span>
|
|
</div>
|
|
<div class="q-job-detail-item" v-if="job?.finishedOn">
|
|
<span class="q-job-detail-label">Finished</span>
|
|
<span class="q-job-detail-value" v-text="humanTime(job?.finishedOn)"></span>
|
|
</div>
|
|
<div class="q-job-detail-item" v-if="job?.attemptsMade">
|
|
<span class="q-job-detail-label">Attempts</span>
|
|
<span class="q-job-detail-value" v-text="job?.attemptsMade"></span>
|
|
</div>
|
|
<div class="q-job-detail-item" v-if="job?.progress && job?.progress?.status">
|
|
<span class="q-job-detail-label">Status</span>
|
|
<span class="q-job-detail-value" v-text="job?.progress?.status"></span>
|
|
</div>
|
|
<div class="q-job-detail-item" v-if="jobProgressPct(job) !== null">
|
|
<span class="q-job-detail-label">Progress</span>
|
|
<span class="q-job-detail-value" v-text="jobProgressPct(job) + '%'"></span>
|
|
</div>
|
|
</div>
|
|
<div class="q-job-detail-error" v-if="job?.failedReason">
|
|
<span class="q-job-detail-label">Error</span>
|
|
<div class="q-error-reason" v-text="job?.failedReason"></div>
|
|
</div>
|
|
<div v-if="job?.stacktrace?.length">
|
|
<span class="q-job-detail-label">Stacktrace</span>
|
|
<pre class="q-error-stack" v-for="(stack, index) in job?.stacktrace" :key="index"><code v-text="stack"></code></pre>
|
|
</div>
|
|
<div class="q-job-detail-actions">
|
|
<button class="btn btn-sm" v-if="job?._state == 'failed'" @click="retryJob(job)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Retry</button>
|
|
<button class="btn btn-sm" @click="removeJob(job)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</button>
|
|
<a class="btn btn-sm" target="_blank" :href="safeUrl("/r/" + (job?.id))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg> View repo</a>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="paper-table-empty" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);" v-if="filteredJobs().length == 0">
|
|
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg>
|
|
<span v-if="!query?.search">No jobs in the {{ selectedQueue }} queue.</span>
|
|
<span v-if="query?.search">No jobs match the current filters.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|