mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-05-15 14:38:03 +02:00
multiple fixes
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<div class="container paper-page">
|
||||
<div class="container paper-page admin-page">
|
||||
<div class="paper-crumbs">Admin / <span class="here">Conferences</span></div>
|
||||
<h1 class="paper-page-title">All <em>conferences</em></h1>
|
||||
<p class="paper-page-lede">Every venue configured on the platform.</p>
|
||||
<h1 class="paper-page-title">Conferences</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
@@ -10,91 +9,51 @@
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-stats">
|
||||
<div class="admin-stat-card">
|
||||
<div class="stat-value" ng-bind="total >= 0 ? (total | number) : '...'"></div>
|
||||
<div class="stat-label">Total conferences</div>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="stat-value">{{query.page}}/{{totalPage || '...'}}</div>
|
||||
<div class="stat-label">Current page</div>
|
||||
</div>
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{total >= 0 ? (total | number) : '…'}}</span>
|
||||
<span class="summary-pill ok" ng-class="{active: query.status == 'ready'}" ng-click="query.status = query.status == 'ready' ? '' : 'ready'; query.page = 1">Ready <span class="count">{{statusCountFor('ready') | number}}</span></span>
|
||||
<span class="summary-pill warn" ng-class="{active: query.status == 'preparing'}" ng-click="query.status = query.status == 'preparing' ? '' : 'preparing'; query.page = 1">Preparing <span class="count">{{statusCountFor('preparing') | number}}</span></span>
|
||||
<span class="summary-pill error" ng-class="{active: query.status == 'error'}" ng-click="query.status = query.status == 'error' ? '' : 'error'; query.page = 1">Errored <span class="count">{{statusCountFor('error') | number}}</span></span>
|
||||
<span class="summary-pill" ng-class="{active: query.status == 'expired'}" ng-click="query.status = query.status == 'expired' ? '' : 'expired'; query.page = 1">Expired <span class="count">{{statusCountFor('expired') | number}}</span></span>
|
||||
<span class="summary-pill" ng-class="{active: query.status == 'removed'}" ng-click="query.status = query.status == 'removed' ? '' : 'removed'; query.page = 1">Removed <span class="count">{{statusCountFor('removed') | number}}</span></span>
|
||||
</div>
|
||||
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Conferences" accept-charset="UTF-8">
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
type="search"
|
||||
class="form-control"
|
||||
aria-label="Search conferences"
|
||||
placeholder="Search conferences…"
|
||||
autocomplete="off"
|
||||
ng-model="query.search"
|
||||
/>
|
||||
<div class="alert alert-danger" ng-if="fetchError" style="margin: 8px 0;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{fetchError}}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Conferences" accept-charset="UTF-8">
|
||||
<div class="admin-filter-row">
|
||||
<div class="search-wrap">
|
||||
<input type="search" class="form-control" placeholder="Search conferences…" autocomplete="off" ng-model="query.search" />
|
||||
<span class="admin-search-hint" ng-if="!query.search">/</span>
|
||||
</div>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" ng-click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<span class="admin-filter-inline" aria-label="Pagination">
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i></button>
|
||||
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{query.page}}/{{totalPage || 1}}</span>
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap" style="gap: 8px; align-items: center;">
|
||||
<div class="pagination-compact">
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="query.page" min="1" max="{{totalPage}}" />
|
||||
<span>/{{totalPage}}</span>
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown">Sort</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownSort">
|
||||
<h6 class="dropdown-header">Sort by</h6>
|
||||
<a class="dropdown-item" href="#" ng-click="query.sort = 'source.conferenceName'">
|
||||
<i class="fas fa-check" ng-show="query.sort == 'source.conferenceName'"></i> Conference
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" ng-click="query.sort = 'anonymizeDate'">
|
||||
<i class="fas fa-check" ng-show="query.sort == 'anonymizeDate'"></i> Anonymize date
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" ng-click="query.sort = 'status'">
|
||||
<i class="fas fa-check" ng-show="query.sort == 'status'"></i> Status
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown">Status</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
|
||||
<h6 class="dropdown-header">Filter by status</h6>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminConfStatusReady" ng-model="query.ready" />
|
||||
<label class="form-check-label" for="adminConfStatusReady">Ready</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminConfStatusPreparing" ng-model="query.preparing" />
|
||||
<label class="form-check-label" for="adminConfStatusPreparing">Preparing</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminConfStatusExpired" ng-model="query.expired" />
|
||||
<label class="form-check-label" for="adminConfStatusExpired">Expired</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminConfStatusRemoved" ng-model="query.removed" />
|
||||
<label class="form-check-label" for="adminConfStatusRemoved">Removed</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminConfStatusError" ng-model="query.error" />
|
||||
<label class="form-check-label" for="adminConfStatusError">Error</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-filter-row" ng-if="chips.length">
|
||||
<div class="admin-active-chips">
|
||||
<span class="admin-active-chip" ng-repeat="chip in chips track by chip.key">
|
||||
<span class="key">{{chip.label}}</span>
|
||||
<span>{{chip.value}}</span>
|
||||
<button type="button" ng-click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="paper-table paper-table-conferences w-100" role="table" aria-label="Conferences">
|
||||
<div class="paper-table-head" role="row">
|
||||
<div role="columnheader">Conference</div>
|
||||
<div role="columnheader">Status</div>
|
||||
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'name'}" ng-click="sortBy('name')">Conference <i class="fas" ng-class="sortIcon('name')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'status'}" ng-click="sortBy('status')">Status <i class="fas" ng-class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader" class="num">Repos</div>
|
||||
<div role="columnheader">Window</div>
|
||||
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'startDate'}" ng-click="sortBy('startDate')">Window <i class="fas" ng-class="sortIcon('startDate')"></i></span></div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div
|
||||
@@ -112,10 +71,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<span class="status-dot" ng-class="{'status-removed': conference.status == 'removed' || conference.status == 'expired', 'status-ready': conference.status == 'ready'}"></span>
|
||||
<span class="status-dot" ng-class="{'status-removed': conference.status == 'removed' || conference.status == 'expired', 'status-ready': conference.status == 'ready', 'status-error': conference.status == 'error', 'status-preparing': conference.status == 'preparing'}"></span>
|
||||
<span ng-bind="conference.status | title"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" ng-bind="::conference.repositories.length || 0 | number"></div>
|
||||
<div class="cell-views num" role="cell">
|
||||
<a ng-href="/admin/?conference={{conference.conferenceID}}" ng-bind="::conference.repositories.length || 0 | number" title="Show repositories in this conference"></a>
|
||||
</div>
|
||||
<div class="cell-expires" role="cell">{{conference.startDate | date}} – {{conference.endDate | date}}</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
@@ -125,6 +86,7 @@
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="/conference/{{conference.conferenceID}}/edit"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" href="/conference/{{conference.conferenceID}}/"><i class="fa fa-eye"></i> View</a>
|
||||
<a class="dropdown-item" href="/admin/?conference={{conference.conferenceID}}"><i class="fas fa-code-branch"></i> View repositories</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#" ng-show="conference.status != 'removed'" ng-click="removeConference(conference)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
</div>
|
||||
@@ -137,15 +99,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-toolbar" ng-if="totalPage > 1" style="justify-content: center; border-bottom: none;">
|
||||
<div class="pagination-compact">
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1">
|
||||
<i class="fas fa-chevron-left"></i> Previous
|
||||
</button>
|
||||
<span>Page {{query.page}} of {{totalPage}}</span>
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">
|
||||
Next <i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{total | number}} results</span>
|
||||
<div class="pagination-compact" ng-if="totalPage > 1">
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="query.page" min="1" max="{{totalPage}}" style="width: 56px;" />
|
||||
<span>of {{totalPage}}</span>
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">Next <i class="fas fa-chevron-right"></i></button>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
<label>Per page</label>
|
||||
<select class="form-control form-control-sm" ng-model="query.limit"><option value="10">10</option><option value="25">25</option><option value="50">50</option><option value="100">100</option></select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<div class="container paper-page">
|
||||
<div class="container paper-page admin-page">
|
||||
<div class="paper-crumbs">Admin / <span class="here">Queues</span></div>
|
||||
<h1 class="paper-page-title">Background <em>queues</em></h1>
|
||||
<p class="paper-page-lede">Watch anonymization jobs as they move through the workers.</p>
|
||||
<h1 class="paper-page-title">Queues</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
@@ -10,117 +9,93 @@
|
||||
<a href="/admin/queues" class="active"><i class="fas fa-tasks"></i> Queues</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-stats">
|
||||
<div class="admin-stat-card">
|
||||
<div class="stat-value">{{downloadJobs.length || 0}}</div>
|
||||
<div class="stat-label">Download jobs</div>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="stat-value">{{removeJobs.length || 0}}</div>
|
||||
<div class="stat-label">Remove jobs</div>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="stat-value">{{removeCaches.length || 0}}</div>
|
||||
<div class="stat-label">Cache jobs</div>
|
||||
</div>
|
||||
<div class="admin-summary">
|
||||
<span class="summary-pill warn" ng-class="{active: query.state == 'active'}" ng-click="query.state = query.state == 'active' ? '' : 'active'">Active <span class="count">{{(counts.download.active || 0) + (counts.remove.active || 0) + (counts.cache.active || 0)}}</span></span>
|
||||
<span class="summary-pill" ng-class="{active: query.state == 'waiting'}" ng-click="query.state = query.state == 'waiting' ? '' : 'waiting'">Waiting <span class="count">{{(counts.download.waiting || 0) + (counts.remove.waiting || 0) + (counts.cache.waiting || 0)}}</span></span>
|
||||
<span class="summary-pill error" ng-class="{active: query.state == 'failed'}" ng-click="query.state = query.state == 'failed' ? '' : 'failed'">Failed <span class="count">{{(counts.download.failed || 0) + (counts.remove.failed || 0) + (counts.cache.failed || 0)}}</span></span>
|
||||
<span class="summary-pill ok" ng-class="{active: query.state == 'completed'}" ng-click="query.state = query.state == 'completed' ? '' : 'completed'">Completed <span class="count">{{(counts.download.completed || 0) + (counts.remove.completed || 0) + (counts.cache.completed || 0)}}</span></span>
|
||||
<span class="summary-pill" ng-class="{active: query.state == 'delayed'}" ng-click="query.state = query.state == 'delayed' ? '' : 'delayed'">Delayed <span class="count">{{(counts.download.delayed || 0) + (counts.remove.delayed || 0) + (counts.cache.delayed || 0)}}</span></span>
|
||||
</div>
|
||||
|
||||
<div class="admin-section-header">
|
||||
<h2><i class="fas fa-download"></i> Download jobs</h2>
|
||||
<span class="section-count">{{downloadJobs.length || 0}}</span>
|
||||
</div>
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Queue filters">
|
||||
<div class="admin-filter-row">
|
||||
<div class="search-wrap">
|
||||
<input type="search" class="form-control" placeholder="Search by job/repo id…" ng-model="query.search" autocomplete="off" />
|
||||
<span class="admin-search-hint" ng-if="!query.search">/</span>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
<label>State</label>
|
||||
<select class="form-control form-control-sm" ng-model="query.state">
|
||||
<option value="">Any</option>
|
||||
<option value="waiting">Waiting</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="delayed">Delayed</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<label class="admin-filter-inline" style="cursor:pointer;">
|
||||
<input type="checkbox" ng-model="query.autoRefresh" />
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button class="btn btn-sm" type="button" ng-click="refreshNow()" title="Refresh now"><i class="fas fa-sync"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="queue-job-card" ng-repeat="job in downloadJobs as filteredDownloadJobs">
|
||||
<div class="job-header">
|
||||
<div class="job-id">
|
||||
<span class="status-dot" ng-class="{'status-ready': job.progress.status == 'ready', 'status-error': job.progress.status == 'error', 'status-preparing': job.progress.status == 'preparing', 'status-removed': job.progress.status == 'removed'}"></span>
|
||||
<a target="_blank" ng-href="/r/{{job.id}}" ng-bind="job.id"></a>
|
||||
<span ng-bind="job.progress.status | title" style="font-family: var(--font-sans); color: var(--ink-muted); font-size: 12px;"></span>
|
||||
<div ng-repeat="qInfo in [
|
||||
{key: 'download', label: 'Download jobs', icon: 'fa-download', jobs: downloadJobs, counts: counts.download},
|
||||
{key: 'remove', label: 'Remove jobs', icon: 'fa-trash', jobs: removeJobs, counts: counts.remove},
|
||||
{key: 'cache', label: 'Cache cleanup jobs', icon: 'fa-broom', jobs: removeCaches, counts: counts.cache}
|
||||
]">
|
||||
<div class="admin-section-header">
|
||||
<h2><i class="fas {{qInfo.icon}}"></i> {{qInfo.label}}</h2>
|
||||
<span class="section-count">{{qInfo.jobs.length || 0}}</span>
|
||||
<span class="queue-state-pills">
|
||||
<span class="pill pill-waiting" ng-if="qInfo.counts.waiting">{{qInfo.counts.waiting}} waiting</span>
|
||||
<span class="pill pill-active" ng-if="qInfo.counts.active">{{qInfo.counts.active}} active</span>
|
||||
<span class="pill pill-completed" ng-if="qInfo.counts.completed">{{qInfo.counts.completed}} done</span>
|
||||
<span class="pill pill-failed" ng-if="qInfo.counts.failed">{{qInfo.counts.failed}} failed</span>
|
||||
<span class="pill pill-delayed" ng-if="qInfo.counts.delayed">{{qInfo.counts.delayed}} delayed</span>
|
||||
</span>
|
||||
<span style="margin-left: auto; display: inline-flex; gap: 6px;">
|
||||
<button class="btn btn-sm" type="button" ng-click="bulkRetryFailed(qInfo.key)" ng-disabled="!qInfo.counts.failed"><i class="fas fa-redo"></i> Retry all failed</button>
|
||||
<button class="btn btn-sm text-danger" type="button" ng-click="bulkDrain(qInfo.key)"><i class="fas fa-eraser"></i> Drain</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="queue-job-card" ng-repeat="job in qInfo.jobs | filter:jobMatchesState as filteredJobs">
|
||||
<div class="job-header">
|
||||
<div class="job-id">
|
||||
<span class="status-dot" ng-class="{'status-ready': job.progress.status == 'ready', 'status-error': job.progress.status == 'error' || (job.stacktrace && job.stacktrace.length), 'status-preparing': job.progress.status == 'preparing' || (job.processedOn && !job.finishedOn), 'status-removed': job.progress.status == 'removed'}"></span>
|
||||
<a target="_blank" ng-href="/r/{{job.id}}" ng-bind="job.id"></a>
|
||||
<span ng-bind="job.progress.status | title" style="font-family: var(--font-sans); color: var(--ink-muted); font-size: 12px;"></span>
|
||||
</div>
|
||||
<div ng-if="jobProgressPct(job) !== null" class="job-progress" title="{{jobProgressPct(job)}}%">
|
||||
<div class="job-progress-bar" style="width: {{jobProgressPct(job)}}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-timestamps">
|
||||
<span ng-if="job.timestamp"><i class="fas fa-clock"></i> Created: {{job.timestamp | humanTime}}</span>
|
||||
<span ng-if="job.processedOn"><i class="fas fa-cog"></i> Processed: {{job.processedOn | humanTime}}</span>
|
||||
<span ng-if="job.finishedOn"><i class="fas fa-check"></i> Finished: {{job.finishedOn | humanTime}}</span>
|
||||
<span ng-if="job.attemptsMade"><i class="fas fa-redo"></i> Attempts: {{job.attemptsMade}}</span>
|
||||
</div>
|
||||
<div ng-if="job.failedReason" style="color:#B42318; font-size:0.8rem; margin-top:4px;">{{job.failedReason}}</div>
|
||||
<div ng-if="job.stacktrace.length">
|
||||
<pre ng-repeat="stack in job.stacktrace track by $index" style="font-size: 0.8rem; max-height: 100px; overflow: auto; margin: 6px 0 0 0"><code ng-bind="stack"></code></pre>
|
||||
</div>
|
||||
<div class="job-actions">
|
||||
<button class="btn btn-sm" ng-click="retryJob(qInfo.key, job)"><i class="fas fa-sync"></i> Retry</button>
|
||||
<button class="btn btn-sm" ng-click="removeJob(qInfo.key, job)"><i class="fas fa-trash-alt"></i> Remove</button>
|
||||
<a class="btn btn-sm" href="/anonymize/{{job.id}}"><i class="far fa-edit"></i> Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-timestamps">
|
||||
<span ng-if="job.timestamp"><i class="fas fa-clock"></i> Created: {{job.timestamp | humanTime}}</span>
|
||||
<span ng-if="job.processedOn"><i class="fas fa-cog"></i> Processed: {{job.processedOn | humanTime}}</span>
|
||||
<span ng-if="job.finishedOn"><i class="fas fa-check"></i> Finished: {{job.finishedOn | humanTime}}</span>
|
||||
</div>
|
||||
<div ng-if="job.stacktrace.length">
|
||||
<pre ng-repeat="stack in job.stacktrace track by $index" style="font-size: 0.8rem; max-height: 100px; overflow: auto; margin: 6px 0 0 0"><code ng-bind="stack"></code></pre>
|
||||
</div>
|
||||
<div class="job-actions">
|
||||
<button class="btn btn-sm" ng-click="retryJob('download', job)"><i class="fas fa-sync"></i> Retry</button>
|
||||
<button class="btn btn-sm" ng-click="removeJob('download', job)"><i class="fas fa-trash-alt"></i> Remove</button>
|
||||
<a class="btn btn-sm" href="/anonymize/{{job.id}}"><i class="far fa-edit"></i> Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paper-table-empty" ng-if="filteredDownloadJobs.length == 0" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>No download jobs in the queue.</span>
|
||||
</div>
|
||||
|
||||
<div class="admin-section-header">
|
||||
<h2><i class="fas fa-trash"></i> Remove jobs</h2>
|
||||
<span class="section-count">{{removeJobs.length || 0}}</span>
|
||||
</div>
|
||||
|
||||
<div class="queue-job-card" ng-repeat="job in removeJobs as filteredRemoveJobs">
|
||||
<div class="job-header">
|
||||
<div class="job-id">
|
||||
<span class="status-dot" ng-class="{'status-ready': job.progress.status == 'ready', 'status-error': job.progress.status == 'error', 'status-preparing': job.progress.status == 'preparing', 'status-removed': job.progress.status == 'removed'}"></span>
|
||||
<a target="_blank" ng-href="/r/{{job.id}}" ng-bind="job.id"></a>
|
||||
<span ng-bind="job.progress.status | title" style="font-family: var(--font-sans); color: var(--ink-muted); font-size: 12px;"></span>
|
||||
</div>
|
||||
<div class="paper-table-empty" ng-if="filteredJobs.length == 0" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span ng-if="!query.search && !query.state">No {{qInfo.label | lowercase}} in the queue.</span>
|
||||
<span ng-if="query.search || query.state">No jobs match the current filters.</span>
|
||||
</div>
|
||||
<div class="job-timestamps">
|
||||
<span ng-if="job.timestamp"><i class="fas fa-clock"></i> Created: {{job.timestamp | humanTime}}</span>
|
||||
<span ng-if="job.processedOn"><i class="fas fa-cog"></i> Processed: {{job.processedOn | humanTime}}</span>
|
||||
<span ng-if="job.finishedOn"><i class="fas fa-check"></i> Finished: {{job.finishedOn | humanTime}}</span>
|
||||
</div>
|
||||
<div ng-if="job.stacktrace.length">
|
||||
<pre ng-repeat="stack in job.stacktrace track by $index" style="font-size: 0.8rem; max-height: 100px; overflow: auto; margin: 6px 0 0 0"><code ng-bind="stack"></code></pre>
|
||||
</div>
|
||||
<div class="job-actions">
|
||||
<button class="btn btn-sm" ng-click="retryJob('remove', job)"><i class="fas fa-sync"></i> Retry</button>
|
||||
<button class="btn btn-sm" ng-click="removeJob('remove', job)"><i class="fas fa-trash-alt"></i> Remove</button>
|
||||
<a class="btn btn-sm" href="/anonymize/{{job.id}}"><i class="far fa-edit"></i> Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paper-table-empty" ng-if="filteredRemoveJobs.length == 0" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>No remove jobs in the queue.</span>
|
||||
</div>
|
||||
|
||||
<div class="admin-section-header">
|
||||
<h2><i class="fas fa-broom"></i> Cache cleanup jobs</h2>
|
||||
<span class="section-count">{{removeCaches.length || 0}}</span>
|
||||
</div>
|
||||
|
||||
<div class="queue-job-card" ng-repeat="job in removeCaches as filteredRemoveCache">
|
||||
<div class="job-header">
|
||||
<div class="job-id">
|
||||
<span class="status-dot" ng-class="{'status-ready': job.progress.status == 'ready', 'status-error': job.progress.status == 'error', 'status-preparing': job.progress.status == 'preparing', 'status-removed': job.progress.status == 'removed'}"></span>
|
||||
<a target="_blank" ng-href="/r/{{job.id}}" ng-bind="job.id"></a>
|
||||
<span ng-bind="job.progress.status | title" style="font-family: var(--font-sans); color: var(--ink-muted); font-size: 12px;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-timestamps">
|
||||
<span ng-if="job.timestamp"><i class="fas fa-clock"></i> Created: {{job.timestamp | humanTime}}</span>
|
||||
<span ng-if="job.processedOn"><i class="fas fa-cog"></i> Processed: {{job.processedOn | humanTime}}</span>
|
||||
<span ng-if="job.finishedOn"><i class="fas fa-check"></i> Finished: {{job.finishedOn | humanTime}}</span>
|
||||
</div>
|
||||
<div ng-if="job.stacktrace.length">
|
||||
<pre ng-repeat="stack in job.stacktrace track by $index" style="font-size: 0.8rem; max-height: 100px; overflow: auto; margin: 6px 0 0 0"><code ng-bind="stack"></code></pre>
|
||||
</div>
|
||||
<div class="job-actions">
|
||||
<button class="btn btn-sm" ng-click="retryJob('cache', job)"><i class="fas fa-sync"></i> Retry</button>
|
||||
<button class="btn btn-sm" ng-click="removeJob('cache', job)"><i class="fas fa-trash-alt"></i> Remove</button>
|
||||
<a class="btn btn-sm" href="/anonymize/{{job.id}}"><i class="far fa-edit"></i> Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paper-table-empty" ng-if="filteredRemoveCache.length == 0" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>No cache cleanup jobs in the queue.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<div class="container paper-page">
|
||||
<div class="container paper-page admin-page">
|
||||
<div class="paper-crumbs">Admin / <span class="here">Repositories</span></div>
|
||||
<h1 class="paper-page-title">Anonymized <em>repositories</em></h1>
|
||||
<p class="paper-page-lede">Every anonymization across every user and conference.</p>
|
||||
<h1 class="paper-page-title">Repositories</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/" class="active"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
@@ -10,111 +9,84 @@
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-stats">
|
||||
<div class="admin-stat-card">
|
||||
<div class="stat-value" ng-bind="total >= 0 ? (total | number) : '...'"></div>
|
||||
<div class="stat-label">Total repos</div>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="stat-value">{{query.page}}/{{totalPage || '...'}}</div>
|
||||
<div class="stat-label">Current page</div>
|
||||
</div>
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{total >= 0 ? (total | number) : '…'}}</span>
|
||||
<span class="summary-meta">{{totalSize | humanFileSize}} on disk</span>
|
||||
<span class="summary-pill ok" ng-class="{active: query.ready}" ng-click="query.ready = !query.ready; query.page = 1" title="Toggle ready filter">Ready <span class="count">{{statusCountFor('ready') | number}}</span></span>
|
||||
<span class="summary-pill warn" ng-class="{active: query.preparing}" ng-click="query.preparing = !query.preparing; query.page = 1" title="Toggle preparing filter">Preparing <span class="count">{{statusCountFor('preparing') + statusCountFor('download') | number}}</span></span>
|
||||
<span class="summary-pill error" ng-class="{active: query.error}" ng-click="query.error = !query.error; query.page = 1" title="Toggle errored filter">Errored <span class="count">{{statusCountFor('error') | number}}</span></span>
|
||||
<span class="summary-pill" ng-class="{active: query.expired}" ng-click="query.expired = !query.expired; query.page = 1" title="Toggle expired filter">Expired <span class="count">{{statusCountFor('expired') + statusCountFor('expiring') | number}}</span></span>
|
||||
<span class="summary-pill" ng-class="{active: query.removed}" ng-click="query.removed = !query.removed; query.page = 1" title="Toggle removed filter">Removed <span class="count">{{statusCountFor('removed') + statusCountFor('removing') | number}}</span></span>
|
||||
</div>
|
||||
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Repositories" accept-charset="UTF-8">
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
type="search"
|
||||
class="form-control"
|
||||
aria-label="Search repositories"
|
||||
placeholder="Search repositories…"
|
||||
autocomplete="off"
|
||||
ng-model="query.search"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap" style="gap: 8px; align-items: center;">
|
||||
<div class="pagination-compact">
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<div class="alert alert-danger" ng-if="fetchError" style="margin: 8px 0;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{fetchError}}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8">
|
||||
<!-- Row 1: search + scoped inputs + headline actions -->
|
||||
<div class="admin-filter-row">
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
type="number"
|
||||
class="form-control form-control-sm"
|
||||
ng-model="query.page"
|
||||
min="1"
|
||||
max="{{totalPage}}"
|
||||
type="search"
|
||||
class="form-control"
|
||||
aria-label="Search repositories"
|
||||
placeholder="Search repoId, source repo, error message…"
|
||||
autocomplete="off"
|
||||
ng-model="query.search"
|
||||
/>
|
||||
<span>/{{totalPage}}</span>
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
<span class="admin-search-hint" ng-if="!query.search">/</span>
|
||||
</div>
|
||||
<span class="admin-filter-inline"><label>Owner</label><input type="text" class="form-control form-control-sm" placeholder="username" ng-model="query.owner" /></span>
|
||||
<span class="admin-filter-inline"><label>Conference</label><input type="text" class="form-control form-control-sm" placeholder="ID" ng-model="query.conference" /></span>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" ng-click="exportCsv()" title="Export current view to CSV"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<span class="admin-filter-inline" aria-label="Pagination">
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i></button>
|
||||
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{query.page}}/{{totalPage || 1}}</span>
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Sort</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownSort">
|
||||
<h6 class="dropdown-header">Sort by</h6>
|
||||
<a class="dropdown-item" href="#" ng-click="query.sort = 'source.repositoryName'">
|
||||
<i class="fas fa-check" ng-show="query.sort == 'source.repositoryName'"></i> Repository
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" ng-click="query.sort = 'anonymizeDate'">
|
||||
<i class="fas fa-check" ng-show="query.sort == 'anonymizeDate'"></i> Anonymize date
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" ng-click="query.sort = 'status'">
|
||||
<i class="fas fa-check" ng-show="query.sort == 'status'"></i> Status
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" ng-click="query.sort = 'lastView'">
|
||||
<i class="fas fa-check" ng-show="query.sort == 'lastView'"></i> Last view
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" ng-click="query.sort = 'pageView'">
|
||||
<i class="fas fa-check" ng-show="query.sort == 'pageView'"></i> Page views
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Status</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
|
||||
<h6 class="dropdown-header">Filter by status</h6>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminStatusReady" ng-model="query.ready" />
|
||||
<label class="form-check-label" for="adminStatusReady">Ready</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminStatusPreparing" ng-model="query.preparing" />
|
||||
<label class="form-check-label" for="adminStatusPreparing">Preparing</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminStatusExpired" ng-model="query.expired" />
|
||||
<label class="form-check-label" for="adminStatusExpired">Expired</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminStatusRemoved" ng-model="query.removed" />
|
||||
<label class="form-check-label" for="adminStatusRemoved">Removed</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" id="adminStatusError" ng-model="query.error" />
|
||||
<label class="form-check-label" for="adminStatusError">Error</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 2: appears only when there are active filter chips -->
|
||||
<div class="admin-filter-row" ng-if="chips.length">
|
||||
<div class="admin-active-chips">
|
||||
<span class="admin-active-chip" ng-repeat="chip in chips track by chip.key">
|
||||
<span class="key">{{chip.label}}</span>
|
||||
<span>{{chip.value}}</span>
|
||||
<button type="button" ng-click="clearFilter(chip.key)" aria-label="Remove filter"><i class="fas fa-times"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="paper-table paper-table-repos w-100" role="table" aria-label="Repositories">
|
||||
<div class="bulk-bar" ng-if="selectedCount() > 0">
|
||||
<span><strong>{{selectedCount()}}</strong> selected</span>
|
||||
<button class="btn btn-sm" type="button" ng-click="bulkRefresh()"><i class="fas fa-sync"></i> Force refresh</button>
|
||||
<button class="btn btn-sm text-danger" type="button" ng-click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" ng-click="clearSelection()">Clear</button>
|
||||
</div>
|
||||
|
||||
<div class="paper-table paper-table-repos has-bulk w-100" role="table" aria-label="Repositories">
|
||||
<div class="paper-table-head" role="row">
|
||||
<div role="columnheader">Repository</div>
|
||||
<div role="columnheader">Status</div>
|
||||
<div role="columnheader" class="num">Views</div>
|
||||
<div role="columnheader">Anonymized</div>
|
||||
<div role="columnheader" style="width: 28px;">
|
||||
<input type="checkbox" ng-click="selectAllOnPage()" ng-checked="allSelected" aria-label="Select all on page" />
|
||||
</div>
|
||||
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'source.repositoryName'}" ng-click="sortBy('source.repositoryName')">Repository <i class="fas" ng-class="sortIcon('source.repositoryName')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'status'}" ng-click="sortBy('status')">Status <i class="fas" ng-class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader" class="num"><span class="sortable" ng-class="{active: query.sort == 'pageView'}" ng-click="sortBy('pageView')">Views <i class="fas" ng-class="sortIcon('pageView')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'anonymizeDate'}" ng-click="sortBy('anonymizeDate')">Anonymized <i class="fas" ng-class="sortIcon('anonymizeDate')"></i></span></div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div
|
||||
class="paper-table-row"
|
||||
role="row"
|
||||
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed'}"
|
||||
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed', 'row-selected': selected[repo.repoId]}"
|
||||
ng-repeat="repo in repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories"
|
||||
>
|
||||
<div role="cell" style="width: 28px;">
|
||||
<input type="checkbox" ng-model="selected[repo.repoId]" aria-label="Select repository" />
|
||||
</div>
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<div class="anon-text">
|
||||
@@ -125,8 +97,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<span class="status-dot" ng-class="{'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-preparing': repo.status == 'preparing'}"></span>
|
||||
<span ng-bind="repo.status | title"></span>
|
||||
<span class="status-line">
|
||||
<span class="status-dot" ng-class="{'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-preparing': repo.status == 'preparing'}"></span>
|
||||
<span ng-bind="repo.status | title"></span>
|
||||
</span>
|
||||
<span class="status-sub" ng-if="repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" ng-bind="::repo.pageView || 0 | number"></div>
|
||||
<div class="cell-expires" role="cell" ng-bind="repo.anonymizeDate | humanTime"></div>
|
||||
@@ -139,9 +114,11 @@
|
||||
<a class="dropdown-item" href="/anonymize/{{repo.repoId}}"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" href="/r/{{repo.repoId}}/"><i class="fa fa-eye"></i> View repo</a>
|
||||
<a class="dropdown-item" href="/w/{{repo.repoId}}/" target="_self" ng-if="repo.options.page && repo.status == 'ready'"><i class="fas fa-globe"></i> View page</a>
|
||||
<a class="dropdown-item" href="#" ng-click="fetchGithubInfo(repo)"><i class="fab fa-github"></i> Live GitHub info</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" ng-show="repo.status == 'ready' || repo.status == 'error'" ng-click="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
|
||||
<a class="dropdown-item" href="#" ng-show="repo.status == 'removed'" ng-click="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a>
|
||||
<a class="dropdown-item" href="#" ng-show="repo.statusMessage" ng-click="showStatusMessage(repo)"><i class="fas fa-exclamation-triangle"></i> View status message</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" ng-click="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a>
|
||||
<a class="dropdown-item text-danger" href="#" ng-show="repo.status == 'ready'" ng-click="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
@@ -155,15 +132,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-toolbar" ng-if="totalPage > 1" style="justify-content: center; border-bottom: none;">
|
||||
<div class="pagination-compact">
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{total | number}} results</span>
|
||||
<div class="pagination-compact" ng-if="totalPage > 1">
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1">
|
||||
<i class="fas fa-chevron-left"></i> Previous
|
||||
</button>
|
||||
<span>Page {{query.page}} of {{totalPage}}</span>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="query.page" min="1" max="{{totalPage}}" style="width: 56px;" aria-label="Page" />
|
||||
<span>of {{totalPage}}</span>
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">
|
||||
Next <i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
<label>Per page</label>
|
||||
<select class="form-control form-control-sm" ng-model="query.limit">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="250">250</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<div class="container paper-page">
|
||||
<div class="container paper-page admin-page">
|
||||
<div class="paper-crumbs"><a href="/admin/users">Users</a> / <span class="here">{{userInfo.username || 'Profile'}}</span></div>
|
||||
<h1 class="paper-page-title">User <em>profile</em></h1>
|
||||
<p class="paper-page-lede">Inspect activity, quota, and repositories for a single account.</p>
|
||||
<h1 class="paper-page-title">{{userInfo.username || 'User'}}</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<div class="container paper-page">
|
||||
<div class="container paper-page admin-page">
|
||||
<div class="paper-crumbs">Admin / <span class="here">Users</span></div>
|
||||
<h1 class="paper-page-title">Registered <em>users</em></h1>
|
||||
<p class="paper-page-lede">Browse, search, and manage every account.</p>
|
||||
<h1 class="paper-page-title">Users</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
@@ -10,73 +9,84 @@
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-stats">
|
||||
<div class="admin-stat-card">
|
||||
<div class="stat-value" ng-bind="total >= 0 ? (total | number) : '...'"></div>
|
||||
<div class="stat-label">Total users</div>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="stat-value">{{query.page}}/{{totalPage || '...'}}</div>
|
||||
<div class="stat-label">Current page</div>
|
||||
</div>
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{total >= 0 ? (total | number) : '…'}}</span>
|
||||
<span class="summary-pill ok" ng-class="{active: query.status == 'active'}" ng-click="query.status = query.status == 'active' ? '' : 'active'; query.page = 1">Active <span class="count">{{statusCountFor('active') | number}}</span></span>
|
||||
<span class="summary-pill error" ng-class="{active: query.status == 'banned'}" ng-click="query.status = query.status == 'banned' ? '' : 'banned'; query.page = 1">Banned <span class="count">{{statusCountFor('banned') | number}}</span></span>
|
||||
<span class="summary-pill" ng-class="{active: query.status == 'removed'}" ng-click="query.status = query.status == 'removed' ? '' : 'removed'; query.page = 1">Removed <span class="count">{{statusCountFor('removed') | number}}</span></span>
|
||||
</div>
|
||||
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Users" accept-charset="UTF-8">
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
type="search"
|
||||
class="form-control"
|
||||
aria-label="Search users"
|
||||
placeholder="Search users…"
|
||||
autocomplete="off"
|
||||
ng-model="query.search"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap" style="gap: 8px; align-items: center;">
|
||||
<div class="pagination-compact">
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="query.page" min="1" max="{{totalPage}}" />
|
||||
<span>/{{totalPage}}</span>
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert alert-danger" ng-if="fetchError" style="margin: 8px 0;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{fetchError}}
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Sort</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownSort">
|
||||
<h6 class="dropdown-header">Sort by</h6>
|
||||
<a class="dropdown-item" href="#" ng-click="query.sort = 'username'">
|
||||
<i class="fas fa-check" ng-show="query.sort == 'username'"></i> Username
|
||||
</a>
|
||||
</div>
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Users" accept-charset="UTF-8">
|
||||
<div class="admin-filter-row">
|
||||
<div class="search-wrap">
|
||||
<input type="search" class="form-control" aria-label="Search users" placeholder="Search username or email…" autocomplete="off" ng-model="query.search" />
|
||||
<span class="admin-search-hint" ng-if="!query.search">/</span>
|
||||
</div>
|
||||
<span class="admin-filter-inline"><label>Role</label>
|
||||
<select class="form-control form-control-sm" ng-model="query.role"><option value="">Any</option><option value="admin">Admin</option></select>
|
||||
</span>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" ng-click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<span class="admin-filter-inline" aria-label="Pagination">
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i></button>
|
||||
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{query.page}}/{{totalPage || 1}}</span>
|
||||
<button class="btn btn-sm" type="button" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="admin-filter-row" ng-if="chips.length">
|
||||
<div class="admin-active-chips">
|
||||
<span class="admin-active-chip" ng-repeat="chip in chips track by chip.key">
|
||||
<span class="key">{{chip.label}}</span>
|
||||
<span>{{chip.value}}</span>
|
||||
<button type="button" ng-click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="paper-table w-100" role="table" aria-label="Users" style="--cols: minmax(280px, 2.4fr) 140px 140px 52px;">
|
||||
<div class="bulk-bar" ng-if="selectedCount() > 0">
|
||||
<span><strong>{{selectedCount()}}</strong> selected</span>
|
||||
<button class="btn btn-sm text-danger" type="button" ng-click="bulkBan()"><i class="fas fa-ban"></i> Ban</button>
|
||||
<button class="btn btn-sm" type="button" ng-click="selected = {}; allSelected = false">Clear</button>
|
||||
</div>
|
||||
|
||||
<div class="paper-table w-100" role="table" aria-label="Users" style="--cols: 28px minmax(280px, 2.4fr) 100px 140px 140px 52px;">
|
||||
<div class="paper-table-head admin-users-row" role="row">
|
||||
<div role="columnheader">User</div>
|
||||
<div role="columnheader">Status</div>
|
||||
<div role="columnheader" style="width: 28px;">
|
||||
<input type="checkbox" ng-click="selectAllOnPage()" ng-checked="allSelected" aria-label="Select all on page" />
|
||||
</div>
|
||||
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'username'}" ng-click="sortBy('username')">User <i class="fas" ng-class="sortIcon('username')"></i></span></div>
|
||||
<div role="columnheader" class="num">Repos</div>
|
||||
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'status'}" ng-click="sortBy('status')">Status <i class="fas" ng-class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader">Role</div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div
|
||||
class="paper-table-row admin-users-row"
|
||||
role="row"
|
||||
ng-class="{'row-selected': selected[u.username]}"
|
||||
ng-repeat="u in users | filter:userFiler | orderBy:orderBy as filteredUsers"
|
||||
>
|
||||
<div role="cell" style="width: 28px;">
|
||||
<input type="checkbox" ng-model="selected[u.username]" aria-label="Select user" />
|
||||
</div>
|
||||
<div class="cell-anon" role="cell">
|
||||
<img ng-src="{{u.photo}}" ng-if="u.photo" width="28" height="28" class="rounded-circle" style="flex-shrink:0;" />
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" ng-href="/admin/users/{{u.username}}" ng-bind="u.username"></a>
|
||||
<div class="anon-sub">
|
||||
<span ng-if="u.emails[0].email">{{u.emails[0].email}}</span><span ng-if="u.emails[0].email"> · </span><a href="https://github.com/{{u.username}}" target="_blank"><i class="fab fa-github"></i> {{u.username}}</a>
|
||||
<span ng-if="u.emails[0].email">{{u.emails[0].email}}</span><span ng-if="u.emails[0].email"> · </span><a href="https://github.com/{{u.username}}" target="_blank"><i class="fab fa-github"></i> {{u.username}}</a><span ng-if="u.dateOfEntry"> · Joined {{u.dateOfEntry | humanTime}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell">
|
||||
<a ng-href="/admin/?owner={{u.username}}" ng-bind="(u.repoCount || 0) | number" title="Show this user's repositories"></a>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<span class="status-dot" ng-class="{'status-ready': u.status == 'active', 'status-removed': u.status == 'removed' || u.status == 'banned'}"></span>
|
||||
<span ng-bind="u.status | title"></span>
|
||||
@@ -92,6 +102,7 @@
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="/admin/users/{{u.username}}"><i class="far fa-eye"></i> View details</a>
|
||||
<a class="dropdown-item" href="/admin/?owner={{u.username}}"><i class="fas fa-code-branch"></i> View repositories</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#" ng-show="u.status == 'active'" ng-click="banUser(u)"><i class="fas fa-ban"></i> Ban</a>
|
||||
<a class="dropdown-item" href="#" ng-show="u.status == 'removed' || u.status == 'banned'" ng-click="activateUser(u)"><i class="fas fa-check-circle"></i> Activate</a>
|
||||
@@ -105,15 +116,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-toolbar" ng-if="totalPage > 1" style="justify-content: center; border-bottom: none;">
|
||||
<div class="pagination-compact">
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1">
|
||||
<i class="fas fa-chevron-left"></i> Previous
|
||||
</button>
|
||||
<span>Page {{query.page}} of {{totalPage}}</span>
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">
|
||||
Next <i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{total | number}} results</span>
|
||||
<div class="pagination-compact" ng-if="totalPage > 1">
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="query.page" min="1" max="{{totalPage}}" style="width: 56px;" />
|
||||
<span>of {{totalPage}}</span>
|
||||
<button class="btn btn-sm" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">Next <i class="fas fa-chevron-right"></i></button>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
<label>Per page</label>
|
||||
<select class="form-control form-control-sm" ng-model="query.limit"><option value="10">10</option><option value="25">25</option><option value="50">50</option><option value="100">100</option></select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user