mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-09-18 00:22:15 +02:00
refactor: migrate the frontend from AngularJS to Vue 3
This commit is contained in:
@@ -12,39 +12,39 @@
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{total >= 0 ? (total | number) : '…'}}</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') | 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') | 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') | number}}</span></span>
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</span>
|
||||
<span class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></span>
|
||||
<span class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></span>
|
||||
<span class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></span>
|
||||
<span class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></span>
|
||||
<span class="summary-pill" title="Toggle removed filter" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></span>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" ng-if="fetchError" style="margin: 8px 0;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{fetchError}}
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Conferences" accept-charset="UTF-8">
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Conferences" accept-charset="UTF-8" v-form="viewState">
|
||||
<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>
|
||||
<input type="search" class="form-control" placeholder="Search conferences…" autocomplete="off" v-field="{ state: viewState, set: value => { query.search = value }, value: query?.search, form: null, options: {} }">
|
||||
<span class="admin-search-hint" v-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>
|
||||
<button class="btn btn-sm" type="button" @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>
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :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" @click="query.page = Math.min(totalPage, query.page + 1)" :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-filter-row" v-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 class="admin-active-chip" v-for="(chip, index) in chips" :key="chip?.key">
|
||||
<span class="key">{{ chip?.label }}</span>
|
||||
<span>{{ chip?.value }}</span>
|
||||
<button type="button" @click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,66 +52,62 @@
|
||||
|
||||
<div class="paper-table paper-table-conferences w-100" role="table" aria-label="Conferences">
|
||||
<div class="paper-table-head" role="row">
|
||||
<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"><span class="sortable" :class="{active: query?.sort == 'name'}" @click="sortBy('name')">Conference <i class="fas" :class="sortIcon('name')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader" class="num">Repos</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"><span class="sortable" :class="{active: query?.sort == 'startDate'}" @click="sortBy('startDate')">Window <i class="fas" :class="sortIcon('startDate')"></i></span></div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div
|
||||
class="paper-table-row"
|
||||
role="row"
|
||||
ng-repeat="conference in conferences as filteredConferences"
|
||||
>
|
||||
<div class="paper-table-row" role="row" v-for="(conference, index) in filteredConferences">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Conf</span>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" ng-href="/conference/{{conference.conferenceID}}" ng-bind="conference.name"></a>
|
||||
<a class="repo-name" v-text="conference?.name" :href="safeUrl("/conference/" + (conference?.conferenceID))"></a>
|
||||
<div class="anon-sub">
|
||||
<span>{{conference.conferenceID}}</span> · <span>{{conference.price || 0 | number}} €</span>
|
||||
<span>{{ conference?.conferenceID }}</span> · <span>{{ fmt?.number(conference?.price || 0) }} €</span>
|
||||
</div>
|
||||
</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', 'status-error': conference.status == 'error', 'status-preparing': conference.status == 'preparing'}"></span>
|
||||
<span ng-bind="conference.status | title"></span>
|
||||
<span class="status-dot" :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 v-text="fmt?.title(conference?.status)"></span>
|
||||
</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>
|
||||
<a title="Show repositories in this conference" v-text="fmt?.number(conference?.repositories?.length || 0)" :href="safeUrl("/admin/?conference=" + (conference?.conferenceID))"></a>
|
||||
</div>
|
||||
<div class="cell-expires" role="cell">{{conference.startDate | date}} – {{conference.endDate | date}}</div>
|
||||
<div class="cell-expires" role="cell">{{ fmt?.date(conference?.startDate) }} – {{ fmt?.date(conference?.endDate) }}</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</button>
|
||||
<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>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/")"><i class="fa fa-eye"></i> View</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/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>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="conference?.status != 'removed'" @click.prevent="removeConference(conference)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" ng-if="filteredConferences.length == 0">
|
||||
<div class="paper-table-empty" v-if="filteredConferences?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<span>No conferences match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
<span>of {{ totalPage }}</span>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :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>
|
||||
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value => { query.limit = value }, value: query?.limit, form: null, options: {} }"><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>
|
||||
|
||||
@@ -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 > 0, down: stats?.delta < 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="{'kpi-error': stats?.dropped > 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 > 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, 'error') + 'px'}"></span>
|
||||
<span class="seg seg-warn" :style="{height: barPx(b, 'warn') + 'px'}"></span>
|
||||
<span class="seg seg-info" :style="{height: barPx(b, 'info') + 'px'}"></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 === ''}" @click="setBucket('')">All</button>
|
||||
<button type="button" :class="{active: query?.bucket === 'error'}" @click="setBucket('error')">5xx</button>
|
||||
<button type="button" :class="{active: query?.bucket === 'warn'}" @click="setBucket('warn')">4xx</button>
|
||||
<button type="button" :class="{active: query?.bucket === 'info'}" @click="setBucket('info')">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:>=400" autocomplete="off" v-field="{ state: viewState, set: value => { 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 => { 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 => { 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 => { 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() && 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="'sev-' + 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 && 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 > 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="'status-' + 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] === 'raw' || !detailTab[row?._key]}" @click="detailTab[row._key] = 'raw'">Raw</button>
|
||||
<button type="button" v-if="row?._stack" :class="{active: detailTab[row?._key] === 'stack'}" @click="detailTab[row._key] = 'stack'">Stack</button>
|
||||
<button type="button" v-if="row?.count > 1" :class="{active: detailTab[row?._key] === 'related'}" @click="detailTab[row._key] = 'related'">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] || 'raw') === 'raw'">{{ row?._detailJson }}</pre>
|
||||
<pre class="stack-pre" v-if="detailTab[row?._key] === 'stack'">{{ row?._stack }}</pre>
|
||||
<div class="related-list" v-if="detailTab[row?._key] === 'related'">
|
||||
<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="'status-' + 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("/r/" + (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>
|
||||
|
||||
@@ -14,37 +14,37 @@
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div ng-if="loading" class="admin-empty">Loading overview…</div>
|
||||
<div ng-if="error" class="alert alert-danger" style="margin:12px 0"><i class="fas fa-exclamation-triangle"></i> {{error}}</div>
|
||||
<div class="admin-empty" v-if="loading">Loading overview…</div>
|
||||
<div class="alert alert-danger" style="margin:12px 0" v-if="error"><i class="fas fa-exclamation-triangle"></i> {{ error }}</div>
|
||||
|
||||
<div ng-if="data">
|
||||
<div v-if="data">
|
||||
|
||||
<!-- ── Top KPI row ──────────────────────────────────────────── -->
|
||||
<section class="ov-kpi-row">
|
||||
<div class="ov-kpi-card">
|
||||
<div class="ov-kpi-label">Repositories <span class="ov-dot ov-dot-ok"></span></div>
|
||||
<div class="ov-kpi-value">{{humanNum(data.repos.total)}}</div>
|
||||
<div class="ov-kpi-sub">+{{data.repos.newRepos24h}} · last 24h</div>
|
||||
<div class="ov-kpi-value">{{ humanNum(data?.repos?.total) }}</div>
|
||||
<div class="ov-kpi-sub">+{{ data?.repos?.newRepos24h }} · last 24h</div>
|
||||
</div>
|
||||
<div class="ov-kpi-card">
|
||||
<div class="ov-kpi-label">CPU</div>
|
||||
<div class="ov-kpi-value" ng-class="{'ov-val-warn': data.system.cpuPercent > 80}">{{data.system.cpuPercent}}%</div>
|
||||
<div class="ov-kpi-sub">{{data.system.cpuCount}} cores · load {{data.system.loadAvg[0].toFixed(1)}}</div>
|
||||
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.cpuPercent > 80}">{{ data?.system?.cpuPercent }}%</div>
|
||||
<div class="ov-kpi-sub">{{ data?.system?.cpuCount }} cores · load {{ data?.system?.loadAvg?.[0]?.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="ov-kpi-card">
|
||||
<div class="ov-kpi-label">Memory</div>
|
||||
<div class="ov-kpi-value" ng-class="{'ov-val-warn': data.system.memPercent > 85}">{{data.system.memPercent}}%</div>
|
||||
<div class="ov-kpi-sub">{{humanBytes(data.system.memUsed)}} / {{humanBytes(data.system.memTotal)}}</div>
|
||||
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.memPercent > 85}">{{ data?.system?.memPercent }}%</div>
|
||||
<div class="ov-kpi-sub">{{ humanBytes(data?.system?.memUsed) }} / {{ humanBytes(data?.system?.memTotal) }}</div>
|
||||
</div>
|
||||
<div class="ov-kpi-card">
|
||||
<div class="ov-kpi-label">Disk</div>
|
||||
<div class="ov-kpi-value" ng-class="{'ov-val-warn': data.system.diskPercent > 85}">{{data.system.diskPercent}}%</div>
|
||||
<div class="ov-kpi-sub">{{humanBytes(data.system.diskUsed)}} / {{humanBytes(data.system.diskTotal)}} · {{data.system.diskMount}}</div>
|
||||
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.diskPercent > 85}">{{ data?.system?.diskPercent }}%</div>
|
||||
<div class="ov-kpi-sub">{{ humanBytes(data?.system?.diskUsed) }} / {{ humanBytes(data?.system?.diskTotal) }} · {{ data?.system?.diskMount }}</div>
|
||||
</div>
|
||||
<div class="ov-kpi-card">
|
||||
<div class="ov-kpi-label">Uptime</div>
|
||||
<div class="ov-kpi-value">{{humanDuration(data.system.uptime)}}</div>
|
||||
<div class="ov-kpi-sub">{{data.system.nodeVersion}} · {{data.system.platform}}</div>
|
||||
<div class="ov-kpi-value">{{ humanDuration(data?.system?.uptime) }}</div>
|
||||
<div class="ov-kpi-sub">{{ data?.system?.nodeVersion }} · {{ data?.system?.platform }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -52,17 +52,17 @@
|
||||
<section class="ov-daily-row">
|
||||
<div class="ov-daily-card">
|
||||
<div class="ov-daily-label">New repos today</div>
|
||||
<div class="ov-daily-value">+{{data.daily.today.repositories | number}}</div>
|
||||
<div class="ov-daily-value">+{{ fmt?.number(data?.daily?.today?.repositories) }}</div>
|
||||
<div class="ov-daily-sub">since yesterday</div>
|
||||
</div>
|
||||
<div class="ov-daily-card">
|
||||
<div class="ov-daily-label">New users today</div>
|
||||
<div class="ov-daily-value">+{{data.daily.today.users | number}}</div>
|
||||
<div class="ov-daily-sub">{{data.users.total | number}} total users</div>
|
||||
<div class="ov-daily-value">+{{ fmt?.number(data?.daily?.today?.users) }}</div>
|
||||
<div class="ov-daily-sub">{{ fmt?.number(data?.users?.total) }} total users</div>
|
||||
</div>
|
||||
<div class="ov-daily-card">
|
||||
<div class="ov-daily-label">Page views today</div>
|
||||
<div class="ov-daily-value">+{{data.daily.today.pageViews | number}}</div>
|
||||
<div class="ov-daily-value">+{{ fmt?.number(data?.daily?.today?.pageViews) }}</div>
|
||||
<div class="ov-daily-sub">since yesterday</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -74,17 +74,15 @@
|
||||
<span class="ov-chart-title">Daily page views · 30d</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views/day</span>
|
||||
</div>
|
||||
<div class="ov-spark-bars" ng-if="data.history.length">
|
||||
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index"
|
||||
ng-attr-data-tip="{{historyLabel(d)}}: +{{d.dailyPageViews | number}} views"
|
||||
ng-attr-aria-label="{{historyLabel(d)}}: +{{d.dailyPageViews | number}} views">
|
||||
<span class="ov-spark-fill" ng-style="{height: historyBarH(d, 'dailyPageViews') + 'px'}"></span>
|
||||
<div class="ov-spark-bars" v-if="data?.history?.length">
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyPageViews)) + " views"" :aria-label="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyPageViews)) + " views"">
|
||||
<span class="ov-spark-fill" :style="{height: historyBarH(d, 'dailyPageViews') + 'px'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-spark-x" ng-if="data.history.length">
|
||||
<span>{{historyLabel(data.history[0])}}</span>
|
||||
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span>
|
||||
<span>{{historyLabel(data.history[data.history.length - 1])}}</span>
|
||||
<div class="ov-spark-x" v-if="data?.history?.length">
|
||||
<span>{{ historyLabel(data?.history[0]) }}</span>
|
||||
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
|
||||
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-chart-card">
|
||||
@@ -92,17 +90,15 @@
|
||||
<span class="ov-chart-title">New repos · 30d</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-ok-fill"></span>repos/day</span>
|
||||
</div>
|
||||
<div class="ov-spark-bars" ng-if="data.history.length">
|
||||
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index"
|
||||
ng-attr-data-tip="{{historyLabel(d)}}: +{{d.dailyRepositories | number}} repos"
|
||||
ng-attr-aria-label="{{historyLabel(d)}}: +{{d.dailyRepositories | number}} repos">
|
||||
<span class="ov-spark-fill ov-spark-fill-alt" ng-style="{height: historyBarH(d, 'dailyRepositories') + 'px'}"></span>
|
||||
<div class="ov-spark-bars" v-if="data?.history?.length">
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyRepositories)) + " repos"" :aria-label="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyRepositories)) + " repos"">
|
||||
<span class="ov-spark-fill ov-spark-fill-alt" :style="{height: historyBarH(d, 'dailyRepositories') + 'px'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-spark-x" ng-if="data.history.length">
|
||||
<span>{{historyLabel(data.history[0])}}</span>
|
||||
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span>
|
||||
<span>{{historyLabel(data.history[data.history.length - 1])}}</span>
|
||||
<div class="ov-spark-x" v-if="data?.history?.length">
|
||||
<span>{{ historyLabel(data?.history[0]) }}</span>
|
||||
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
|
||||
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-chart-card">
|
||||
@@ -110,17 +106,15 @@
|
||||
<span class="ov-chart-title">Users · 30d</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>total users</span>
|
||||
</div>
|
||||
<div class="ov-spark-bars" ng-if="data.history.length">
|
||||
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index"
|
||||
ng-attr-data-tip="{{historyLabel(d)}}: {{d.nbUsers | number}} users"
|
||||
ng-attr-aria-label="{{historyLabel(d)}}: {{d.nbUsers | number}} users">
|
||||
<span class="ov-spark-fill ov-spark-fill-user" ng-style="{height: historyBarH(d, 'nbUsers') + 'px'}"></span>
|
||||
<div class="ov-spark-bars" v-if="data?.history?.length">
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": " + (fmt?.number(d?.nbUsers)) + " users"" :aria-label="(historyLabel(d)) + ": " + (fmt?.number(d?.nbUsers)) + " users"">
|
||||
<span class="ov-spark-fill ov-spark-fill-user" :style="{height: historyBarH(d, 'nbUsers') + 'px'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-spark-x" ng-if="data.history.length">
|
||||
<span>{{historyLabel(data.history[0])}}</span>
|
||||
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span>
|
||||
<span>{{historyLabel(data.history[data.history.length - 1])}}</span>
|
||||
<div class="ov-spark-x" v-if="data?.history?.length">
|
||||
<span>{{ historyLabel(data?.history[0]) }}</span>
|
||||
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
|
||||
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-chart-card">
|
||||
@@ -128,17 +122,15 @@
|
||||
<span class="ov-chart-title">New users · 30d</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-new-user-fill"></span>users/day</span>
|
||||
</div>
|
||||
<div class="ov-spark-bars" ng-if="data.history.length">
|
||||
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index"
|
||||
ng-attr-data-tip="{{historyLabel(d)}}: +{{d.dailyUsers | number}} users"
|
||||
ng-attr-aria-label="{{historyLabel(d)}}: +{{d.dailyUsers | number}} users">
|
||||
<span class="ov-spark-fill ov-spark-fill-new-user" ng-style="{height: historyBarH(d, 'dailyUsers') + 'px'}"></span>
|
||||
<div class="ov-spark-bars" v-if="data?.history?.length">
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyUsers)) + " users"" :aria-label="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyUsers)) + " users"">
|
||||
<span class="ov-spark-fill ov-spark-fill-new-user" :style="{height: historyBarH(d, 'dailyUsers') + 'px'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-spark-x" ng-if="data.history.length">
|
||||
<span>{{historyLabel(data.history[0])}}</span>
|
||||
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span>
|
||||
<span>{{historyLabel(data.history[data.history.length - 1])}}</span>
|
||||
<div class="ov-spark-x" v-if="data?.history?.length">
|
||||
<span>{{ historyLabel(data?.history[0]) }}</span>
|
||||
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
|
||||
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -150,21 +142,21 @@
|
||||
<div class="ov-panel-card">
|
||||
<div class="ov-panel-head">
|
||||
<span class="ov-panel-title">Repo status</span>
|
||||
<span class="ov-panel-meta">{{data.repos.total | number}} total</span>
|
||||
<span class="ov-panel-meta">{{ fmt?.number(data?.repos?.total) }} total</span>
|
||||
</div>
|
||||
<div class="ov-stacked-bar">
|
||||
<span class="ov-bar-seg ov-bar-ready" ng-style="{width: barPct('ready') + '%'}" title="Ready"></span>
|
||||
<span class="ov-bar-seg ov-bar-preparing" ng-style="{width: barPct('preparing') + '%'}" title="Preparing"></span>
|
||||
<span class="ov-bar-seg ov-bar-error" ng-style="{width: barPct('error') + '%'}" title="Error"></span>
|
||||
<span class="ov-bar-seg ov-bar-expired" ng-style="{width: barPct('expired') + '%'}" title="Expired"></span>
|
||||
<span class="ov-bar-seg ov-bar-removed" ng-style="{width: barPct('removed') + '%'}" title="Removed"></span>
|
||||
<span class="ov-bar-seg ov-bar-ready" title="Ready" :style="{width: barPct('ready') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-preparing" title="Preparing" :style="{width: barPct('preparing') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-error" title="Error" :style="{width: barPct('error') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-expired" title="Expired" :style="{width: barPct('expired') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-removed" title="Removed" :style="{width: barPct('removed') + '%'}"></span>
|
||||
</div>
|
||||
<div class="ov-bar-legend">
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-ready"></span> ready <span class="ov-legend-n">{{statusCount('ready') | number}}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-preparing"></span> preparing <span class="ov-legend-n">{{statusCount('preparing') + statusCount('download') | number}}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-error"></span> error <span class="ov-legend-n">{{statusCount('error') | number}}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-expired"></span> expired <span class="ov-legend-n">{{statusCount('expired') + statusCount('expiring') | number}}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-removed"></span> removed <span class="ov-legend-n">{{statusCount('removed') + statusCount('removing') | number}}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-ready"></span> ready <span class="ov-legend-n">{{ fmt?.number(statusCount('ready')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-preparing"></span> preparing <span class="ov-legend-n">{{ fmt?.number(statusCount('preparing') + statusCount('download')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-error"></span> error <span class="ov-legend-n">{{ fmt?.number(statusCount('error')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-expired"></span> expired <span class="ov-legend-n">{{ fmt?.number(statusCount('expired') + statusCount('expiring')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-removed"></span> removed <span class="ov-legend-n">{{ fmt?.number(statusCount('removed') + statusCount('removing')) }}</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -172,27 +164,27 @@
|
||||
<div class="ov-panel-card">
|
||||
<div class="ov-panel-head">
|
||||
<span class="ov-panel-title">Errors · 24h</span>
|
||||
<span class="ov-panel-meta">{{data.errors.last24h}} total</span>
|
||||
<span class="ov-panel-meta">{{ data?.errors?.last24h }} total</span>
|
||||
</div>
|
||||
<div class="ov-error-bars">
|
||||
<div class="ov-ebar-row">
|
||||
<span class="ov-ebar-label">5xx</span>
|
||||
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-error" ng-style="{width: errPct('error') + '%'}"></span></span>
|
||||
<span class="ov-ebar-n">{{data.errors.severity.error}}</span>
|
||||
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-error" :style="{width: errPct('error') + '%'}"></span></span>
|
||||
<span class="ov-ebar-n">{{ data?.errors?.severity?.error }}</span>
|
||||
</div>
|
||||
<div class="ov-ebar-row">
|
||||
<span class="ov-ebar-label">4xx</span>
|
||||
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-warn" ng-style="{width: errPct('warn') + '%'}"></span></span>
|
||||
<span class="ov-ebar-n">{{data.errors.severity.warn}}</span>
|
||||
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-warn" :style="{width: errPct('warn') + '%'}"></span></span>
|
||||
<span class="ov-ebar-n">{{ data?.errors?.severity?.warn }}</span>
|
||||
</div>
|
||||
<div class="ov-ebar-row">
|
||||
<span class="ov-ebar-label">Info</span>
|
||||
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-info" ng-style="{width: errPct('info') + '%'}"></span></span>
|
||||
<span class="ov-ebar-n">{{data.errors.severity.info}}</span>
|
||||
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-info" :style="{width: errPct('info') + '%'}"></span></span>
|
||||
<span class="ov-ebar-n">{{ data?.errors?.severity?.info }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-panel-foot" ng-if="data.repos.recentErrors24h">
|
||||
<a href="/admin/errors">{{data.repos.recentErrors24h}} repos in error state →</a>
|
||||
<div class="ov-panel-foot" v-if="data?.repos?.recentErrors24h">
|
||||
<a href="/admin/errors">{{ data?.repos?.recentErrors24h }} repos in error state →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -211,21 +203,21 @@
|
||||
</div>
|
||||
<a class="ov-route-row" href="/admin/queues">
|
||||
<span class="ov-route-name">download</span>
|
||||
<span class="ov-route-n">{{data.queues.download.active}}</span>
|
||||
<span class="ov-route-n">{{data.queues.download.waiting}}</span>
|
||||
<span class="ov-route-n ov-route-lat" ng-class="{'ov-n-bad': data.queues.download.failed > 0}">{{data.queues.download.failed}}</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.download?.active }}</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.download?.waiting }}</span>
|
||||
<span class="ov-route-n ov-route-lat" :class="{'ov-n-bad': data?.queues?.download?.failed > 0}">{{ data?.queues?.download?.failed }}</span>
|
||||
</a>
|
||||
<a class="ov-route-row" href="/admin/queues">
|
||||
<span class="ov-route-name">remove</span>
|
||||
<span class="ov-route-n">{{data.queues.remove.active}}</span>
|
||||
<span class="ov-route-n">{{data.queues.remove.waiting}}</span>
|
||||
<span class="ov-route-n ov-route-lat" ng-class="{'ov-n-bad': data.queues.remove.failed > 0}">{{data.queues.remove.failed}}</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.remove?.active }}</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.remove?.waiting }}</span>
|
||||
<span class="ov-route-n ov-route-lat" :class="{'ov-n-bad': data?.queues?.remove?.failed > 0}">{{ data?.queues?.remove?.failed }}</span>
|
||||
</a>
|
||||
<a class="ov-route-row" href="/admin/queues">
|
||||
<span class="ov-route-name">cache</span>
|
||||
<span class="ov-route-n">{{data.queues.cache.active}}</span>
|
||||
<span class="ov-route-n">{{data.queues.cache.waiting}}</span>
|
||||
<span class="ov-route-n ov-route-lat" ng-class="{'ov-n-bad': data.queues.cache.failed > 0}">{{data.queues.cache.failed}}</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.cache?.active }}</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.cache?.waiting }}</span>
|
||||
<span class="ov-route-n ov-route-lat" :class="{'ov-n-bad': data?.queues?.cache?.failed > 0}">{{ data?.queues?.cache?.failed }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,46 +228,46 @@
|
||||
<section class="ov-services-card">
|
||||
<div class="ov-services-head">
|
||||
<span class="ov-panel-title">Services</span>
|
||||
<span class="ov-panel-meta">{{data.users.total | number}} users · {{data.conferences.total}} conferences</span>
|
||||
<span class="ov-panel-meta">{{ fmt?.number(data?.users?.total) }} users · {{ data?.conferences?.total }} conferences</span>
|
||||
</div>
|
||||
<div class="ov-services-grid">
|
||||
<div class="ov-svc">
|
||||
<span class="ov-svc-dot ov-dot-ok"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">web</span>
|
||||
<span class="ov-svc-meta">{{data.system.nodeVersion}}</span>
|
||||
<span class="ov-svc-meta">{{ data?.system?.nodeVersion }}</span>
|
||||
</div>
|
||||
<span class="ov-svc-detail">uptime {{humanDuration(data.system.uptime)}}</span>
|
||||
<span class="ov-svc-detail">uptime {{ humanDuration(data?.system?.uptime) }}</span>
|
||||
</div>
|
||||
<div class="ov-svc">
|
||||
<span class="ov-svc-dot" ng-class="queueTotal(data.queues.download) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
||||
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.download) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">download</span>
|
||||
<span class="ov-svc-meta" ng-if="queueTotal(data.queues.download)">{{queueTotal(data.queues.download)}} jobs</span>
|
||||
<span class="ov-svc-meta" ng-if="!queueTotal(data.queues.download)">idle</span>
|
||||
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.download)">{{ queueTotal(data?.queues?.download) }} jobs</span>
|
||||
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.download)">idle</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-svc">
|
||||
<span class="ov-svc-dot" ng-class="queueTotal(data.queues.cache) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
||||
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.cache) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">cache</span>
|
||||
<span class="ov-svc-meta" ng-if="queueTotal(data.queues.cache)">{{queueTotal(data.queues.cache)}} jobs</span>
|
||||
<span class="ov-svc-meta" ng-if="!queueTotal(data.queues.cache)">idle</span>
|
||||
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.cache)">{{ queueTotal(data?.queues?.cache) }} jobs</span>
|
||||
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.cache)">idle</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-svc">
|
||||
<span class="ov-svc-dot" ng-class="queueTotal(data.queues.remove) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
||||
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.remove) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">remove</span>
|
||||
<span class="ov-svc-meta" ng-if="queueTotal(data.queues.remove)">{{queueTotal(data.queues.remove)}} jobs</span>
|
||||
<span class="ov-svc-meta" ng-if="!queueTotal(data.queues.remove)">idle</span>
|
||||
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.remove)">{{ queueTotal(data?.queues?.remove) }} jobs</span>
|
||||
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.remove)">idle</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-svc">
|
||||
<span class="ov-svc-dot" ng-class="data.repos.recentErrors24h > 10 ? 'ov-dot-warn' : 'ov-dot-ok'"></span>
|
||||
<span class="ov-svc-dot" :class="data?.repos?.recentErrors24h > 10 ? 'ov-dot-warn' : 'ov-dot-ok'"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">errors</span>
|
||||
<span class="ov-svc-meta">{{data.errors.last24h}} / 24h</span>
|
||||
<span class="ov-svc-meta">{{ data?.errors?.last24h }} / 24h</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<h1 class="paper-page-title">Queues</h1>
|
||||
<div class="q-header-actions">
|
||||
<div class="q-range-btns">
|
||||
<button class="btn btn-sm" ng-class="{active: range == '1h'}" ng-click="setRange('1h')">1h</button>
|
||||
<button class="btn btn-sm" ng-class="{active: range == '6h'}" ng-click="setRange('6h')">6h</button>
|
||||
<button class="btn btn-sm" ng-class="{active: range == '24h'}" ng-click="setRange('24h')">24h</button>
|
||||
<button class="btn btn-sm" ng-class="{active: range == '7d'}" ng-click="setRange('7d')">7d</button>
|
||||
<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>
|
||||
<button class="btn btn-sm" ng-click="pauseAll()">Pause all</button>
|
||||
<button class="btn btn-sm btn-dark" ng-click="drainSelected()">Drain {{selectedQueue}}</button>
|
||||
<button class="btn btn-sm" @click="pauseAll()">Pause all</button>
|
||||
<button class="btn btn-sm btn-dark" @click="drainSelected()">Drain {{ selectedQueue }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,25 +26,25 @@
|
||||
|
||||
<!-- Queue overview cards -->
|
||||
<div class="q-cards">
|
||||
<div class="q-card" ng-repeat="q in queueList" ng-class="{selected: selectedQueue == q.key, paused: q.paused}" ng-click="selectQueue(q.key)">
|
||||
<div class="q-card" 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" ng-class="{'q-dot-red': q.paused || q.counts.failed > 0}"></span>
|
||||
<span class="q-card-name" ng-bind="q.label"></span>
|
||||
<span class="q-dot" :class="{'q-dot-red': q?.paused || q?.counts?.failed > 0}"></span>
|
||||
<span class="q-card-name" v-text="q?.label"></span>
|
||||
</div>
|
||||
<div class="q-card-count" ng-bind="(q.counts.waiting || 0) + (q.counts.active || 0) + (q.counts.delayed || 0)"></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</span>
|
||||
<div class="q-card-bar" ng-if="q.counts.active">
|
||||
<div class="q-card-bar-fill" ng-style="{width: (q.counts.active / ((q.counts.waiting || 0) + (q.counts.active || 0) + (q.counts.delayed || 0) || 1) * 100) + '%'}"></div>
|
||||
<span>waiting · {{ q?.counts?.active || 0 }} active</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" ng-if="selectedStats">
|
||||
<div class="q-detail" v-if="selectedStats">
|
||||
<div class="q-throughput">
|
||||
<div class="q-section-label">{{selectedQueue}}·throughput <span class="q-section-right"><span class="q-legend-completed">●</span> completed <span class="q-legend-failed">●</span> failed <span class="q-legend-exec">- -</span> avg time · {{range | uppercase}}</span></div>
|
||||
<div class="q-section-label">{{ selectedQueue }}·throughput <span class="q-section-right"><span class="q-legend-completed">●</span> completed <span class="q-legend-failed">●</span> failed <span class="q-legend-exec">- -</span> avg time · {{ fmt?.uppercase(range) }}</span></div>
|
||||
<div class="q-chart-wrap">
|
||||
<canvas id="q-throughput-chart" height="180"></canvas>
|
||||
<div id="q-chart-tooltip" class="q-chart-tooltip" style="display:none;"></div>
|
||||
@@ -52,67 +52,67 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-stats-panel">
|
||||
<div class="q-section-label">{{selectedQueue}}·stats</div>
|
||||
<div class="q-section-label">{{ selectedQueue }}·stats</div>
|
||||
<div class="q-stats-grid">
|
||||
<div class="q-stat">
|
||||
<div class="q-stat-label">WAITING</div>
|
||||
<div class="q-stat-value" ng-bind="selectedStats.counts.waiting || 0"></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" ng-bind="selectedStats.counts.active || 0"></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" ng-bind="selectedStats.completed24h | number"></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" ng-bind="selectedStats.failed24h || 0"></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" ng-bind="selectedStats.counts.delayed || 0"></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" ng-bind="selectedStats.workers || 0"></div>
|
||||
<div class="q-stat-value" v-text="selectedStats?.workers || 0"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-stats-actions">
|
||||
<button class="btn btn-sm" ng-click="togglePause()">{{selectedStats.paused ? 'Resume' : 'Pause'}}</button>
|
||||
<button class="btn btn-sm" ng-click="retryFailed()" ng-disabled="!selectedStats.counts.failed">Retry failed</button>
|
||||
<button class="btn btn-sm" ng-click="emptyQueue()">Empty</button>
|
||||
<button class="btn btn-sm" @click="togglePause()">{{ selectedStats?.paused ? 'Resume' : 'Pause' }}</button>
|
||||
<button class="btn btn-sm" @click="retryFailed()" :disabled="!selectedStats?.counts?.failed">Retry failed</button>
|
||||
<button class="btn btn-sm" @click="emptyQueue()">Empty</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-toast-error" ng-if="actionError" ng-click="actionError = null">
|
||||
<i class="fas fa-exclamation-circle"></i> {{actionError}}
|
||||
<div class="q-toast-error" v-if="actionError" @click="actionError = null">
|
||||
<i class="fas fa-exclamation-circle"></i> {{ actionError }}
|
||||
</div>
|
||||
|
||||
<!-- Jobs table -->
|
||||
<div class="q-jobs">
|
||||
<div class="q-jobs-header">
|
||||
<div class="q-section-label">ALL JOBS · {{selectedQueue | uppercase}}</div>
|
||||
<div class="q-section-label">ALL JOBS · {{ fmt?.uppercase(selectedQueue) }}</div>
|
||||
<div class="q-state-filters">
|
||||
<label class="q-state-toggle" ng-repeat="s in allStates">
|
||||
<input type="checkbox" ng-model="stateFilter[s]" />
|
||||
<span class="q-state-chip q-state-{{s}}">{{s}}</span>
|
||||
<label class="q-state-toggle" v-for="(s, index) in allStates">
|
||||
<input type="checkbox" v-field="{ state: viewState, set: value => { stateFilter[s] = value }, value: stateFilter[s], form: null, options: {} }">
|
||||
<span :class=""q-state-chip q-state-" + (s)">{{ s }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-search-row">
|
||||
<input type="search" class="form-control" placeholder="Search by job/repo id…" ng-model="query.search" autocomplete="off" />
|
||||
<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" ng-model="query.autoRefresh" />
|
||||
<input type="checkbox" v-field="{ state: viewState, set: value => { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button class="btn btn-sm" type="button" ng-click="refreshNow()" title="Refresh now"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm" type="button" title="Refresh now" @click="refreshNow()"><i class="fas fa-sync"></i></button>
|
||||
</div>
|
||||
|
||||
<table class="q-table" ng-if="filteredJobs().length > 0">
|
||||
<table class="q-table" v-if="filteredJobs().length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>STATE</th>
|
||||
@@ -124,90 +124,90 @@
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-repeat="job in filteredJobs()">
|
||||
<tr ng-class="{'q-row-failed': job._state == 'failed', 'q-row-expanded': expanded[job.id]}" ng-click="toggleJob(job)" style="cursor:pointer;">
|
||||
<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 class="q-state-badge q-state-{{job._state}}" ng-bind="job._state"></span>
|
||||
<span class="q-delay-hint" ng-if="job._state == 'delayed' && job.delayUntil" ng-bind="delayCountdown(job.delayUntil)"></span>
|
||||
<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">
|
||||
<i class="fas fa-chevron-right q-chevron" ng-class="{'q-chevron-open': expanded[job.id]}"></i>
|
||||
<a target="_blank" ng-href="/r/{{job.id}}" ng-click="$event.stopPropagation()" ng-bind="'job:' + (job.id | limitTo:6)"></a>
|
||||
<i class="fas fa-chevron-right q-chevron" :class="{'q-chevron-open': expanded[job?.id]}"></i>
|
||||
<a target="_blank" v-text="'job:' + fmt.limitTo(job?.id, 6)" @click="$event.stopPropagation()" :href="safeUrl("/r/" + (job?.id))"></a>
|
||||
</td>
|
||||
<td class="q-cell-payload">
|
||||
<span ng-bind="job.name || 'anonymize'"></span>
|
||||
<span class="q-payload-detail" ng-if="job.data.repoId && job.data.repoId !== job.name"> · {{job.data.repoId}}</span>
|
||||
<span v-text="job?.name || 'anonymize'"></span>
|
||||
<span class="q-payload-detail" v-if="job?.data?.repoId && job?.data?.repoId !== job?.name"> · {{ job?.data?.repoId }}</span>
|
||||
</td>
|
||||
<td class="q-cell-num" ng-bind="job.attemptsMade || 1"></td>
|
||||
<td class="q-cell-num" ng-bind="jobDuration(job)"></td>
|
||||
<td class="q-cell-num" v-text="job?.attemptsMade || 1"></td>
|
||||
<td class="q-cell-num" v-text="jobDuration(job)"></td>
|
||||
<td class="q-cell-progress">
|
||||
<div class="q-progress-wrap" ng-if="jobProgressPct(job) !== null">
|
||||
<div class="q-progress-bar" ng-style="{'--pct': jobProgressPct(job) + '%'}"></div>
|
||||
<span class="q-progress-label" ng-bind="jobProgressPct(job) + '%'"></span>
|
||||
<div class="q-progress-wrap" v-if="jobProgressPct(job) !== null">
|
||||
<div class="q-progress-bar" :style="{'--pct': jobProgressPct(job) + '%'}"></div>
|
||||
<span class="q-progress-label" v-text="jobProgressPct(job) + '%'"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="q-cell-actions">
|
||||
<button class="btn btn-sm" ng-click="retryJob(job); $event.stopPropagation()" title="Retry" ng-if="job._state == 'failed'"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm" ng-click="removeJob(job); $event.stopPropagation()" title="Remove"><i class="fas fa-trash-alt"></i></button>
|
||||
<button class="btn btn-sm" title="Retry" v-if="job?._state == 'failed'" @click="retryJob(job); $event.stopPropagation()"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm" title="Remove" @click="removeJob(job); $event.stopPropagation()"><i class="fas fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="q-detail-row" ng-if="expanded[job.id]">
|
||||
<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" ng-href="/r/{{job.id}}" ng-bind="job.id"></a></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 class="q-state-badge q-state-{{job._state}}" ng-bind="job._state"></span></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" ng-if="job.data.repoId">
|
||||
<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" ng-bind="job.data.repoId"></span>
|
||||
<span class="q-job-detail-value" v-text="job?.data?.repoId"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.timestamp">
|
||||
<div class="q-job-detail-item" v-if="job?.timestamp">
|
||||
<span class="q-job-detail-label">CREATED</span>
|
||||
<span class="q-job-detail-value" ng-bind="humanTime(job.timestamp)"></span>
|
||||
<span class="q-job-detail-value" v-text="humanTime(job?.timestamp)"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job._state == 'delayed' && job.delayUntil">
|
||||
<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>
|
||||
<span class="q-job-detail-value">{{ humanTime(job?.delayUntil) }} ({{ delayCountdown(job?.delayUntil) }})</span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.processedOn">
|
||||
<div class="q-job-detail-item" v-if="job?.processedOn">
|
||||
<span class="q-job-detail-label">PROCESSED</span>
|
||||
<span class="q-job-detail-value" ng-bind="humanTime(job.processedOn)"></span>
|
||||
<span class="q-job-detail-value" v-text="humanTime(job?.processedOn)"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.finishedOn">
|
||||
<div class="q-job-detail-item" v-if="job?.finishedOn">
|
||||
<span class="q-job-detail-label">FINISHED</span>
|
||||
<span class="q-job-detail-value" ng-bind="humanTime(job.finishedOn)"></span>
|
||||
<span class="q-job-detail-value" v-text="humanTime(job?.finishedOn)"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.attemptsMade">
|
||||
<div class="q-job-detail-item" v-if="job?.attemptsMade">
|
||||
<span class="q-job-detail-label">ATTEMPTS</span>
|
||||
<span class="q-job-detail-value" ng-bind="job.attemptsMade"></span>
|
||||
<span class="q-job-detail-value" v-text="job?.attemptsMade"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.progress && job.progress.status">
|
||||
<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" ng-bind="job.progress.status"></span>
|
||||
<span class="q-job-detail-value" v-text="job?.progress?.status"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="jobProgressPct(job) !== null">
|
||||
<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" ng-bind="jobProgressPct(job) + '%'"></span>
|
||||
<span class="q-job-detail-value" v-text="jobProgressPct(job) + '%'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="job.failedReason" class="q-job-detail-error">
|
||||
<div class="q-job-detail-error" v-if="job?.failedReason">
|
||||
<span class="q-job-detail-label">ERROR</span>
|
||||
<div class="q-error-reason" ng-bind="job.failedReason"></div>
|
||||
<div class="q-error-reason" v-text="job?.failedReason"></div>
|
||||
</div>
|
||||
<div ng-if="job.stacktrace.length">
|
||||
<div v-if="job?.stacktrace?.length">
|
||||
<span class="q-job-detail-label">STACKTRACE</span>
|
||||
<pre ng-repeat="stack in job.stacktrace track by $index" class="q-error-stack"><code ng-bind="stack"></code></pre>
|
||||
<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" ng-click="retryJob(job)" ng-if="job._state == 'failed'"><i class="fas fa-sync"></i> Retry</button>
|
||||
<button class="btn btn-sm" ng-click="removeJob(job)"><i class="fas fa-trash-alt"></i> Remove</button>
|
||||
<a class="btn btn-sm" target="_blank" ng-href="/r/{{job.id}}"><i class="fas fa-external-link-alt"></i> View repo</a>
|
||||
<button class="btn btn-sm" v-if="job?._state == 'failed'" @click="retryJob(job)"><i class="fas fa-sync"></i> Retry</button>
|
||||
<button class="btn btn-sm" @click="removeJob(job)"><i class="fas fa-trash-alt"></i> Remove</button>
|
||||
<a class="btn btn-sm" target="_blank" :href="safeUrl("/r/" + (job?.id))"><i class="fas fa-external-link-alt"></i> View repo</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -215,10 +215,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="paper-table-empty" ng-if="filteredJobs().length == 0" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);">
|
||||
<div class="paper-table-empty" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);" v-if="filteredJobs().length == 0">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span ng-if="!query.search">No jobs in the {{selectedQueue}} queue.</span>
|
||||
<span ng-if="query.search">No jobs match the current filters.</span>
|
||||
<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>
|
||||
|
||||
@@ -12,143 +12,131 @@
|
||||
</nav>
|
||||
|
||||
<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>
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</span>
|
||||
<span class="summary-meta">{{ fmt?.humanFileSize(totalSize) }} on disk</span>
|
||||
<span class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></span>
|
||||
<span class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing') + statusCountFor('download')) }}</span></span>
|
||||
<span class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></span>
|
||||
<span class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1">Expired <span class="count">{{ fmt?.number(statusCountFor('expired') + statusCountFor('expiring')) }}</span></span>
|
||||
<span class="summary-pill" title="Toggle removed filter" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1">Removed <span class="count">{{ fmt?.number(statusCountFor('removed') + statusCountFor('removing')) }}</span></span>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" ng-if="fetchError" style="margin: 8px 0;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{fetchError}}
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8">
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8" v-form="viewState">
|
||||
<!-- Row 1: search + scoped inputs + headline actions -->
|
||||
<div class="admin-filter-row">
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
type="search"
|
||||
class="form-control"
|
||||
aria-label="Search repositories"
|
||||
placeholder="Search repoId, source repo, error message…"
|
||||
autocomplete="off"
|
||||
ng-model="query.search"
|
||||
/>
|
||||
<span class="admin-search-hint" ng-if="!query.search">/</span>
|
||||
<input type="search" class="form-control" aria-label="Search repositories" placeholder="Search repoId, source repo, error message…" autocomplete="off" v-field="{ state: viewState, set: value => { query.search = value }, value: query?.search, form: null, options: {} }">
|
||||
<span class="admin-search-hint" v-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-inline"><label>Owner</label><input type="text" class="form-control form-control-sm" placeholder="username" v-field="{ state: viewState, set: value => { query.owner = value }, value: query?.owner, form: null, options: {} }"></span>
|
||||
<span class="admin-filter-inline"><label>Conference</label><input type="text" class="form-control form-control-sm" placeholder="ID" v-field="{ state: viewState, set: value => { query.conference = value }, value: query?.conference, form: null, options: {} }"></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>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @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>
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :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" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: appears only when there are active filter chips -->
|
||||
<div class="admin-filter-row" ng-if="chips.length">
|
||||
<div class="admin-filter-row" v-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 class="admin-active-chip" v-for="(chip, index) in chips" :key="chip?.key">
|
||||
<span class="key">{{ chip?.label }}</span>
|
||||
<span>{{ chip?.value }}</span>
|
||||
<button type="button" aria-label="Remove filter" @click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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 class="bulk-bar" v-if="selectedCount() > 0">
|
||||
<span><strong>{{ selectedCount() }}</strong> selected</span>
|
||||
<button class="btn btn-sm" type="button" @click="bulkRefresh()"><i class="fas fa-sync"></i> Force refresh</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @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" style="width: 28px;">
|
||||
<input type="checkbox" ng-click="selectAllOnPage()" ng-checked="allSelected" aria-label="Select all on page" />
|
||||
<input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
|
||||
</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"><span class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <i class="fas" :class="sortIcon('source.repositoryName')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <i class="fas" :class="sortIcon('pageView')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <i class="fas" :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', 'repo-error': repo.status == 'error', 'row-selected': selected[repo.repoId]}"
|
||||
ng-repeat="repo in repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories"
|
||||
>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error', 'row-selected': selected[repo?.repoId]}">
|
||||
<div role="cell" style="width: 28px;">
|
||||
<input type="checkbox" ng-model="selected[repo.repoId]" aria-label="Select repository" />
|
||||
<input type="checkbox" aria-label="Select repository" v-field="{ state: viewState, set: value => { selected[repo.repoId] = value }, value: selected[repo?.repoId], form: null, options: {} }">
|
||||
</div>
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" target="_blank" ng-href="/r/{{repo.repoId}}" ng-bind="repo.repoId"></a>
|
||||
<a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId))"></a>
|
||||
<div class="anon-sub">
|
||||
<a href="https://github.com/{{repo.source.repositoryName}}/" ng-bind="repo.source.repositoryName"></a><span ng-if="repo.options.update"> · <a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.branch}}" ng-bind="repo.source.branch"></a></span><span ng-if="!repo.options.update"> · @<a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.commit}}" ng-bind="repo.source.commit.substring(0, 8)"></a></span><span ng-if="::repo.conference"> · <i class="fas fa-chalkboard-teacher"></i> {{repo.conference}}</span><span> · {{::repo.size.storage | humanFileSize}}</span><span> · {{::repo.options.terms.length | number}} terms</span>
|
||||
<a v-text="repo?.source?.repositoryName" :href="safeUrl("https://github.com/" + (repo?.source?.repositoryName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<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 class="status-dot" :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 v-text="fmt?.title(repo?.status)"></span>
|
||||
</span>
|
||||
<span class="status-sub" ng-if="repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage | statusMsg"></span>
|
||||
<span class="status-sub" v-if="repo?.statusMessage" v-text="fmt?.statusMsg(repo?.statusMessage)" :title="(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>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView || 0)"></div>
|
||||
<div class="cell-expires" role="cell" v-text="fmt?.humanTime(repo?.anonymizeDate)"></div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<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>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><i class="fa fa-eye"></i> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><i class="fas fa-globe"></i> View page</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="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>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="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>
|
||||
<a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" ng-if="filteredRepositories.length == 0">
|
||||
<div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<span>No repositories match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :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;" 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">
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" aria-label="Page" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
<span>of {{ totalPage }}</span>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :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">
|
||||
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value => { query.limit = value }, value: query?.limit, form: null, options: {} }">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<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">{{userInfo.username || 'User'}}</h1>
|
||||
<div class="paper-crumbs"><a href="/admin/users">Users</a> / <span class="here">{{ userInfo?.username || 'Profile' }}</span></div>
|
||||
<h1 class="paper-page-title">{{ userInfo?.username || 'User' }}</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
@@ -11,119 +11,119 @@
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="user-detail-card" ng-if="userInfo">
|
||||
<div class="user-detail-card" v-if="userInfo">
|
||||
<div class="user-header">
|
||||
<img ng-src="{{userInfo.photo}}" ng-if="userInfo.photo" width="56" height="56" />
|
||||
<img width="56" height="56" v-if="userInfo?.photo" :src="safeUrl((userInfo?.photo))">
|
||||
<div>
|
||||
<h1>
|
||||
{{userInfo.username}}
|
||||
{{ userInfo?.username }}
|
||||
<span class="status-dot-wrap">
|
||||
<span class="status-dot" ng-class="{'status-ready': userInfo.status == 'active', 'status-removed': userInfo.status != 'active'}"></span>
|
||||
<span ng-bind="userInfo.status | title"></span>
|
||||
<span class="status-dot" :class="{'status-ready': userInfo?.status == 'active', 'status-removed': userInfo?.status != 'active'}"></span>
|
||||
<span v-text="fmt?.title(userInfo?.status)"></span>
|
||||
</span>
|
||||
<span class="type-badge type-repo" ng-if="userInfo.isAdmin">Admin</span>
|
||||
<span class="type-badge type-repo" v-if="userInfo?.isAdmin">Admin</span>
|
||||
</h1>
|
||||
<div class="user-actions" style="margin-top: 4px;">
|
||||
<button class="btn btn-sm text-danger" ng-if="userInfo.status !== 'banned'" ng-click="banUser()"><i class="fas fa-ban"></i> Ban</button>
|
||||
<button class="btn btn-sm" ng-if="userInfo.status === 'banned' || userInfo.status === 'removed'" ng-click="activateUser()"><i class="fas fa-check-circle"></i> Activate</button>
|
||||
<button class="btn btn-sm" ng-if="!userInfo.isAdmin" ng-click="promoteUser()"><i class="fas fa-user-shield"></i> Promote to admin</button>
|
||||
<button class="btn btn-sm text-danger" ng-if="userInfo.isAdmin" ng-click="demoteUser()"><i class="fas fa-user-minus"></i> Remove admin</button>
|
||||
<button class="btn btn-sm text-danger" v-if="userInfo?.status !== 'banned'" @click="banUser()"><i class="fas fa-ban"></i> Ban</button>
|
||||
<button class="btn btn-sm" v-if="userInfo?.status === 'banned' || userInfo?.status === 'removed'" @click="activateUser()"><i class="fas fa-check-circle"></i> Activate</button>
|
||||
<button class="btn btn-sm" v-if="!userInfo?.isAdmin" @click="promoteUser()"><i class="fas fa-user-shield"></i> Promote to admin</button>
|
||||
<button class="btn btn-sm text-danger" v-if="userInfo?.isAdmin" @click="demoteUser()"><i class="fas fa-user-minus"></i> Remove admin</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-detail-grid">
|
||||
<div class="detail-label">ID</div>
|
||||
<div class="detail-value">{{userInfo._id}}</div>
|
||||
<div class="detail-value">{{ userInfo?._id }}</div>
|
||||
|
||||
<div class="detail-label">Email</div>
|
||||
<div class="detail-value">{{userInfo.emails[0].email}}</div>
|
||||
<div class="detail-value">{{ userInfo?.emails?.[0]?.email }}</div>
|
||||
|
||||
<div class="detail-label">Access token</div>
|
||||
<div class="detail-value" style="font-family: var(--font-mono); font-size: 0.85rem;">{{userInfo.accessTokens.github}}</div>
|
||||
<div class="detail-value" style="font-family: var(--font-mono); font-size: 0.85rem;">{{ userInfo?.accessTokens?.github }}</div>
|
||||
|
||||
<div class="detail-label">GitHub</div>
|
||||
<div class="detail-value">
|
||||
<a ng-href="https://github.com/{{userInfo.username}}" target="_blank">
|
||||
<i class="fab fa-github"></i> {{userInfo.username}}
|
||||
<a target="_blank" :href="safeUrl("https://github.com/" + (userInfo?.username))">
|
||||
<i class="fab fa-github"></i> {{ userInfo?.username }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="detail-label">Created</div>
|
||||
<div class="detail-value">{{userInfo.dateOfEntry | humanTime}}</div>
|
||||
<div class="detail-value">{{ fmt?.humanTime(userInfo?.dateOfEntry) }}</div>
|
||||
|
||||
<div class="detail-label">Last connection</div>
|
||||
<div class="detail-value">
|
||||
<span ng-if="userInfo.accessTokenDates.github">{{userInfo.accessTokenDates.github | humanTime}}</span>
|
||||
<span class="text-muted" ng-if="!userInfo.accessTokenDates.github">never</span>
|
||||
<span v-if="userInfo?.accessTokenDates?.github">{{ fmt?.humanTime(userInfo?.accessTokenDates?.github) }}</span>
|
||||
<span class="text-muted" v-if="!userInfo?.accessTokenDates?.github">never</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-label">GitHub repos</div>
|
||||
<div class="detail-value">
|
||||
{{userInfo.repositories.length}} repositories
|
||||
<button class="btn btn-sm ml-2" ng-click="showRepos = !showRepos">
|
||||
{{showRepos ? 'Hide' : 'Show'}}
|
||||
{{ userInfo?.repositories?.length }} repositories
|
||||
<button class="btn btn-sm ml-2" @click="showRepos = !showRepos">
|
||||
{{ showRepos ? 'Hide' : 'Show' }}
|
||||
</button>
|
||||
<button class="btn btn-sm ml-1" ng-click="getGitHubRepositories()">
|
||||
<button class="btn btn-sm ml-1" @click="getGitHubRepositories()">
|
||||
<i class="fas fa-sync"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="showRepos" style="margin-top: 20px">
|
||||
<div style="margin-top: 20px" v-if="showRepos">
|
||||
<div class="paper-section-eyebrow">GitHub repositories</div>
|
||||
<div class="paper-table w-100" style="margin-top: 10px;">
|
||||
<div class="paper-table-row" ng-repeat="repo in userInfo.repositories" style="grid-template-columns: 1fr 160px;">
|
||||
<div class="paper-table-row" style="grid-template-columns: 1fr 160px;" v-for="(repo, index) in userInfo?.repositories">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<div class="anon-text">
|
||||
<span class="repo-name" ng-bind="repo.name"></span>
|
||||
<span class="repo-name" v-text="repo?.name"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<i class="fas fa-database"></i> {{::repo.size | humanFileSize}}
|
||||
<i class="fas fa-database"></i> {{ fmt?.humanFileSize(repo?.size) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section-header" ng-if="userInfo && userInfo.isAdmin && user && user.username == userInfo.username">
|
||||
<div class="admin-section-header" v-if="userInfo && userInfo?.isAdmin && user && user?.username == userInfo?.username">
|
||||
<h2><i class="fas fa-key"></i> API tokens</h2>
|
||||
<span class="section-count">{{tokens.length}}</span>
|
||||
<span class="section-count">{{ tokens?.length }}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="userInfo && userInfo.isAdmin && user && user.username == userInfo.username" class="user-detail-card">
|
||||
<div class="user-detail-card" v-if="userInfo && userInfo?.isAdmin && user && user?.username == userInfo?.username">
|
||||
<p class="paper-page-lede">Personal API tokens for this admin account. Send as <code>Authorization: Bearer <token></code> to authenticate without GitHub OAuth (useful for development).</p>
|
||||
|
||||
<form ng-submit="createToken()" class="d-flex" style="gap: 8px; margin-bottom: 12px;">
|
||||
<input type="text" class="form-control" ng-model="tokenForm.name" placeholder="Token name (e.g. dev-laptop)" required />
|
||||
<form class="d-flex" style="gap: 8px; margin-bottom: 12px;" v-form="viewState" @submit.prevent="submitForm($event, () => { createToken() })">
|
||||
<input type="text" class="form-control" placeholder="Token name (e.g. dev-laptop)" required v-field="{ state: viewState, set: value => { tokenForm.name = value }, value: tokenForm?.name, form: null, options: {} }">
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-plus"></i> Generate</button>
|
||||
</form>
|
||||
|
||||
<div ng-if="tokenForm.plaintext" class="alert alert-warning" role="alert">
|
||||
<div class="alert alert-warning" role="alert" v-if="tokenForm?.plaintext">
|
||||
<strong>Copy this token now — it will not be shown again:</strong>
|
||||
<pre style="white-space: pre-wrap; word-break: break-all; margin: 8px 0 0; font-family: var(--font-mono); font-size: 0.85rem;">{{tokenForm.plaintext}}</pre>
|
||||
<button class="btn btn-sm" ng-click="tokenForm.plaintext = null">Dismiss</button>
|
||||
<pre style="white-space: pre-wrap; word-break: break-all; margin: 8px 0 0; font-family: var(--font-mono); font-size: 0.85rem;">{{ tokenForm?.plaintext }}</pre>
|
||||
<button class="btn btn-sm" @click="tokenForm.plaintext = null">Dismiss</button>
|
||||
</div>
|
||||
|
||||
<div class="paper-table w-100" ng-if="tokens.length">
|
||||
<div class="paper-table w-100" v-if="tokens?.length">
|
||||
<div class="paper-table-head" role="row" style="grid-template-columns: 1fr 200px 200px 80px;">
|
||||
<div role="columnheader">Name</div>
|
||||
<div role="columnheader">Created</div>
|
||||
<div role="columnheader">Last used</div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div class="paper-table-row" role="row" ng-repeat="t in tokens" style="grid-template-columns: 1fr 200px 200px 80px;">
|
||||
<div role="cell" ng-bind="t.name"></div>
|
||||
<div role="cell" ng-bind="t.createdAt | humanTime"></div>
|
||||
<div role="cell"><span ng-if="t.lastUsedAt">{{t.lastUsedAt | humanTime}}</span><span ng-if="!t.lastUsedAt" class="text-muted">never</span></div>
|
||||
<div class="paper-table-row" role="row" style="grid-template-columns: 1fr 200px 200px 80px;" v-for="(t, index) in tokens">
|
||||
<div role="cell" v-text="t?.name"></div>
|
||||
<div role="cell" v-text="fmt?.humanTime(t?.createdAt)"></div>
|
||||
<div role="cell"><span v-if="t?.lastUsedAt">{{ fmt?.humanTime(t?.lastUsedAt) }}</span><span class="text-muted" v-if="!t?.lastUsedAt">never</span></div>
|
||||
<div role="cell">
|
||||
<button class="btn btn-sm text-danger" ng-click="revokeToken(t)" title="Revoke"><i class="fas fa-trash-alt"></i></button>
|
||||
<button class="btn btn-sm text-danger" title="Revoke" @click="revokeToken(t)"><i class="fas fa-trash-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" ng-if="!tokens.length">
|
||||
<div class="paper-table-empty" v-if="!tokens?.length">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<span>No tokens yet.</span>
|
||||
</div>
|
||||
@@ -131,102 +131,90 @@
|
||||
|
||||
<div class="admin-section-header">
|
||||
<h2><i class="fas fa-code-branch"></i> Anonymized repositories</h2>
|
||||
<span class="section-count">{{repositories.length}}</span>
|
||||
<span class="section-count">{{ repositories?.length }}</span>
|
||||
</div>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{repositories.length | number}}</span>
|
||||
<span class="summary-pill ok" ng-class="{active: filters.status.ready === false}" ng-click="filters.status.ready = !filters.status.ready" title="Toggle ready filter">Ready <span class="count">{{statusCountFor('ready') | number}}</span></span>
|
||||
<span class="summary-pill warn" ng-class="{active: filters.status.preparing === false}" ng-click="filters.status.preparing = !filters.status.preparing" title="Toggle preparing filter">Preparing <span class="count">{{statusCountFor('preparing') | number}}</span></span>
|
||||
<span class="summary-pill error" ng-class="{active: filters.status.error === false}" ng-click="filters.status.error = !filters.status.error" title="Toggle errored filter">Errored <span class="count">{{statusCountFor('error') | number}}</span></span>
|
||||
<span class="summary-pill" ng-class="{active: filters.status.expired === false}" ng-click="filters.status.expired = !filters.status.expired" title="Toggle expired filter">Expired <span class="count">{{statusCountFor('expired') | number}}</span></span>
|
||||
<span class="summary-pill" ng-class="{active: filters.status.removed === false}" ng-click="filters.status.removed = !filters.status.removed" title="Toggle removed filter">Removed <span class="count">{{statusCountFor('removed') | number}}</span></span>
|
||||
<span class="summary-total">{{ fmt?.number(repositories?.length) }}</span>
|
||||
<span class="summary-pill ok" title="Toggle ready filter" :class="{active: filters?.status?.ready === false}" @click="filters.status.ready = !filters.status.ready">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></span>
|
||||
<span class="summary-pill warn" title="Toggle preparing filter" :class="{active: filters?.status?.preparing === false}" @click="filters.status.preparing = !filters.status.preparing">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></span>
|
||||
<span class="summary-pill error" title="Toggle errored filter" :class="{active: filters?.status?.error === false}" @click="filters.status.error = !filters.status.error">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></span>
|
||||
<span class="summary-pill" title="Toggle expired filter" :class="{active: filters?.status?.expired === false}" @click="filters.status.expired = !filters.status.expired">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></span>
|
||||
<span class="summary-pill" title="Toggle removed filter" :class="{active: filters?.status?.removed === false}" @click="filters.status.removed = !filters.status.removed">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></span>
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8">
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8" v-form="viewState">
|
||||
<div class="admin-filter-row">
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
type="search"
|
||||
class="form-control"
|
||||
aria-label="Search repositories"
|
||||
placeholder="Search repoId, source repo, error message…"
|
||||
autocomplete="off"
|
||||
ng-model="search"
|
||||
/>
|
||||
<input type="search" class="form-control" aria-label="Search repositories" placeholder="Search repoId, source repo, error message…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<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>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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 class="bulk-bar" v-if="selectedCount() > 0">
|
||||
<span><strong>{{ selectedCount() }}</strong> selected</span>
|
||||
<button class="btn btn-sm" type="button" @click="bulkRefresh()"><i class="fas fa-sync"></i> Force refresh</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @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" style="width: 28px;">
|
||||
<input type="checkbox" ng-click="selectAllOnPage()" ng-checked="allSelected" aria-label="Select all on page" />
|
||||
<input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
|
||||
</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"><span class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <i class="fas" :class="sortIcon('source.repositoryName')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <i class="fas" :class="sortIcon('pageView')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <i class="fas" :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', 'repo-error': repo.status == 'error', 'row-selected': selected[repo.repoId]}"
|
||||
ng-repeat="repo in repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories"
|
||||
>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error', 'row-selected': selected[repo?.repoId]}">
|
||||
<div role="cell" style="width: 28px;">
|
||||
<input type="checkbox" ng-model="selected[repo.repoId]" aria-label="Select repository" />
|
||||
<input type="checkbox" aria-label="Select repository" v-field="{ state: viewState, set: value => { selected[repo.repoId] = value }, value: selected[repo?.repoId], form: null, options: {} }">
|
||||
</div>
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" target="_blank" ng-href="/r/{{repo.repoId}}" ng-bind="repo.repoId"></a>
|
||||
<a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId))"></a>
|
||||
<div class="anon-sub">
|
||||
<a href="https://github.com/{{repo.source.fullName}}/" ng-bind="repo.source.fullName"></a><span ng-if="repo.options.update"> · <a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.branch}}" ng-bind="repo.source.branch"></a></span><span ng-if="!repo.options.update"> · @<a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.commit}}" ng-bind="repo.source.commit.substring(0, 8)"></a></span><span ng-if="::repo.conference"> · <i class="fas fa-chalkboard-teacher"></i> {{repo.conference}}</span><span> · {{::repo.size.storage | humanFileSize}}</span><span> · {{::repo.options.terms.length | number}} terms</span>
|
||||
<a v-text="repo?.source?.fullName" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<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 class="status-dot" :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 v-text="fmt?.title(repo?.status)"></span>
|
||||
</span>
|
||||
<span class="status-sub" ng-if="repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage | statusMsg"></span>
|
||||
<span class="status-sub" v-if="repo?.statusMessage" v-text="fmt?.statusMsg(repo?.statusMessage)" :title="(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>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView || 0)"></div>
|
||||
<div class="cell-expires" role="cell" v-text="fmt?.humanTime(repo?.anonymizeDate)"></div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<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>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><i class="fa fa-eye"></i> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><i class="fas fa-globe"></i> View page</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="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>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="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>
|
||||
<a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" ng-if="filteredRepositories.length == 0">
|
||||
<div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<span>No repositories match the current filters.</span>
|
||||
</div>
|
||||
|
||||
@@ -12,90 +12,85 @@
|
||||
</nav>
|
||||
|
||||
<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>
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</span>
|
||||
<span class="summary-pill ok" :class="{active: query?.status == 'active'}" @click="query.status = query.status == 'active' ? '' : 'active'; query.page = 1">Active <span class="count">{{ fmt?.number(statusCountFor('active')) }}</span></span>
|
||||
<span class="summary-pill error" :class="{active: query?.status == 'banned'}" @click="query.status = query.status == 'banned' ? '' : 'banned'; query.page = 1">Banned <span class="count">{{ fmt?.number(statusCountFor('banned')) }}</span></span>
|
||||
<span class="summary-pill" :class="{active: query?.status == 'removed'}" @click="query.status = query.status == 'removed' ? '' : 'removed'; query.page = 1">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></span>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" ng-if="fetchError" style="margin: 8px 0;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{fetchError}}
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Users" accept-charset="UTF-8">
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Users" accept-charset="UTF-8" v-form="viewState">
|
||||
<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>
|
||||
<input type="search" class="form-control" aria-label="Search users" placeholder="Search username or email…" autocomplete="off" v-field="{ state: viewState, set: value => { query.search = value }, value: query?.search, form: null, options: {} }">
|
||||
<span class="admin-search-hint" v-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>
|
||||
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value => { query.role = value }, value: query?.role, form: null, options: {} }"><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>
|
||||
<button class="btn btn-sm" type="button" @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>
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :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" @click="query.page = Math.min(totalPage, query.page + 1)" :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-filter-row" v-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 class="admin-active-chip" v-for="(chip, index) in chips" :key="chip?.key">
|
||||
<span class="key">{{ chip?.label }}</span>
|
||||
<span>{{ chip?.value }}</span>
|
||||
<button type="button" @click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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 class="bulk-bar" v-if="selectedCount() > 0">
|
||||
<span><strong>{{ selectedCount() }}</strong> selected</span>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkBan()"><i class="fas fa-ban"></i> Ban</button>
|
||||
<button class="btn btn-sm" type="button" @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" style="width: 28px;">
|
||||
<input type="checkbox" ng-click="selectAllOnPage()" ng-checked="allSelected" aria-label="Select all on page" />
|
||||
<input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
|
||||
</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"><span class="sortable" :class="{active: query?.sort == 'username'}" @click="sortBy('username')">User <i class="fas" :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"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :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 class="paper-table-row admin-users-row" role="row" v-for="(u, index) in filteredUsers" :class="{'row-selected': selected[u?.username]}">
|
||||
<div role="cell" style="width: 28px;">
|
||||
<input type="checkbox" ng-model="selected[u.username]" aria-label="Select user" />
|
||||
<input type="checkbox" aria-label="Select user" v-field="{ state: viewState, set: value => { selected[u.username] = value }, value: selected[u?.username], form: null, options: {} }">
|
||||
</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;" />
|
||||
<img width="28" height="28" class="rounded-circle" style="flex-shrink:0;" v-if="u?.photo" :src="safeUrl((u?.photo))">
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" ng-href="/admin/users/{{u.username}}" ng-bind="u.username"></a>
|
||||
<a class="repo-name" v-text="u?.username" :href="safeUrl("/admin/users/" + (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.dateOfEntry"> · Joined {{u.dateOfEntry | humanTime}}</span>
|
||||
<span v-if="u?.emails[0].email">{{ u?.emails[0].email }}</span><span v-if="u?.emails[0].email"> · </span><a target="_blank" :href="safeUrl("https://github.com/" + (u?.username))"><i class="fab fa-github"></i> {{ u?.username }}</a><span v-if="u?.dateOfEntry"> · Joined {{ fmt?.humanTime(u?.dateOfEntry) }}</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>
|
||||
<a title="Show this user's repositories" v-text="fmt?.number((u?.repoCount || 0))" :href="safeUrl("/admin/?owner=" + (u?.username))"></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>
|
||||
<span class="status-dot" :class="{'status-ready': u?.status == 'active', 'status-removed': u?.status == 'removed' || u?.status == 'banned'}"></span>
|
||||
<span v-text="fmt?.title(u?.status)"></span>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<span ng-if="u.isAdmin" class="type-badge type-repo">Admin</span>
|
||||
<span ng-if="!u.isAdmin" class="empty-dash">—</span>
|
||||
<span class="type-badge type-repo" v-if="u?.isAdmin">Admin</span>
|
||||
<span class="empty-dash" v-if="!u?.isAdmin">—</span>
|
||||
</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
@@ -103,32 +98,32 @@
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</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>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/users/" + (u?.username))"><i class="far fa-eye"></i> View details</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/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>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="u?.status == 'active'" @click.prevent="banUser(u)"><i class="fas fa-ban"></i> Ban</a>
|
||||
<a class="dropdown-item" href="#" v-show="u?.status == 'removed' || u?.status == 'banned'" @click.prevent="activateUser(u)"><i class="fas fa-check-circle"></i> Activate</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" ng-if="filteredUsers.length == 0">
|
||||
<div class="paper-table-empty" v-if="filteredUsers?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<span>No users match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
<span>of {{ totalPage }}</span>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :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>
|
||||
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value => { query.limit = value }, value: query?.limit, form: null, options: {} }"><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