mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-09-18 16:42:16 +02:00
refactor: migrate the frontend from AngularJS to Vue 3
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<div class="paper-empty-inner">
|
||||
<div class="paper-eyebrow">Error · 404</div>
|
||||
<h1 class="paper-empty-title">
|
||||
This link<br /><em>isn’t here.</em>
|
||||
This link<br><em>isn’t here.</em>
|
||||
</h1>
|
||||
<p class="paper-empty-lede">
|
||||
The anonymous mirror you’re looking for has expired, been removed
|
||||
|
||||
@@ -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>
|
||||
|
||||
+145
-190
@@ -1,6 +1,6 @@
|
||||
<div class="anonymize-page h-100">
|
||||
<!-- ===== STATE 1: No URL — centered input ===== -->
|
||||
<div class="anonymize-landing" ng-hide="sourceUrl">
|
||||
<div class="anonymize-landing" v-show="!(sourceUrl)">
|
||||
<div class="anonymize-landing-inner">
|
||||
<div class="paper-crumbs">My work / <span class="here">New anonymization</span></div>
|
||||
<h1 class="paper-page-title">New <em>anonymization</em></h1>
|
||||
@@ -10,15 +10,7 @@
|
||||
</p>
|
||||
<div class="form-group mt-4 mb-2">
|
||||
<label class="paper-field-label" for="sourceUrl-landing">Source URL</label>
|
||||
<input
|
||||
id="sourceUrl-landing"
|
||||
type="text"
|
||||
class="form-control form-control-lg"
|
||||
ng-model="sourceUrl"
|
||||
placeholder="https://github.com/owner/repository"
|
||||
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
|
||||
ng-change="urlSelected()"
|
||||
/>
|
||||
<input id="sourceUrl-landing" type="text" class="form-control form-control-lg" placeholder="https://github.com/owner/repository" v-field="{ state: viewState, set: value => { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }">
|
||||
</div>
|
||||
<small class="form-text" style="color: var(--ink-muted);">
|
||||
Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted.
|
||||
@@ -27,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ===== STATE 2: URL provided — form (left) + preview (right) ===== -->
|
||||
<div class="anonymize-workspace" ng-show="sourceUrl">
|
||||
<div class="anonymize-workspace" v-show="sourceUrl">
|
||||
<header class="anonymize-topbar">
|
||||
<div class="anonymize-topbar-inner">
|
||||
<div class="paper-crumbs">
|
||||
@@ -36,11 +28,11 @@
|
||||
</div>
|
||||
<div class="anonymize-topbar-head">
|
||||
<h1 class="paper-page-title anonymize-topbar-title">
|
||||
<span ng-if="!isUpdate">New <em>anonymization</em></span>
|
||||
<span ng-if="isUpdate">Edit <em>anonymization</em></span>
|
||||
<span v-if="!isUpdate">New <em>anonymization</em></span>
|
||||
<span v-if="isUpdate">Edit <em>anonymization</em></span>
|
||||
</h1>
|
||||
<span class="type-badge" ng-show="detectedType" ng-class="{'type-repo': detectedType === 'repo', 'type-pr': detectedType === 'pr', 'type-gist': detectedType === 'gist'}">
|
||||
{{detectedType === 'repo' ? 'Repo' : detectedType === 'pr' ? 'PR' : 'Gist'}}
|
||||
<span class="type-badge" v-show="detectedType" :class="{'type-repo': detectedType === 'repo', 'type-pr': detectedType === 'pr', 'type-gist': detectedType === 'gist'}">
|
||||
{{ detectedType === 'repo' ? 'Repo' : detectedType === 'pr' ? 'PR' : 'Gist' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,47 +41,37 @@
|
||||
<div class="anonymize-split">
|
||||
<!-- Form column (left) -->
|
||||
<div class="anonymize-form-col overflow-auto">
|
||||
<form class="form needs-validation paper-settings-main" name="anonymize" novalidate>
|
||||
<form class="form needs-validation paper-settings-main" name="anonymize" novalidate v-form="viewState">
|
||||
|
||||
<section class="paper-settings-section">
|
||||
<div class="paper-section-eyebrow">Source</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="sourceUrl">GitHub URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="sourceUrl"
|
||||
id="sourceUrl"
|
||||
ng-class="{'is-invalid': anonymize.sourceUrl.$invalid}"
|
||||
ng-model="sourceUrl"
|
||||
placeholder="Paste a GitHub repo or pull request URL"
|
||||
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
|
||||
ng-change="urlSelected()"
|
||||
/>
|
||||
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.github">
|
||||
<input type="text" class="form-control" name="sourceUrl" id="sourceUrl" :disabled="isUpdate && detectedType !== 'repo'" placeholder="Paste a GitHub repo or pull request URL" v-field="{ state: viewState, set: value => { sourceUrl = value }, value: sourceUrl, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }" :class="{'is-invalid': anonymize?.sourceUrl?.invalid}">
|
||||
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.github">
|
||||
Please provide a valid GitHub URL.
|
||||
</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.access">
|
||||
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.access">
|
||||
Not accessible. The organization may restrict access.
|
||||
</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.missing">
|
||||
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.missing">
|
||||
Does not exist or is not accessible.
|
||||
</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.used">
|
||||
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.used">
|
||||
Already anonymized.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="detectedType === 'repo'" class="form-grid-2">
|
||||
<div class="form-grid-2" v-show="detectedType === 'repo'">
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="branch">Branch</label>
|
||||
<div class="input-group">
|
||||
<select class="form-control" id="branch" name="branch" ng-model="source.branch">
|
||||
<option ng-repeat="b in branches" ng-bind="b.name" value="{{b.name}}"></option>
|
||||
<select class="form-control" id="branch" name="branch" v-field="{ state: viewState, set: value => { source.branch = value }, value: source?.branch, form: "anonymize", options: {} }">
|
||||
<option v-for="(b, index) in branches" v-text="b?.name" :value="(b?.name)"></option>
|
||||
</select>
|
||||
<div class="input-group-append">
|
||||
<button class="btn" type="button" ng-click="getBranches(true)" title="Refresh" data-toggle="tooltip" data-placement="bottom">
|
||||
<button class="btn" type="button" title="Refresh" data-toggle="tooltip" data-placement="bottom" @click="getBranches(true)">
|
||||
<i class="fas fa-sync" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -98,250 +80,241 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="commit">Commit</label>
|
||||
<input class="form-control" id="commit" name="commit" pattern="[a-fA-Z0-9]{6,}" ng-model="source.commit" required ng-class="{'is-invalid': anonymize.commit.$invalid}" />
|
||||
<div class="invalid-feedback" ng-show="anonymize.commit.$error.pattern || anonymize.commit.$error.required">
|
||||
<input class="form-control" id="commit" :disabled="options.update" name="commit" pattern="[a-fA-Z0-9]{6,}" required v-field="{ state: viewState, set: value => { source.commit = value }, value: source?.commit, form: "anonymize", options: {} }" :class="{'is-invalid': anonymize?.commit?.invalid}">
|
||||
<div class="invalid-feedback" v-show="anonymize?.commit?.errors?.pattern || anonymize?.commit?.errors?.required">
|
||||
The commit SHA is not valid.
|
||||
</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.commit.$error.exists">
|
||||
<div class="invalid-feedback" v-show="anonymize?.commit?.errors?.exists">
|
||||
This commit no longer exists in the repository. Click refresh to get the latest.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check" ng-show="detectedType">
|
||||
<input class="form-check-input" type="checkbox" id="update" name="update" ng-model="options.update" />
|
||||
<div class="form-check" v-show="detectedType">
|
||||
<input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value => { options.update = value }, value: options?.update, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="update">Auto-update from GitHub</label>
|
||||
<small class="form-text text-muted">Follow the branch and pull the latest commit automatically, at most once an hour.</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="paper-settings-section" ng-show="detectedType">
|
||||
<section class="paper-settings-section" v-show="detectedType">
|
||||
<div class="paper-section-eyebrow">Identity</div>
|
||||
|
||||
<div class="form-group" ng-show="detectedType === 'repo'">
|
||||
<div class="form-group" v-show="detectedType === 'repo'">
|
||||
<label class="paper-field-label" for="repoId">Anonymized repository ID</label>
|
||||
<input type="text" class="form-control" name="repoId" id="repoId" ng-class="{'is-invalid': anonymize.repoId.$invalid}" ng-model="repoId" ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }" />
|
||||
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/r/{{repoId}}</code>.</small>
|
||||
<div class="invalid-feedback" ng-show="anonymize.repoId.$error.format">ID can only contain letters and numbers.</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.repoId.$error.used">{{repoId}} is already used.</div>
|
||||
<input type="text" class="form-control" name="repoId" id="repoId" :disabled="isUpdate" v-field="{ state: viewState, set: value => { repoId = value }, value: repoId, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.repoId?.invalid}">
|
||||
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/r/{{ repoId }}</code>.</small>
|
||||
<div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.format">ID can only contain letters and numbers.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.used">{{ repoId }} is already used.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="detectedType === 'pr'">
|
||||
<div class="form-group" v-show="detectedType === 'pr'">
|
||||
<label class="paper-field-label" for="pullRequestId">Anonymized pull request ID</label>
|
||||
<input type="text" class="form-control" name="pullRequestId" id="pullRequestId" ng-class="{'is-invalid': anonymize.pullRequestId.$invalid}" ng-model="pullRequestId" ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }" />
|
||||
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/pr/{{pullRequestId}}</code>.</small>
|
||||
<div class="invalid-feedback" ng-show="anonymize.pullRequestId.$error.format">ID can only contain letters and numbers.</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.pullRequestId.$error.used">{{pullRequestId}} is already used.</div>
|
||||
<input type="text" class="form-control" name="pullRequestId" id="pullRequestId" :disabled="isUpdate" v-field="{ state: viewState, set: value => { pullRequestId = value }, value: pullRequestId, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.pullRequestId?.invalid}">
|
||||
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/pr/{{ pullRequestId }}</code>.</small>
|
||||
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.format">ID can only contain letters and numbers.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.used">{{ pullRequestId }} is already used.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="detectedType === 'gist'">
|
||||
<div class="form-group" v-show="detectedType === 'gist'">
|
||||
<label class="paper-field-label" for="gistId">Anonymized gist ID</label>
|
||||
<input type="text" class="form-control" name="gistId" id="gistId" ng-class="{'is-invalid': anonymize.gistId.$invalid}" ng-model="gistId" ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }" />
|
||||
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/gist/{{gistId}}</code>.</small>
|
||||
<div class="invalid-feedback" ng-show="anonymize.gistId.$error.format">ID can only contain letters and numbers.</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.gistId.$error.used">{{gistId}} is already used.</div>
|
||||
<input type="text" class="form-control" name="gistId" id="gistId" :disabled="isUpdate" v-field="{ state: viewState, set: value => { gistId = value }, value: gistId, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.gistId?.invalid}">
|
||||
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/gist/{{ gistId }}</code>.</small>
|
||||
<div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.format">ID can only contain letters and numbers.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.used">{{ gistId }} is already used.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="conference">Conference <span class="paper-optional">(optional)</span></label>
|
||||
<input class="form-control" id="conference" name="conference" ng-model="conference" ng-model-options="{ debounce: { default: 800, blur: 0 }, updateOn: 'default blur' }" ng-class="{'is-invalid': anonymize.conference.$invalid}" />
|
||||
<small class="form-text text-muted" ng-show="conference_data">
|
||||
<a ng-href="{{conference_data.url}}" target="_blank">{{conference_data.name}}</a> expires {{conference_data.endDate | date}}.
|
||||
<input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value => { conference = value }, value: conference, form: "anonymize", options: { debounce: { default: 800, blur: 0 }, updateOn: 'default blur' } }" :class="{'is-invalid': anonymize?.conference?.invalid}">
|
||||
<small class="form-text text-muted" v-show="conference_data">
|
||||
<a target="_blank" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a> expires {{ fmt?.date(conference_data?.endDate) }}.
|
||||
</small>
|
||||
<div class="invalid-feedback" ng-show="anonymize.conference.$error.activated">The conference is not activated.</div>
|
||||
<small class="form-text text-muted" ng-show="!conference_data">Link to a conference to apply its shared defaults.</small>
|
||||
<div class="invalid-feedback" v-show="anonymize?.conference?.errors?.activated">The conference is not activated.</div>
|
||||
<small class="form-text text-muted" v-show="!conference_data">Link to a conference to apply its shared defaults.</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="paper-settings-section" ng-show="detectedType">
|
||||
<section class="paper-settings-section" v-show="detectedType">
|
||||
<div class="paper-section-eyebrow">Anonymization</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="terms">Terms to redact</label>
|
||||
<textarea class="form-control" id="terms" name="terms" rows="4" ng-model="terms" ng-model-options="{ debounce: 250 }" ng-class="{'is-invalid': anonymize.terms.$invalid}"></textarea>
|
||||
<small class="form-text text-muted">One term per line (regex allowed). Replaced by <code>{{site_options.ANONYMIZATION_MASK}}-[N]</code>, or use <code>term=>replacement</code> to pick your own (e.g. <code>Anonymous=>ABC</code>).</small>
|
||||
<div class="warning-feedback" ng-show="termsRegexWarning">Regex characters detected. Escape them if unintentional.</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.terms.$error.format">Terms are in an invalid format.</div>
|
||||
<textarea class="form-control" id="terms" name="terms" rows="4" v-field="{ state: viewState, set: value => { terms = value }, value: terms, form: "anonymize", options: { debounce: 250 } }" :class="{'is-invalid': anonymize?.terms?.invalid}"></textarea>
|
||||
<small class="form-text text-muted">One term per line (regex allowed). Replaced by <code>{{ site_options?.ANONYMIZATION_MASK }}-[N]</code>, or use <code>term=>replacement</code> to pick your own (e.g. <code>Anonymous=>ABC</code>).</small>
|
||||
<div class="warning-feedback" v-show="termsRegexWarning">Regex characters detected. Escape them if unintentional.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.terms?.errors?.format">Terms are in an invalid format.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="paper-settings-section" ng-show="detectedType">
|
||||
<section class="paper-settings-section" v-show="detectedType">
|
||||
<div class="paper-section-eyebrow">Display</div>
|
||||
|
||||
<div ng-show="detectedType === 'repo'">
|
||||
<div v-show="detectedType === 'repo'">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="link" name="link" ng-model="options.link" />
|
||||
<input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value => { options.link = value }, value: options?.link, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="link">Keep links</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="image" name="image" ng-model="options.image" />
|
||||
<input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value => { options.image = value }, value: options?.image, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="image">Display images</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" ng-model="options.pdf" />
|
||||
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value => { options.pdf = value }, value: options?.pdf, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="pdf">Display PDFs</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" ng-model="options.notebook" />
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.notebook = value }, value: options?.notebook, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="notebook">Display Notebooks</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="page" name="page" ng-model="options.page" ng-disabled="!details.hasPage || (details.pageSource && details.pageSource.branch !== source.branch)" />
|
||||
<input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value => { options.page = value }, value: options?.page, form: "anonymize", options: {} }" :disabled="!details?.hasPage || (details?.pageSource && details?.pageSource?.branch !== source?.branch)">
|
||||
<label class="form-check-label" for="page">GitHub Pages</label>
|
||||
<small class="form-text text-muted d-block" ng-show="!details.hasPage">
|
||||
{{ 'WARNINGS.page_not_enabled_on_repo' | translate }}
|
||||
<small class="form-text text-muted d-block" v-show="!details?.hasPage">
|
||||
{{ fmt?.translate('WARNINGS.page_not_enabled_on_repo') }}
|
||||
</small>
|
||||
<small class="form-text text-muted d-block" ng-show="details.hasPage && details.pageSource && details.pageSource.branch !== source.branch">
|
||||
{{ 'WARNINGS.page_branch_mismatch' | translate:{ pageBranch: details.pageSource.branch, selectedBranch: source.branch } }}
|
||||
<small class="form-text text-muted d-block" v-show="details?.hasPage && details?.pageSource && details?.pageSource?.branch !== source?.branch">
|
||||
{{ fmt?.translate('WARNINGS.page_branch_mismatch', { pageBranch: details?.pageSource?.branch, selectedBranch: source?.branch }) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="detectedType === 'gist'">
|
||||
<div v-show="detectedType === 'gist'">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="title-gist" name="title-gist" ng-model="options.title" />
|
||||
<input class="form-check-input" type="checkbox" id="title-gist" name="title-gist" v-field="{ state: viewState, set: value => { options.title = value }, value: options?.title, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="title-gist">Gist description</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="content-gist" name="content-gist" ng-model="options.content" />
|
||||
<input class="form-check-input" type="checkbox" id="content-gist" name="content-gist" v-field="{ state: viewState, set: value => { options.content = value }, value: options?.content, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="content-gist">File contents</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="comments-gist" name="comments-gist" ng-model="options.comments" />
|
||||
<input class="form-check-input" type="checkbox" id="comments-gist" name="comments-gist" v-field="{ state: viewState, set: value => { options.comments = value }, value: options?.comments, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="comments-gist">Comments</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="username-gist" name="username-gist" ng-model="options.username" />
|
||||
<input class="form-check-input" type="checkbox" id="username-gist" name="username-gist" v-field="{ state: viewState, set: value => { options.username = value }, value: options?.username, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="username-gist">Usernames</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="date-gist" name="date-gist" ng-model="options.date" />
|
||||
<input class="form-check-input" type="checkbox" id="date-gist" name="date-gist" v-field="{ state: viewState, set: value => { options.date = value }, value: options?.date, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="date-gist">Dates</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="origin-gist" name="origin-gist" ng-model="options.origin" />
|
||||
<input class="form-check-input" type="checkbox" id="origin-gist" name="origin-gist" v-field="{ state: viewState, set: value => { options.origin = value }, value: options?.origin, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="origin-gist">Source gist ID</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="detectedType === 'pr'">
|
||||
<div v-show="detectedType === 'pr'">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="title" name="title" ng-model="options.title" />
|
||||
<input class="form-check-input" type="checkbox" id="title" name="title" v-field="{ state: viewState, set: value => { options.title = value }, value: options?.title, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="title">PR title</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="body" name="body" ng-model="options.body" />
|
||||
<input class="form-check-input" type="checkbox" id="body" name="body" v-field="{ state: viewState, set: value => { options.body = value }, value: options?.body, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="body">PR body</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="diff" name="diff" ng-model="options.diff" />
|
||||
<input class="form-check-input" type="checkbox" id="diff" name="diff" v-field="{ state: viewState, set: value => { options.diff = value }, value: options?.diff, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="diff">Diff</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="comments" name="comments" ng-model="options.comments" />
|
||||
<input class="form-check-input" type="checkbox" id="comments" name="comments" v-field="{ state: viewState, set: value => { options.comments = value }, value: options?.comments, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="comments">Comments</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="username" name="username" ng-model="options.username" />
|
||||
<input class="form-check-input" type="checkbox" id="username" name="username" v-field="{ state: viewState, set: value => { options.username = value }, value: options?.username, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="username">Usernames</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="date" name="date" ng-model="options.date" />
|
||||
<input class="form-check-input" type="checkbox" id="date" name="date" v-field="{ state: viewState, set: value => { options.date = value }, value: options?.date, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="date">Dates</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="origin" name="origin" ng-model="options.origin" />
|
||||
<input class="form-check-input" type="checkbox" id="origin" name="origin" v-field="{ state: viewState, set: value => { options.origin = value }, value: options?.origin, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="origin">Project name</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="paper-settings-section" ng-show="detectedType">
|
||||
<section class="paper-settings-section" v-show="detectedType">
|
||||
<div class="paper-section-eyebrow">Expiration</div>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="expiration">Strategy</label>
|
||||
<select class="form-control" id="expiration" name="expiration" ng-model="options.expirationMode">
|
||||
<select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value => { options.expirationMode = value }, value: options?.expirationMode, form: "anonymize", options: {} }">
|
||||
<option value="redirect">Redirect to GitHub when expired</option>
|
||||
<option value="remove" selected>Remove when expired</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="expirationDate">Expiration date</label>
|
||||
<input class="form-control" type="date" name="expirationDate" id="expirationDate" ng-model="options.expirationDate" ng-class="{'is-invalid': anonymize.expirationDate.$invalid}" min="{{minExpirationDate}}" max="{{maxExpirationDate}}" required />
|
||||
<div class="invalid-feedback" ng-show="anonymize.expirationDate.$error.min">Pick a date in the future.</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.expirationDate.$error.max">Pick a date on or before {{maxExpirationDate | date}}.</div>
|
||||
<div class="invalid-feedback" ng-show="anonymize.expirationDate.$error.required || anonymize.expirationDate.$error.date">Enter a valid expiration date.</div>
|
||||
<input class="form-control" type="date" name="expirationDate" id="expirationDate" required v-field="{ state: viewState, set: value => { options.expirationDate = value }, value: options?.expirationDate, form: "anonymize", options: {} }" :class="{'is-invalid': anonymize?.expirationDate?.invalid}" :min="(minExpirationDate)" :max="(maxExpirationDate)">
|
||||
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.min">Pick a date in the future.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.max">Pick a date on or before {{ fmt?.date(maxExpirationDate) }}.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.required || anonymize?.expirationDate?.errors?.date">Enter a valid expiration date.</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted" ng-show="options.expirationMode=='remove' && options.expirationDate">After {{options.expirationDate | date}}, the content will be removed.</small>
|
||||
<small class="form-text text-muted" ng-show="options.expirationMode=='redirect' && options.expirationDate">After {{options.expirationDate | date}}, visitors will be redirected to GitHub.</small>
|
||||
<small class="form-text text-muted" v-show="options?.expirationMode=='remove' && options?.expirationDate">After {{ fmt?.date(options?.expirationDate) }}, the content will be removed.</small>
|
||||
<small class="form-text text-muted" v-show="options?.expirationMode=='redirect' && options?.expirationDate">After {{ fmt?.date(options?.expirationDate) }}, visitors will be redirected to GitHub.</small>
|
||||
</section>
|
||||
|
||||
<section class="paper-settings-section" ng-show="isUpdate && detectedType === 'repo'">
|
||||
<section class="paper-settings-section" v-show="isUpdate && detectedType === 'repo'">
|
||||
<div class="paper-section-eyebrow">Co-authors</div>
|
||||
<p class="form-text text-muted" style="margin-bottom: 8px;">
|
||||
Co-authors can view and edit these settings. They cannot delete the anonymization or manage co-authors.
|
||||
</p>
|
||||
|
||||
<div class="form-group" ng-show="role === 'owner' || role === 'admin'">
|
||||
<div class="form-group" v-show="role === 'owner' || role === 'admin'">
|
||||
<label class="paper-field-label" for="coauthorSearch">Add a GitHub user</label>
|
||||
<div style="position: relative;">
|
||||
<input
|
||||
type="text"
|
||||
id="coauthorSearch"
|
||||
class="form-control"
|
||||
placeholder="Search GitHub username…"
|
||||
ng-model="coauthorSearch"
|
||||
ng-change="searchCoauthors()"
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="dropdown-menu show" style="display: block; max-height: 220px; overflow-y: auto; width: 100%;" ng-show="coauthorResults.length > 0">
|
||||
<a href="#" class="dropdown-item d-flex align-items-center" ng-repeat="u in coauthorResults" ng-click="addCoauthor(u, $event)">
|
||||
<img ng-src="{{u.photo}}" alt="" style="width: 22px; height: 22px; border-radius: 50%; margin-right: 8px;" />
|
||||
<span ng-bind="u.username"></span>
|
||||
<input type="text" id="coauthorSearch" class="form-control" placeholder="Search GitHub username…" autocomplete="off" v-field="{ state: viewState, set: value => { coauthorSearch = value }, value: coauthorSearch, form: "anonymize", options: { debounce: 300 }, change: () => { searchCoauthors() } }">
|
||||
<div class="dropdown-menu show" style="display: block; max-height: 220px; overflow-y: auto; width: 100%;" v-show="coauthorResults?.length > 0">
|
||||
<a href="#" class="dropdown-item d-flex align-items-center" v-for="(u, index) in coauthorResults" @click.prevent="addCoauthor(u, $event)">
|
||||
<img alt="" style="width: 22px; height: 22px; border-radius: 50%; margin-right: 8px;" :src="safeUrl((u?.photo))">
|
||||
<span v-text="u?.username"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted" ng-show="coauthorError" ng-bind="coauthorError"></small>
|
||||
<small class="form-text text-muted" v-show="coauthorError" v-text="coauthorError"></small>
|
||||
</div>
|
||||
|
||||
<div class="coauthor-list">
|
||||
<div class="coauthor-row d-flex align-items-center" ng-repeat="c in coauthors" style="padding: 6px 0; gap: 8px;">
|
||||
<img ng-src="{{c.photo}}" alt="" style="width: 24px; height: 24px; border-radius: 50%;" ng-if="c.photo" />
|
||||
<a ng-href="https://github.com/{{c.username}}" target="_blank" ng-bind="c.username"></a>
|
||||
<div class="coauthor-row d-flex align-items-center" style="padding: 6px 0; gap: 8px;" v-for="(c, index) in coauthors">
|
||||
<img alt="" style="width: 24px; height: 24px; border-radius: 50%;" v-if="c?.photo" :src="safeUrl((c?.photo))">
|
||||
<a target="_blank" v-text="c?.username" :href="safeUrl("https://github.com/" + (c?.username))"></a>
|
||||
<span class="type-badge type-coauthor">Co-author</span>
|
||||
<button type="button" class="btn btn-sm" ng-click="removeCoauthor(c)" ng-show="role === 'owner' || role === 'admin'" style="margin-left: auto;" title="Remove co-author">
|
||||
<button type="button" class="btn btn-sm" style="margin-left: auto;" title="Remove co-author" v-show="role === 'owner' || role === 'admin'" @click="removeCoauthor(c)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text text-muted" ng-show="!coauthors || coauthors.length === 0">
|
||||
<div class="form-text text-muted" v-show="!coauthors || coauthors?.length === 0">
|
||||
No co-authors yet.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="alert alert-danger" role="alert" ng-if="error" ng-bind="error"></div>
|
||||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||||
|
||||
<div class="anonymize-submit-bar" ng-show="detectedType">
|
||||
<button type="submit" class="btn btn-ink" ng-click="anonymizeRepo($event)" ng-if="detectedType === 'repo' && !isUpdate">
|
||||
<div class="anonymize-submit-bar" v-show="detectedType">
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'repo' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize Repository
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" ng-click="anonymizeRepo($event)" ng-if="detectedType === 'repo' && isUpdate">
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'repo' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update Repository
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" ng-click="anonymizePullRequest($event)" ng-if="detectedType === 'pr' && !isUpdate">
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'pr' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize Pull Request
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" ng-click="anonymizePullRequest($event)" ng-if="detectedType === 'pr' && isUpdate">
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'pr' && isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update Pull Request
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" ng-click="anonymizeGist($event)" ng-if="detectedType === 'gist' && !isUpdate">
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'gist' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize Gist
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" ng-click="anonymizeGist($event)" ng-if="detectedType === 'gist' && isUpdate">
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'gist' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update Gist
|
||||
</button>
|
||||
</div>
|
||||
@@ -349,18 +322,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Preview column (right) -->
|
||||
<div
|
||||
class="anonymize-preview-col"
|
||||
ng-if="detectedType === 'repo' && html_readme"
|
||||
>
|
||||
<div class="anonymize-preview-col" v-if="detectedType === 'repo' && html_readme">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">README with redactions applied</span>
|
||||
</div>
|
||||
<div class="anonymize-preview-body markdown-body body" ng-bind-html="html_readme"></div>
|
||||
<div class="anonymize-preview-body markdown-body body" v-html="sanitize(html_readme)"></div>
|
||||
</div>
|
||||
|
||||
<div class="anonymize-preview-col" ng-if="detectedType === 'gist' && details">
|
||||
<div class="anonymize-preview-col" v-if="detectedType === 'gist' && details">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">Gist with redactions applied</span>
|
||||
@@ -368,38 +338,38 @@
|
||||
<div class="anonymize-preview-body">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span ng-if="options.title">{{anonymizeGistContent(details.gist.description) || 'Untitled gist'}}</span>
|
||||
<span class="badge" ng-class="{'badge-success': details.gist.isPublic, 'badge-secondary': !details.gist.isPublic}">
|
||||
{{details.gist.isPublic ? 'public' : 'secret'}}
|
||||
<span v-if="options?.title">{{ anonymizeGistContent(details?.gist?.description) || 'Untitled gist' }}</span>
|
||||
<span class="badge" :class="{'badge-success': details?.gist?.isPublic, 'badge-secondary': !details?.gist?.isPublic}">
|
||||
{{ details?.gist?.isPublic ? 'public' : 'secret' }}
|
||||
</span>
|
||||
</h2>
|
||||
<small ng-bind="details.gist.updatedDate | date" ng-if="options.date"></small>
|
||||
<small v-if="options?.date" v-text="fmt?.date(details?.gist?.updatedDate)"></small>
|
||||
</div>
|
||||
<small ng-if="options.origin">Gist ID: {{details.source.gistId}}</small>
|
||||
<small ng-if="options.username && details.gist.ownerLogin">By @{{anonymizeGistContent(details.gist.ownerLogin)}}</small>
|
||||
<div ng-if="options.content && previewGistFiles.length">
|
||||
<small v-if="options?.origin">Gist ID: {{ details?.source?.gistId }}</small>
|
||||
<small v-if="options?.username && details?.gist?.ownerLogin">By @{{ anonymizeGistContent(details?.gist?.ownerLogin) }}</small>
|
||||
<div v-if="options?.content && previewGistFiles?.length">
|
||||
<ul class="pr-comments mt-3">
|
||||
<li class="pr-comment" ng-repeat="file in previewGistFiles">
|
||||
<li class="pr-comment" v-for="(file, index) in previewGistFiles">
|
||||
<div class="pr-comment-head">
|
||||
<strong ng-bind="file.filename"></strong>
|
||||
<span class="pr-comment-date" ng-if="file.language">{{file.language}}</span>
|
||||
<strong v-text="file?.filename"></strong>
|
||||
<span class="pr-comment-date" v-if="file?.language">{{ file?.language }}</span>
|
||||
</div>
|
||||
<gist-file file="file" terms="terms" options="options"></gist-file>
|
||||
<gist-file :file="file" :terms="terms" :options="options"></gist-file>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div ng-if="options.comments && details.gist.comments && details.gist.comments.length">
|
||||
<div v-if="options?.comments && details?.gist?.comments && details?.gist?.comments?.length">
|
||||
<h3 class="paper-section-eyebrow mt-3">Comments</h3>
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" ng-repeat="comment in details.gist.comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.gist?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" ng-if="options.username">
|
||||
<i class="far fa-user"></i> @<span ng-bind="anonymizeGistContent(comment.author)"></span>
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<i class="far fa-user"></i> @<span v-text="anonymizeGistContent(comment?.author)"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" ng-if="options.date" ng-bind="comment.updatedDate | date"></span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
<div class="pr-comment-body" ng-if="options.body">
|
||||
<markdown content="anonymizeGistContent(comment.body)" options="options" terms="terms"></markdown>
|
||||
<div class="pr-comment-body" v-if="options?.body">
|
||||
<markdown :content="anonymizeGistContent(comment?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -407,8 +377,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="anonymize-preview-col" ng-if="detectedType === 'pr' && details"
|
||||
ng-init="prTabState = { active: options.diff ? 'diff' : 'comments' }">
|
||||
<div class="anonymize-preview-col" v-if="detectedType === 'pr' && details">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">Pull request with redactions applied</span>
|
||||
@@ -416,55 +385,41 @@
|
||||
<div class="anonymize-preview-body">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span ng-if="options.title">{{anonymizePrContent(details.pullRequest.title)}}</span>
|
||||
<span class="badge" ng-class="{'badge-success':details.pullRequest.merged, 'badge-warning':details.pullRequest.state=='open', 'badge-danger':details.pullRequest.state=='closed' && !details.pullRequest.merged}">
|
||||
{{details.pullRequest.merged ? "merged" : details.pullRequest.state | title}}
|
||||
<span v-if="options?.title">{{ anonymizePrContent(details?.pullRequest?.title) }}</span>
|
||||
<span class="badge" :class="{'badge-success':details?.pullRequest?.merged, 'badge-warning':details?.pullRequest?.state=='open', 'badge-danger':details?.pullRequest?.state=='closed' && !details?.pullRequest?.merged}">
|
||||
{{ fmt?.title(details?.pullRequest?.merged ? "merged" : details?.pullRequest?.state) }}
|
||||
</span>
|
||||
</h2>
|
||||
<small ng-bind="details.pullRequest.updatedDate | date" ng-if="options.date"></small>
|
||||
<small v-if="options?.date" v-text="fmt?.date(details?.pullRequest?.updatedDate)"></small>
|
||||
</div>
|
||||
<small ng-if="options.origin">Pull Request on {{details.pullRequest.baseRepositoryFullName}}</small>
|
||||
<div class="pr-body shadow-sm p-3 mb-4 rounded" style="background: var(--paper-bg-alt)" ng-if="options.body">
|
||||
<markdown content="anonymizePrContent(details.pullRequest.body)" options="options" terms="terms"></markdown>
|
||||
<small v-if="options?.origin">Pull Request on {{ details?.pullRequest?.baseRepositoryFullName }}</small>
|
||||
<div class="pr-body shadow-sm p-3 mb-4 rounded" style="background: var(--paper-bg-alt)" v-if="options?.body">
|
||||
<markdown :content="anonymizePrContent(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
<nav class="paper-tabs" ng-if="options.diff || options.comments" role="tablist">
|
||||
<button
|
||||
class="paper-tab"
|
||||
ng-if="options.diff"
|
||||
ng-class="{'active': prTabState.active == 'diff'}"
|
||||
ng-click="prTabState.active = 'diff'"
|
||||
type="button"
|
||||
role="tab"
|
||||
>
|
||||
<nav class="paper-tabs" role="tablist" v-if="options?.diff || options?.comments">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.diff" :class="{'active': prTabState?.active == 'diff'}" @click="prTabState.active = 'diff'">
|
||||
<i class="fas fa-code"></i> Diff
|
||||
</button>
|
||||
<button
|
||||
class="paper-tab"
|
||||
ng-if="options.comments"
|
||||
ng-class="{'active': prTabState.active == 'comments'}"
|
||||
ng-click="prTabState.active = 'comments'"
|
||||
type="button"
|
||||
role="tab"
|
||||
>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.comments" :class="{'active': prTabState?.active == 'comments'}" @click="prTabState.active = 'comments'">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<ng-pluralize count="details.pullRequest.comments.length" when="{'0': 'No comments', 'one': '1 comment', 'other': '{} comments'}"></ng-pluralize>
|
||||
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="paper-tab-content">
|
||||
<div ng-if="options.diff && prTabState.active == 'diff'">
|
||||
<div class="pr-diff" ng-bind-html="anonymizePrContent(details.pullRequest.diff) | diff"></div>
|
||||
<div v-if="options?.diff && prTabState?.active == 'diff'">
|
||||
<div class="pr-diff" v-html="sanitize(fmt?.diff(anonymizePrContent(details?.pullRequest?.diff)))"></div>
|
||||
</div>
|
||||
<div ng-if="options.comments && prTabState.active == 'comments'">
|
||||
<div v-if="options?.comments && prTabState?.active == 'comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" ng-repeat="comment in details.pullRequest.comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.pullRequest?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" ng-if="options.username">
|
||||
<i class="far fa-user"></i> @<span ng-bind="anonymizePrContent(comment.author)"></span>
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<i class="far fa-user"></i> @<span v-text="anonymizePrContent(comment?.author)"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" ng-if="options.date" ng-bind="comment.updatedDate | date"></span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
<div class="pr-comment-body" ng-if="options.body">
|
||||
<markdown content="anonymizePrContent(comment.body)" options="options" terms="terms"></markdown>
|
||||
<div class="pr-comment-body" v-if="options?.body">
|
||||
<markdown :content="anonymizePrContent(comment?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
<div class="container-fluid h-100 anonymize-page">
|
||||
<div class="row h-100 flex-column flex-md-row">
|
||||
<div
|
||||
class="col-md sidePanel shadow overflow-auto anonymize-form-col"
|
||||
>
|
||||
<div
|
||||
class="p-0 py-2 m-auto"
|
||||
ng-class="{'card': !pullRequestUrl,'container': pullRequestUrl}"
|
||||
>
|
||||
<form
|
||||
class="form needs-validation"
|
||||
ng-class="{'card-body': !pullRequestUrl}"
|
||||
name="anonymizeForm"
|
||||
novalidate
|
||||
>
|
||||
<div class="col-md sidePanel shadow overflow-auto anonymize-form-col">
|
||||
<div class="p-0 py-2 m-auto" :class="{'card': !pullRequestUrl,'container': pullRequestUrl}">
|
||||
<form class="form needs-validation" name="anonymizeForm" novalidate v-form="viewState" :class="{'card-body': !pullRequestUrl}">
|
||||
<div class="paper-crumbs">Anonymize / <span class="here">Pull request</span></div>
|
||||
<h1 class="paper-page-title">Anonymize a <em>pull request</em></h1>
|
||||
<p class="paper-page-lede">
|
||||
@@ -24,56 +14,27 @@
|
||||
</div>
|
||||
<!-- pullRequestUrl -->
|
||||
<div class="form-group">
|
||||
<label for="pullRequestUrl"
|
||||
>URL of your pull request</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="pullRequestUrl"
|
||||
id="pullRequestUrl"
|
||||
ng-class="{'is-invalid': anonymize.pullRequestUrl.$invalid}"
|
||||
ng-model="pullRequestUrl"
|
||||
placeholder="https://github.com/owner/repo/pull/123"
|
||||
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
|
||||
ng-change="pullRequestSelected()"
|
||||
/>
|
||||
<div
|
||||
class="invalid-feedback"
|
||||
ng-show="anonymize.pullRequestUrl.$error.access"
|
||||
>
|
||||
{{pullRequestUrl}} is not accessible. Some organizations are
|
||||
<label for="pullRequestUrl">URL of your pull request</label>
|
||||
<input type="text" class="form-control" name="pullRequestUrl" id="pullRequestUrl" placeholder="https://github.com/owner/repo/pull/123" v-field="{ state: viewState, set: value => { pullRequestUrl = value }, value: pullRequestUrl, form: "anonymizeForm", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { pullRequestSelected() } }" :class="{'is-invalid': anonymize?.pullRequestUrl?.invalid}">
|
||||
<div class="invalid-feedback" v-show="anonymize?.pullRequestUrl?.errors?.access">
|
||||
{{ pullRequestUrl }} is not accessible. Some organizations are
|
||||
restricting the access to the repositories.
|
||||
</div>
|
||||
<div
|
||||
class="invalid-feedback"
|
||||
ng-show="anonymize.pullRequestUrl.$error.missing"
|
||||
>
|
||||
{{pullRequestUrl}} does not exist or is not accessible
|
||||
<div class="invalid-feedback" v-show="anonymize?.pullRequestUrl?.errors?.missing">
|
||||
{{ pullRequestUrl }} does not exist or is not accessible
|
||||
</div>
|
||||
<div
|
||||
class="invalid-feedback"
|
||||
ng-show="anonymize.pullRequestUrl.$error.used"
|
||||
>
|
||||
{{pullRequestUrl}} is already anonymized
|
||||
<div class="invalid-feedback" v-show="anonymize?.pullRequestUrl?.errors?.used">
|
||||
{{ pullRequestUrl }} is already anonymized
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="pullRequestUrl">
|
||||
<div v-show="pullRequestUrl">
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="update"
|
||||
name="update"
|
||||
ng-model="options.update"
|
||||
/>
|
||||
<input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value => { options.update = value }, value: options?.update, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="update">Auto update</label>
|
||||
<small id="updateHelp" class="form-text text-muted"
|
||||
>Automatically update the anonymized pull request with the
|
||||
<small id="updateHelp" class="form-text text-muted">Automatically update the anonymized pull request with the
|
||||
latest updates. The pull request is updated once per day
|
||||
maximum.</small
|
||||
>
|
||||
maximum.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,31 +43,15 @@
|
||||
</div>
|
||||
<!-- Conference -->
|
||||
<div class="form-group">
|
||||
<label for="conference"
|
||||
>Conference ID <span class="text-muted">(Optional)</span></label
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
id="conference"
|
||||
name="conference"
|
||||
ng-model="conference"
|
||||
ng-model-options="{ debounce: { default: 800, blur: 0 }, updateOn: 'default blur' }"
|
||||
ng-class="{'is-invalid': anonymize.conference.$invalid}"
|
||||
/>
|
||||
<small class="form-text text-muted" ng-show="conference_data"
|
||||
><a ng-href="{{conference_data.url}}" target="_target"
|
||||
>{{conference_data.name}}</a
|
||||
>
|
||||
will expire on {{conference_data.endDate | date}}.</small
|
||||
>
|
||||
<label for="conference">Conference ID <span class="text-muted">(Optional)</span></label>
|
||||
<input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value => { conference = value }, value: conference, form: "anonymizeForm", options: { debounce: { default: 800, blur: 0 }, updateOn: 'default blur' } }" :class="{'is-invalid': anonymize?.conference?.invalid}">
|
||||
<small class="form-text text-muted" v-show="conference_data"><a target="_target" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a>
|
||||
will expire on {{ fmt?.date(conference_data?.endDate) }}.</small>
|
||||
|
||||
<div
|
||||
class="invalid-feedback"
|
||||
ng-show="anonymize.conference.$error.activated"
|
||||
>
|
||||
<div class="invalid-feedback" v-show="anonymize?.conference?.errors?.activated">
|
||||
The conference is not activated.
|
||||
</div>
|
||||
<small class="form-text text-muted" ng-show="!conference_data">
|
||||
<small class="form-text text-muted" v-show="!conference_data">
|
||||
Use the Conference ID that your conference provided you. This
|
||||
will update automatically the anonymization options based on the
|
||||
conference preferences.
|
||||
@@ -119,247 +64,101 @@
|
||||
<!-- Pull Request ID -->
|
||||
<div class="form-group">
|
||||
<label for="pullRequestId">Anonymized pull request id</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="pullRequestId"
|
||||
id="pullRequestId"
|
||||
ng-class="{'is-invalid': anonymize.pullRequestId.$invalid}"
|
||||
ng-model="pullRequestId"
|
||||
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
|
||||
/>
|
||||
<small id="idHelp" class="form-text text-muted"
|
||||
>Id used in the url:
|
||||
https://anonymous.4open.science/r/{{pullRequestId}}</small
|
||||
>
|
||||
<div
|
||||
class="invalid-feedback"
|
||||
ng-show="anonymize.pullRequestId.$error.format"
|
||||
>
|
||||
<input type="text" class="form-control" name="pullRequestId" id="pullRequestId" v-field="{ state: viewState, set: value => { pullRequestId = value }, value: pullRequestId, form: "anonymizeForm", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.pullRequestId?.invalid}">
|
||||
<small id="idHelp" class="form-text text-muted">Id used in the url:
|
||||
https://anonymous.4open.science/r/{{ pullRequestId }}</small>
|
||||
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.format">
|
||||
Repository id can only contain letters and numbers
|
||||
</div>
|
||||
<div
|
||||
class="invalid-feedback"
|
||||
ng-show="anonymize.pullRequestId.$error.used"
|
||||
>
|
||||
{{pullRequestId}} is already used
|
||||
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.used">
|
||||
{{ pullRequestId }} is already used
|
||||
</div>
|
||||
</div>
|
||||
<!-- Terms -->
|
||||
<div class="form-group">
|
||||
<label for="terms">Terms to anonymize</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="terms"
|
||||
name="terms"
|
||||
rows="3"
|
||||
ng-model="terms"
|
||||
ng-model-options="{ debounce: 250 }"
|
||||
ng-class="{'is-invalid': anonymize.terms.$invalid}"
|
||||
></textarea>
|
||||
<small id="termsHelp" class="form-text text-muted"
|
||||
>One term per line. Each term will be replaced by XXX.</small
|
||||
>
|
||||
<div
|
||||
class="invalid-feedback"
|
||||
ng-show="anonymize.terms.$error.format"
|
||||
>
|
||||
<textarea class="form-control" id="terms" name="terms" rows="3" v-field="{ state: viewState, set: value => { terms = value }, value: terms, form: "anonymizeForm", options: { debounce: 250 } }" :class="{'is-invalid': anonymize?.terms?.invalid}"></textarea>
|
||||
<small id="termsHelp" class="form-text text-muted">One term per line. Each term will be replaced by XXX.</small>
|
||||
<div class="invalid-feedback" v-show="anonymize?.terms?.errors?.format">
|
||||
Terms are in an invalid format
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="expiration">Expiration strategy</label>
|
||||
<select
|
||||
class="form-control"
|
||||
id="expiration"
|
||||
name="expiration"
|
||||
ng-model="options.expirationMode"
|
||||
>
|
||||
<select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value => { options.expirationMode = value }, value: options?.expirationMode, form: "anonymizeForm", options: {} }">
|
||||
<option value="never" selected>Never expire</option>
|
||||
<option value="redirect">
|
||||
Redirect to GitHub when expired
|
||||
</option>
|
||||
<option value="remove">Remove when expired</option>
|
||||
</select>
|
||||
<small class="form-text text-muted"
|
||||
>Define the expiration strategy for the anonymized
|
||||
repository.</small
|
||||
>
|
||||
<small class="form-text text-muted">Define the expiration strategy for the anonymized
|
||||
repository.</small>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
id="expiration-date-form"
|
||||
ng-hide="options.expirationMode=='never'"
|
||||
>
|
||||
<label for="expirationDate"
|
||||
>Expiration date of the anonymized repository</label
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
type="date"
|
||||
name="expirationDate"
|
||||
id="expirationDate"
|
||||
ng-model="options.expirationDate"
|
||||
/>
|
||||
<small
|
||||
class="form-text text-muted"
|
||||
ng-show="options.expirationMode=='remove'"
|
||||
>After {{options.expirationDate | date}}, the repository will be
|
||||
<div class="form-group" id="expiration-date-form" v-show="!(options?.expirationMode=='never')">
|
||||
<label for="expirationDate">Expiration date of the anonymized repository</label>
|
||||
<input class="form-control" type="date" name="expirationDate" id="expirationDate" v-field="{ state: viewState, set: value => { options.expirationDate = value }, value: options?.expirationDate, form: "anonymizeForm", options: {} }">
|
||||
<small class="form-text text-muted" v-show="options?.expirationMode=='remove'">After {{ fmt?.date(options?.expirationDate) }}, the repository will be
|
||||
removed and the visitor will not be able to see the content of
|
||||
the repository.</small
|
||||
>
|
||||
<small
|
||||
class="form-text text-muted"
|
||||
ng-show="options.expirationMode=='redirect'"
|
||||
>After {{options.expirationDate | date}}, the visitors of the
|
||||
the repository.</small>
|
||||
<small class="form-text text-muted" v-show="options?.expirationMode=='redirect'">After {{ fmt?.date(options?.expirationDate) }}, the visitors of the
|
||||
anonymized repository will be redirected to
|
||||
{{pullRequestUrl}}.</small
|
||||
>
|
||||
{{ pullRequestUrl }}.</small>
|
||||
</div>
|
||||
|
||||
<div class="accordion mb-3" id="options">
|
||||
<div class="card">
|
||||
<div class="card-header" id="headingOne">
|
||||
<h2 class="mb-0">
|
||||
<button
|
||||
class="btn btn-block text-left"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#collapseOne"
|
||||
aria-expanded="true"
|
||||
aria-controls="collapseOne"
|
||||
>
|
||||
<button class="btn btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
|
||||
<i class="fas fa-cog mr-1"></i> Advanced options
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="collapseOne"
|
||||
class="collapse show"
|
||||
aria-labelledby="headingOne"
|
||||
data-parent="#options"
|
||||
>
|
||||
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#options">
|
||||
<div class="card-body">
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="link"
|
||||
name="link"
|
||||
ng-model="options.link"
|
||||
/>
|
||||
<label class="form-check-label" for="link"
|
||||
>Keep links</label
|
||||
>
|
||||
<small class="form-text text-muted"
|
||||
>Keep or remove all the links.</small
|
||||
>
|
||||
<input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value => { options.link = value }, value: options?.link, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="link">Keep links</label>
|
||||
<small class="form-text text-muted">Keep or remove all the links.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="image"
|
||||
name="image"
|
||||
ng-model="options.image"
|
||||
/>
|
||||
<label class="form-check-label" for="image"
|
||||
>Display images</label
|
||||
>
|
||||
<small class="form-text text-muted"
|
||||
>Images are not anonymized</small
|
||||
>
|
||||
<input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value => { options.image = value }, value: options?.image, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="image">Display images</label>
|
||||
<small class="form-text text-muted">Images are not anonymized</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="date"
|
||||
name="date"
|
||||
ng-model="options.date"
|
||||
/>
|
||||
<label class="form-check-label" for="date"
|
||||
>Display dates</label
|
||||
>
|
||||
<small class="form-text text-muted"
|
||||
>Display the date of the Pull Request and the date of
|
||||
the comments.</small
|
||||
>
|
||||
<input class="form-check-input" type="checkbox" id="date" name="date" v-field="{ state: viewState, set: value => { options.date = value }, value: options?.date, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="date">Display dates</label>
|
||||
<small class="form-text text-muted">Display the date of the Pull Request and the date of
|
||||
the comments.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="username"
|
||||
name="username"
|
||||
ng-model="options.username"
|
||||
/>
|
||||
<label class="form-check-label" for="username"
|
||||
>Display username</label
|
||||
>
|
||||
<input class="form-check-input" type="checkbox" id="username" name="username" v-field="{ state: viewState, set: value => { options.username = value }, value: options?.username, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="username">Display username</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="comments"
|
||||
name="comments"
|
||||
ng-model="options.comments"
|
||||
/>
|
||||
<label class="form-check-label" for="comments"
|
||||
>Display comments</label
|
||||
>
|
||||
<input class="form-check-input" type="checkbox" id="comments" name="comments" v-field="{ state: viewState, set: value => { options.comments = value }, value: options?.comments, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="comments">Display comments</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="diff"
|
||||
name="diff"
|
||||
ng-model="options.diff"
|
||||
/>
|
||||
<label class="form-check-label" for="diff"
|
||||
>Display diff</label
|
||||
>
|
||||
<input class="form-check-input" type="checkbox" id="diff" name="diff" v-field="{ state: viewState, set: value => { options.diff = value }, value: options?.diff, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="diff">Display diff</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="origin"
|
||||
name="origin"
|
||||
ng-model="options.origin"
|
||||
/>
|
||||
<label class="form-check-label" for="origin"
|
||||
>Display the project name</label
|
||||
>
|
||||
<input class="form-check-input" type="checkbox" id="origin" name="origin" v-field="{ state: viewState, set: value => { options.origin = value }, value: options?.origin, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="origin">Display the project name</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="title"
|
||||
name="title"
|
||||
ng-model="options.title"
|
||||
/>
|
||||
<label class="form-check-label" for="title"
|
||||
>Display the PR title</label
|
||||
>
|
||||
<input class="form-check-input" type="checkbox" id="title" name="title" v-field="{ state: viewState, set: value => { options.title = value }, value: options?.title, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="title">Display the PR title</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="body"
|
||||
name="body"
|
||||
ng-model="options.body"
|
||||
/>
|
||||
<label class="form-check-label" for="body"
|
||||
>Display the PR body and comment bodies</label
|
||||
>
|
||||
<input class="form-check-input" type="checkbox" id="body" name="body" v-field="{ state: viewState, set: value => { options.body = value }, value: options?.body, form: "anonymizeForm", options: {} }">
|
||||
<label class="form-check-label" for="body">Display the PR body and comment bodies</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -367,149 +166,64 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="alert alert-danger"
|
||||
role="alert"
|
||||
ng-if="error"
|
||||
ng-bind="error"
|
||||
></div>
|
||||
<div class="anonymize-submit-bar" ng-show="pullRequestUrl">
|
||||
<button
|
||||
id="submit"
|
||||
type="submit"
|
||||
class="btn btn-ink btn-block"
|
||||
ng-click="anonymizePullRequest($event)"
|
||||
ng-if="!isUpdate"
|
||||
>
|
||||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||||
<div class="anonymize-submit-bar" v-show="pullRequestUrl">
|
||||
<button id="submit" type="submit" class="btn btn-ink btn-block" v-if="!isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize
|
||||
</button>
|
||||
<button
|
||||
id="submit"
|
||||
type="submit"
|
||||
class="btn btn-ink btn-block"
|
||||
ng-click="updatePullRequest($event)"
|
||||
ng-if="isUpdate"
|
||||
>
|
||||
<button id="submit" type="submit" class="btn btn-ink btn-block" v-if="isUpdate" @click.prevent="submitForm($event, () => { updatePullRequest($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 p-2 overflow-auto anonymize-preview-col" ng-if="details">
|
||||
<div class="col-md-8 p-2 overflow-auto anonymize-preview-col" v-if="details">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span ng-if="options.title"
|
||||
>{{anonymize(details.pullRequest.title)}}</span
|
||||
>
|
||||
<span
|
||||
class="badge"
|
||||
ng-class="{'badge-success':details.pullRequest.merged, 'badge-warning':details.pullRequest.state=='open', 'badge-danger':details.pullRequest.state=='closed' &&!details.pullRequest.merged}"
|
||||
>
|
||||
{{details.pullRequest.merged?"merged":details.pullRequest.state |
|
||||
title}}
|
||||
<span v-if="options?.title">{{ anonymize(details?.pullRequest?.title) }}</span>
|
||||
<span class="badge" :class="{'badge-success':details?.pullRequest?.merged, 'badge-warning':details?.pullRequest?.state=='open', 'badge-danger':details?.pullRequest?.state=='closed' &&!details?.pullRequest?.merged}">
|
||||
{{ fmt?.title(details?.pullRequest?.merged?"merged":details?.pullRequest?.state) }}
|
||||
</span>
|
||||
</h2>
|
||||
<small
|
||||
ng-bind="details.pullRequest.updatedDate | date"
|
||||
ng-if="options.date"
|
||||
></small>
|
||||
<small v-if="options?.date" v-text="fmt?.date(details?.pullRequest?.updatedDate)"></small>
|
||||
</div>
|
||||
<small ng-if="options.origin"
|
||||
>Pull Request on {{details.pullRequest.baseRepositoryFullName}}</small
|
||||
>
|
||||
<div
|
||||
class="pr-body shadow-sm p-3 mb-4 rounded"
|
||||
style="background: var(--sidebar-bg-color)"
|
||||
ng-if="options.body"
|
||||
>
|
||||
<markdown
|
||||
content="anonymize(details.pullRequest.body)"
|
||||
options="options"
|
||||
terms="terms"
|
||||
></markdown>
|
||||
<small v-if="options?.origin">Pull Request on {{ details?.pullRequest?.baseRepositoryFullName }}</small>
|
||||
<div class="pr-body shadow-sm p-3 mb-4 rounded" style="background: var(--sidebar-bg-color)" v-if="options?.body">
|
||||
<markdown :content="anonymize(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation" ng-if="options.diff">
|
||||
<button
|
||||
class="nav-link active"
|
||||
id="pills-diff-tab"
|
||||
data-toggle="pill"
|
||||
data-target="#pills-diff"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="pills-diff"
|
||||
aria-selected="true"
|
||||
>
|
||||
<li class="nav-item" role="presentation" v-if="options?.diff">
|
||||
<button class="nav-link active" id="pills-diff-tab" data-toggle="pill" data-target="#pills-diff" type="button" role="tab" aria-controls="pills-diff" aria-selected="true">
|
||||
Diff
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation" ng-if="options.comments">
|
||||
<button
|
||||
class="nav-link"
|
||||
ng-class="{'active':!options.diff}"
|
||||
id="pills-comments-tab"
|
||||
data-toggle="pill"
|
||||
data-target="#pills-comments"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="pills-comments"
|
||||
aria-selected="false"
|
||||
>
|
||||
<ng-pluralize
|
||||
count="details.pullRequest.comments.length"
|
||||
when="{'0': 'No comment',
|
||||
'one': 'One Comment',
|
||||
'other': '{} Comments'}"
|
||||
>
|
||||
</ng-pluralize>
|
||||
<li class="nav-item" role="presentation" v-if="options?.comments">
|
||||
<button class="nav-link" id="pills-comments-tab" data-toggle="pill" data-target="#pills-comments" type="button" role="tab" aria-controls="pills-comments" aria-selected="false" :class="{'active':!options?.diff}">
|
||||
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {'0': 'No comment',
|
||||
'one': 'One Comment',
|
||||
'other': '{} Comments'})">
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="pills-tabContent">
|
||||
<div
|
||||
class="tab-pane show active"
|
||||
id="pills-diff"
|
||||
role="tabpanel"
|
||||
aria-labelledby="pills-diff-tab"
|
||||
>
|
||||
<div
|
||||
class="pr-diff shadow-sm p-3 mb-4 rounded"
|
||||
style="background: var(--sidebar-bg-color)"
|
||||
ng-if="options.diff"
|
||||
>
|
||||
<pre style="overflow-x: auto"><code ng-bind-html="anonymize(details.pullRequest.diff) | diff"></code></pre>
|
||||
<div class="tab-pane show active" id="pills-diff" role="tabpanel" aria-labelledby="pills-diff-tab">
|
||||
<div class="pr-diff shadow-sm p-3 mb-4 rounded" style="background: var(--sidebar-bg-color)" v-if="options?.diff">
|
||||
<pre style="overflow-x: auto"><code v-html="sanitize(fmt?.diff(anonymize(details?.pullRequest?.diff)))"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane"
|
||||
ng-class="{'show active':!options.diff}"
|
||||
id="pills-comments"
|
||||
role="tabpanel"
|
||||
aria-labelledby="pills-comments-tab"
|
||||
>
|
||||
<ul class="pr-comments list-group" ng-if="options.comments">
|
||||
<li
|
||||
class="pr-comment list-group-item"
|
||||
ng-repeat="comment in details.pullRequest.comments"
|
||||
>
|
||||
<div class="tab-pane" id="pills-comments" role="tabpanel" aria-labelledby="pills-comments-tab" :class="{'show active':!options?.diff}">
|
||||
<ul class="pr-comments list-group" v-if="options?.comments">
|
||||
<li class="pr-comment list-group-item" v-for="(comment, index) in details?.pullRequest?.comments">
|
||||
<div class="d-flex w-100 justify-content-between flex-wrap">
|
||||
<h5 class="mb-1" ng-if="options.username">
|
||||
@{{anonymize(comment.author)}}
|
||||
<h5 class="mb-1" v-if="options?.username">
|
||||
@{{ anonymize(comment?.author) }}
|
||||
</h5>
|
||||
<small
|
||||
ng-bind="comment.updatedDate | date"
|
||||
ng-if="options.date"
|
||||
></small>
|
||||
<small v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></small>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
<markdown
|
||||
class="pr-comment-body"
|
||||
ng-if="options.body"
|
||||
content="anonymize(comment.body)"
|
||||
options="options"
|
||||
terms="terms"
|
||||
></markdown>
|
||||
<markdown class="pr-comment-body" v-if="options?.body" :content="anonymize(comment?.body)" :options="options" :terms="terms"></markdown>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -7,43 +7,21 @@
|
||||
</p>
|
||||
<div class="paper-settings-main claim-form">
|
||||
<p class="paper-section-copy">Use this when an anonymization was created by a co-author or from another account. You must have access to the GitHub repository it was made from.</p>
|
||||
<form class="form needs-validation" name="claimForm" novalidate ng-submit="claim()">
|
||||
<form class="form needs-validation" name="claimForm" novalidate v-form="viewState" @submit.prevent="submitForm($event, () => { claim() })">
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="repoUrl">GitHub repository URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="repoUrl"
|
||||
id="repoUrl"
|
||||
required
|
||||
ng-class="{'is-invalid': claimForm.repoUrl.$invalid && (claimForm.repoUrl.$touched || claimForm.$submitted)}"
|
||||
ng-model="repoUrl"
|
||||
/>
|
||||
<div class="invalid-feedback" ng-show="claimForm.repoUrl.$error.not_found">
|
||||
<input type="text" class="form-control" name="repoUrl" id="repoUrl" required v-field="{ state: viewState, set: value => { repoUrl = value }, value: repoUrl, form: "claimForm", options: {} }" :class="{'is-invalid': claimForm?.repoUrl?.invalid && (claimForm?.repoUrl?.touched || claimForm?.submitted)}">
|
||||
<div class="invalid-feedback" v-show="claimForm?.repoUrl?.errors?.not_found">
|
||||
No anonymization matches this repository and ID. Check both values and that you can access the repository on GitHub.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="repoId">Anonymized repository ID</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="repoId"
|
||||
id="repoId"
|
||||
required
|
||||
ng-model="repoId"
|
||||
ng-class="{'is-invalid': claimForm.repoId.$invalid && (claimForm.repoId.$touched || claimForm.$submitted)}"
|
||||
/>
|
||||
<small id="idHelp" class="form-text text-muted"
|
||||
>The ID is the last part of the anonymized URL: <code>anonymous.4open.science/r/<ID></code>.</small
|
||||
>
|
||||
<input type="text" class="form-control" name="repoId" id="repoId" required v-field="{ state: viewState, set: value => { repoId = value }, value: repoId, form: "claimForm", options: {} }" :class="{'is-invalid': claimForm?.repoId?.invalid && (claimForm?.repoId?.touched || claimForm?.submitted)}">
|
||||
<small id="idHelp" class="form-text text-muted">The ID is the last part of the anonymized URL: <code>anonymous.4open.science/r/<ID></code>.</small>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="submit"
|
||||
type="submit"
|
||||
class="btn btn-ink"
|
||||
>
|
||||
<button id="submit" type="submit" class="btn btn-ink">
|
||||
Claim anonymization
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,99 +1,77 @@
|
||||
<div class="container page paper-page">
|
||||
<div>
|
||||
<div class="paper-crumbs">
|
||||
<a href="/dashboard">My work</a> / <a href="/conferences">Conferences</a> / <span class="here">{{conference.conferenceID}}</span>
|
||||
<a href="/dashboard">My work</a> / <a href="/conferences">Conferences</a> / <span class="here">{{ conference?.conferenceID }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-end flex-wrap" style="gap: 12px; justify-content: space-between;">
|
||||
<div>
|
||||
<h1 class="paper-page-title" ng-bind="conference.name"></h1>
|
||||
<p class="paper-page-lede" ng-if="conference.url">
|
||||
<a ng-href="{{conference.url}}" target="_blank" rel="noopener" ng-bind="conference.url"></a>
|
||||
<h1 class="paper-page-title" v-text="conference?.name"></h1>
|
||||
<p class="paper-page-lede" v-if="conference?.url">
|
||||
<a target="_blank" rel="noopener" v-text="conference?.url" :href="safeUrl((conference?.url))"></a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-center flex-wrap" style="gap: 10px;">
|
||||
<span class="status-pill" ng-class="{'status-pill-ready': conference.status == 'ready', 'status-pill-removed': conference.status == 'removed' || conference.status == 'expired'}">
|
||||
<span class="status-dot" ng-class="'status-' + conference.status" aria-hidden="true"></span>
|
||||
<span ng-bind="conference.status | statusLabel"></span>
|
||||
<span class="status-pill" :class="{'status-pill-ready': conference?.status == 'ready', 'status-pill-removed': conference?.status == 'removed' || conference?.status == 'expired'}">
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + conference?.status"></span>
|
||||
<span v-text="fmt?.statusLabel(conference?.status)"></span>
|
||||
</span>
|
||||
<a class="btn btn-outline-ink" ng-href="/conference/{{conference.conferenceID}}/edit"><i class="far fa-edit mr-1" aria-hidden="true"></i> Edit conference</a>
|
||||
<a class="btn btn-outline-ink" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")"><i class="far fa-edit mr-1" aria-hidden="true"></i> Edit conference</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-meta-rule">
|
||||
<span>ID <b class="commit-hash" ng-bind="conference.conferenceID"></b></span>
|
||||
<span>Review window <b>{{conference.startDate | date:'mediumDate'}} – {{conference.endDate | date:'mediumDate'}}</b></span>
|
||||
<span>Repositories <b>{{conference.repositories.length | number}}</b></span>
|
||||
<span>Plan <b ng-bind="conference.plan.name || conference.plan.planID || 'Free'"></b></span>
|
||||
<span ng-if="conference.price">Cost so far <b>{{conference.price | number:2}} €</b></span>
|
||||
<span>ID <b class="commit-hash" v-text="conference?.conferenceID"></b></span>
|
||||
<span>Review window <b>{{ fmt?.date(conference?.startDate, 'mediumDate') }} – {{ fmt?.date(conference?.endDate, 'mediumDate') }}</b></span>
|
||||
<span>Repositories <b>{{ fmt?.number(conference?.repositories?.length) }}</b></span>
|
||||
<span>Plan <b v-text="conference?.plan?.name || conference?.plan?.planID || 'Free'"></b></span>
|
||||
<span v-if="conference?.price">Cost so far <b>{{ fmt?.number(conference?.price, 2) }} €</b></span>
|
||||
</div>
|
||||
|
||||
<div class="paper-section-eyebrow">Repositories</div>
|
||||
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter repositories" accept-charset="UTF-8" ng-submit="$event.preventDefault()">
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter repositories" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () => { $event.preventDefault() })">
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
type="search"
|
||||
id="search"
|
||||
class="form-control"
|
||||
aria-label="Search repositories"
|
||||
placeholder="Search by ID or source repository…"
|
||||
autocomplete="off"
|
||||
ng-model="search"
|
||||
/>
|
||||
<input type="search" id="search" class="form-control" aria-label="Search repositories" placeholder="Search by ID or source repository…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<div class="dashboard-filter-controls">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownSort"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="filter-btn-label">Sort</span>
|
||||
<span class="filter-btn-value">{{ ({'-anonymizeDate': 'Anonymize date', repoId: 'ID', '-status': 'Status', '-pageView': 'Views'})[orderBy] || 'Custom' }}</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownSort">
|
||||
<h6 class="dropdown-header">Order by</h6>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="anonymizeDate" value="-anonymizeDate" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="anonymizeDate" value="-anonymizeDate" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="anonymizeDate">Anonymize date</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortID" value="repoId" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortID" value="repoId" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortID">ID</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortStatus">Status</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortViews" value="-pageView" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortViews" value="-pageView" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortViews">Views</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown" ng-init="statusLabels = {ready: 'Ready', expired: 'Expired', removed: 'Removed'}">
|
||||
<button
|
||||
class="btn dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownStatus"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="filter-btn-label">Status</span>
|
||||
<span class="filter-btn-value" ng-if="filters.status.ready && filters.status.expired && filters.status.removed">All</span>
|
||||
<span class="filter-btn-value" ng-if="!(filters.status.ready && filters.status.expired && filters.status.removed)">
|
||||
{{ (filters.status.ready ? 0 : 1) + (filters.status.expired ? 0 : 1) + (filters.status.removed ? 0 : 1) }} hidden
|
||||
<span class="filter-btn-value" v-if="filters?.status?.ready && filters?.status?.expired && filters?.status?.removed">All</span>
|
||||
<span class="filter-btn-value" v-if="!(filters?.status?.ready && filters?.status?.expired && filters?.status?.removed)">
|
||||
{{ (filters?.status?.ready ? 0 : 1) + (filters?.status?.expired ? 0 : 1) + (filters?.status?.removed ? 0 : 1) }} hidden
|
||||
</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
|
||||
<h6 class="dropdown-header">Show statuses</h6>
|
||||
<div class="form-check dropdown-item" ng-repeat="(key, label) in statusLabels">
|
||||
<input class="form-check-input" type="checkbox" id="status-{{key}}" ng-model="filters.status[key]" />
|
||||
<label class="form-check-label" for="status-{{key}}">{{label}}</label>
|
||||
<div class="form-check dropdown-item" v-for="(label, key, index) in statusLabels">
|
||||
<input class="form-check-input" type="checkbox" v-field="{ state: viewState, set: value => { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id=""status-" + (key)">
|
||||
<label class="form-check-label" :for=""status-" + (key)">{{ label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,13 +80,13 @@
|
||||
|
||||
<div class="dashboard-meta">
|
||||
<span class="dashboard-count">
|
||||
<span ng-if="filteredRepositories.length === conference.repositories.length">{{conference.repositories.length | number}} repositor<span ng-if="conference.repositories.length === 1">y</span><span ng-if="conference.repositories.length !== 1">ies</span></span>
|
||||
<span ng-if="filteredRepositories.length !== conference.repositories.length">{{filteredRepositories.length | number}} of {{conference.repositories.length | number}} shown</span>
|
||||
</span>
|
||||
<span class="filter-chip" ng-repeat="(f, v) in filters.status" ng-if="v === false">
|
||||
Hiding {{statusLabels[f]}}
|
||||
<button type="button" class="filter-chip-close" aria-label="Show {{statusLabels[f]}} again" ng-click="filters.status[f] = true;">×</button>
|
||||
<span v-if="filteredRepositories?.length === conference?.repositories?.length">{{ fmt?.number(conference?.repositories?.length) }} repositor<span v-if="conference?.repositories?.length === 1">y</span><span v-if="conference?.repositories?.length !== 1">ies</span></span>
|
||||
<span v-if="filteredRepositories?.length !== conference?.repositories?.length">{{ fmt?.number(filteredRepositories?.length) }} of {{ fmt?.number(conference?.repositories?.length) }} shown</span>
|
||||
</span>
|
||||
<template v-for="(v, f, index) in filters?.status"><span class="filter-chip" v-if="v === false">
|
||||
Hiding {{ statusLabels[f] }}
|
||||
<button type="button" class="filter-chip-close" @click="filters.status[f] = true;" :aria-label=""Show " + (statusLabels[f]) + " again"">×</button>
|
||||
</span></template>
|
||||
</div>
|
||||
|
||||
<div class="paper-table paper-table-repos w-100" role="table" aria-label="Repositories">
|
||||
@@ -119,66 +97,54 @@
|
||||
<div role="columnheader">Expires</div>
|
||||
<div role="columnheader"><span class="sr-only">Actions</span></div>
|
||||
</div>
|
||||
<div
|
||||
class="paper-table-row"
|
||||
role="row"
|
||||
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed', 'repo-error': repo.status == 'error'}"
|
||||
ng-repeat="repo in conference.repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories track by repo.repoId"
|
||||
>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :key="repo?.repoId" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error'}">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" ng-href="/r/{{repo.repoId}}/" ng-bind="repo.repoId"></a>
|
||||
<a class="repo-name" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId) + "/")"></a>
|
||||
<div class="anon-sub">
|
||||
<span class="anon-source">
|
||||
<a href="https://github.com/{{repo.source.fullName}}/" target="_blank" rel="noopener" ng-bind="repo.source.fullName"></a><span ng-if="repo.options.update && repo.source.branch"> · <a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.branch}}" target="_blank" rel="noopener" ng-bind="repo.source.branch"></a></span><span ng-if="repo.source.commit"> · <a class="commit-hash" href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.commit}}" target="_blank" rel="noopener">@{{repo.source.commit.substring(0, 8)}}</a></span>
|
||||
<a target="_blank" rel="noopener" v-text="repo?.source?.fullName" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")"></a><span v-if="repo?.options?.update && repo?.source?.branch"> · <a target="_blank" rel="noopener" v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="repo?.source?.commit"> · <a class="commit-hash" target="_blank" rel="noopener" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))">@{{ repo?.source?.commit?.substring(0, 8) }}</a></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<div class="status-line">
|
||||
<span class="status-dot" ng-class="'status-' + (repo.status == 'error' ? 'error' : repo.status == 'ready' ? 'ready' : (repo.status == 'expired' || repo.status == 'removed') ? repo.status : 'progress')" aria-hidden="true"></span>
|
||||
<span class="status-word" ng-bind="repo.status | statusLabel"></span>
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + (repo?.status == 'error' ? 'error' : repo?.status == 'ready' ? 'ready' : (repo?.status == 'expired' || repo?.status == 'removed') ? repo?.status : 'progress')"></span>
|
||||
<span class="status-word" v-text="fmt?.statusLabel(repo?.status)"></span>
|
||||
</div>
|
||||
<div class="status-sub" ng-if="repo.anonymizeDate">anonymized {{repo.anonymizeDate | humanTime}}</div>
|
||||
<div class="status-sub" v-if="repo?.anonymizeDate">anonymized {{ fmt?.humanTime(repo?.anonymizeDate) }}</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" ng-bind="::repo.pageView | number"></div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView)"></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<span class="expires-never" ng-if="repo.status == 'ready' && (repo.options.expirationMode === 'never' || !repo.options.expirationDate)">Never</span>
|
||||
<span ng-if="repo.status == 'ready' && repo.options.expirationMode !== 'never' && repo.options.expirationDate" ng-bind="repo.options.expirationDate | humanTime"></span>
|
||||
<span class="expires-past" ng-if="repo.status == 'expired'">Expired<span ng-if="repo.options.expirationDate"> {{repo.options.expirationDate | humanTime}}</span></span>
|
||||
<span class="empty-dash" ng-if="repo.status != 'ready' && repo.status != 'expired'" aria-label="Not applicable">—</span>
|
||||
<span class="expires-never" v-if="repo?.status == 'ready' && (repo?.options?.expirationMode === 'never' || !repo?.options?.expirationDate)">Never</span>
|
||||
<span v-if="repo?.status == 'ready' && repo?.options?.expirationMode !== 'never' && repo?.options?.expirationDate" v-text="fmt?.humanTime(repo?.options?.expirationDate)"></span>
|
||||
<span class="expires-past" v-if="repo?.status == 'expired'">Expired<span v-if="repo?.options?.expirationDate"> {{ fmt?.humanTime(repo?.options?.expirationDate) }}</span></span>
|
||||
<span class="empty-dash" aria-label="Not applicable" v-if="repo?.status != 'ready' && repo?.status != 'expired'">—</span>
|
||||
</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 for {{repo.repoId}}"
|
||||
>
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :aria-label=""Actions for " + (repo?.repoId)">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="/r/{{repo.repoId}}/">
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View
|
||||
</a>
|
||||
<a class="dropdown-item" href="/anonymize/{{repo.repoId}}">
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
</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" aria-hidden="true"></i>
|
||||
<span ng-if="!conference.repositories.length">No repository has been submitted to this conference yet.</span>
|
||||
<span ng-if="conference.repositories.length">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions" ng-if="conference.repositories.length">
|
||||
<button type="button" class="btn btn-outline-ink" ng-click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
|
||||
<span v-if="!conference?.repositories?.length">No repository has been submitted to this conference yet.</span>
|
||||
<span v-if="conference?.repositories?.length">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions" v-if="conference?.repositories?.length">
|
||||
<button type="button" class="btn btn-outline-ink" @click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,68 +12,46 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter conferences" accept-charset="UTF-8" ng-submit="$event.preventDefault()">
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter conferences" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () => { $event.preventDefault() })">
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
type="search"
|
||||
id="search"
|
||||
class="form-control"
|
||||
aria-label="Search conferences"
|
||||
placeholder="Search by name or ID…"
|
||||
autocomplete="off"
|
||||
ng-model="search"
|
||||
/>
|
||||
<input type="search" id="search" class="form-control" aria-label="Search conferences" placeholder="Search by name or ID…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<div class="dashboard-filter-controls">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownSort"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="filter-btn-label">Sort</span>
|
||||
<span class="filter-btn-value">{{ ({name: 'Name', conferenceID: 'ID', '-status': 'Status'})[orderBy] || 'Custom' }}</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownSort">
|
||||
<h6 class="dropdown-header">Order by</h6>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortName" value="name" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortName" value="name" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortName">Name</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortID" value="conferenceID" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortID" value="conferenceID" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortID">ID</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortStatus">Status</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown" ng-init="statusLabels = {ready: 'Ready', expired: 'Expired', removed: 'Removed'}">
|
||||
<button
|
||||
class="btn dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownStatus"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="filter-btn-label">Status</span>
|
||||
<span class="filter-btn-value" ng-if="filters.status.ready && filters.status.expired && filters.status.removed">All</span>
|
||||
<span class="filter-btn-value" ng-if="!(filters.status.ready && filters.status.expired && filters.status.removed)">
|
||||
{{ (filters.status.ready ? 0 : 1) + (filters.status.expired ? 0 : 1) + (filters.status.removed ? 0 : 1) }} hidden
|
||||
<span class="filter-btn-value" v-if="filters?.status?.ready && filters?.status?.expired && filters?.status?.removed">All</span>
|
||||
<span class="filter-btn-value" v-if="!(filters?.status?.ready && filters?.status?.expired && filters?.status?.removed)">
|
||||
{{ (filters?.status?.ready ? 0 : 1) + (filters?.status?.expired ? 0 : 1) + (filters?.status?.removed ? 0 : 1) }} hidden
|
||||
</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
|
||||
<h6 class="dropdown-header">Show statuses</h6>
|
||||
<div class="form-check dropdown-item" ng-repeat="(key, label) in statusLabels">
|
||||
<input class="form-check-input" type="checkbox" id="status-{{key}}" ng-model="filters.status[key]" />
|
||||
<label class="form-check-label" for="status-{{key}}">{{label}}</label>
|
||||
<div class="form-check dropdown-item" v-for="(label, key, index) in statusLabels">
|
||||
<input class="form-check-input" type="checkbox" v-field="{ state: viewState, set: value => { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id=""status-" + (key)">
|
||||
<label class="form-check-label" :for=""status-" + (key)">{{ label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,13 +60,13 @@
|
||||
|
||||
<div class="dashboard-meta">
|
||||
<span class="dashboard-count">
|
||||
<span ng-if="filteredConferences.length === conferences.length">{{conferences.length | number}} conference<span ng-if="conferences.length !== 1">s</span></span>
|
||||
<span ng-if="filteredConferences.length !== conferences.length">{{filteredConferences.length | number}} of {{conferences.length | number}} shown</span>
|
||||
</span>
|
||||
<span class="filter-chip" ng-repeat="(f, v) in filters.status" ng-if="v === false">
|
||||
Hiding {{statusLabels[f]}}
|
||||
<button type="button" class="filter-chip-close" aria-label="Show {{statusLabels[f]}} again" ng-click="filters.status[f] = true;">×</button>
|
||||
<span v-if="filteredConferences?.length === conferences?.length">{{ fmt?.number(conferences?.length) }} conference<span v-if="conferences?.length !== 1">s</span></span>
|
||||
<span v-if="filteredConferences?.length !== conferences?.length">{{ fmt?.number(filteredConferences?.length) }} of {{ fmt?.number(conferences?.length) }} shown</span>
|
||||
</span>
|
||||
<template v-for="(v, f, index) in filters?.status"><span class="filter-chip" v-if="v === false">
|
||||
Hiding {{ statusLabels[f] }}
|
||||
<button type="button" class="filter-chip-close" @click="filters.status[f] = true;" :aria-label=""Show " + (statusLabels[f]) + " again"">×</button>
|
||||
</span></template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -100,54 +78,42 @@
|
||||
<div role="columnheader">Review window</div>
|
||||
<div role="columnheader"><span class="sr-only">Actions</span></div>
|
||||
</div>
|
||||
<div
|
||||
class="paper-table-row row-clickable"
|
||||
role="row"
|
||||
ng-class="{'repo-inactive': conference.status == 'expired' || conference.status == 'removed'}"
|
||||
ng-repeat="conference in conferences | filter:conferenceFilter | orderBy:orderBy as filteredConferences track by conference.conferenceID"
|
||||
>
|
||||
<div class="paper-table-row row-clickable" role="row" v-for="(conference, index) in filteredConferences" :key="conference?.conferenceID" :class="{'repo-inactive': conference?.status == 'expired' || conference?.status == 'removed'}">
|
||||
<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 class="anon-source">ID <span class="commit-hash" ng-bind="conference.conferenceID"></span><span ng-if="conference.url"> · <a ng-href="{{conference.url}}" target="_blank" rel="noopener" ng-bind="conference.url | limitTo: 60"></a></span></span>
|
||||
<span class="anon-source">ID <span class="commit-hash" v-text="conference?.conferenceID"></span><span v-if="conference?.url"> · <a target="_blank" rel="noopener" v-text="fmt?.limitTo(conference?.url, 60)" :href="safeUrl((conference?.url))"></a></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<div class="status-line">
|
||||
<span class="status-dot" ng-class="'status-' + conference.status" aria-hidden="true"></span>
|
||||
<span class="status-word" ng-bind="conference.status | statusLabel"></span>
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + conference?.status"></span>
|
||||
<span class="status-word" v-text="fmt?.statusLabel(conference?.status)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" ng-bind="::conference.nbRepositories || 0 | number"></div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(conference?.nbRepositories || 0)"></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<span ng-if="conference.startDate">{{conference.startDate | date:'mediumDate'}} – {{conference.endDate | date:'mediumDate'}}</span>
|
||||
<span class="empty-dash" ng-if="!conference.startDate" aria-label="No review window">—</span>
|
||||
<span v-if="conference?.startDate">{{ fmt?.date(conference?.startDate, 'mediumDate') }} – {{ fmt?.date(conference?.endDate, 'mediumDate') }}</span>
|
||||
<span class="empty-dash" aria-label="No review window" v-if="!conference?.startDate">—</span>
|
||||
</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 for {{conference.name}}"
|
||||
>
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :aria-label=""Actions for " + (conference?.name)">
|
||||
<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}}/">
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/")">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View
|
||||
</a>
|
||||
<a class="dropdown-item" href="/conference/{{conference.conferenceID}}/edit">
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
</a>
|
||||
<div ng-if="conference.status != 'removed'">
|
||||
<div v-if="conference?.status != 'removed'">
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="button" class="dropdown-item dropdown-item-danger" ng-click="removeConference(conference)">
|
||||
<button type="button" class="dropdown-item dropdown-item-danger" @click="removeConference(conference)">
|
||||
<i class="fas fa-trash-alt" aria-hidden="true"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
@@ -155,13 +121,13 @@
|
||||
</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" aria-hidden="true"></i>
|
||||
<span ng-if="conferences.length == 0">You have not created a conference yet.</span>
|
||||
<span ng-if="conferences.length > 0">Nothing matches the current filters.</span>
|
||||
<span v-if="conferences?.length == 0">You have not created a conference yet.</span>
|
||||
<span v-if="conferences?.length > 0">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions">
|
||||
<button type="button" class="btn btn-outline-ink" ng-if="conferences.length > 0" ng-click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
|
||||
<a href="/conference/new" class="btn btn-ink" ng-if="conferences.length == 0"><i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New conference</a>
|
||||
<button type="button" class="btn btn-outline-ink" v-if="conferences?.length > 0" @click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
|
||||
<a href="/conference/new" class="btn btn-ink" v-if="conferences?.length == 0"><i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New conference</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+100
-145
@@ -13,105 +13,74 @@
|
||||
</div>
|
||||
|
||||
<!-- Quota -->
|
||||
<div class="quota-row" ng-if="quota">
|
||||
<div class="quota-item" ng-repeat="q in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes'},
|
||||
{key: 'file', label: 'Files', kind: 'count'}
|
||||
<div class="quota-row" v-if="quota">
|
||||
<div class="quota-item" v-for="(q, index) in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes'},
|
||||
{key: 'file', label: 'Files', kind: 'count'}
|
||||
]">
|
||||
<div class="quota-header">
|
||||
<span class="quota-label">{{q.label}}</span>
|
||||
<span class="quota-value" ng-if="q.kind === 'count'">
|
||||
{{quota[q.key].used | number}}<span ng-if="!quota[q.key].unlimited"> / {{quota[q.key].total | number}}</span>
|
||||
<span class="quota-unlimited-tag" ng-if="quota[q.key].unlimited">Unlimited</span>
|
||||
<span class="quota-label">{{ q?.label }}</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'count'">
|
||||
{{ fmt?.number(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.number(quota[q?.key].total) }}</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
<span class="quota-value" ng-if="q.kind === 'bytes'">
|
||||
{{quota[q.key].used | humanFileSize}}<span ng-if="!quota[q.key].unlimited"> / {{quota[q.key].total | humanFileSize}}</span>
|
||||
<span class="quota-unlimited-tag" ng-if="quota[q.key].unlimited">Unlimited</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'bytes'">
|
||||
{{ fmt?.humanFileSize(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.humanFileSize(quota[q?.key].total) }}</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="quota-track"
|
||||
ng-class="'quota-' + quota[q.key].level"
|
||||
role="progressbar"
|
||||
aria-label="{{q.label}} quota"
|
||||
aria-valuenow="{{quota[q.key].used}}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="{{quota[q.key].unlimited ? quota[q.key].used : quota[q.key].total}}"
|
||||
aria-valuetext="{{quota[q.key].unlimited ? 'unlimited' : (quota[q.key].percent | number:0) + '% used'}}"
|
||||
>
|
||||
<div class="quota-fill" ng-if="!quota[q.key].unlimited" ng-style="{width: quota[q.key].percent + '%'}"></div>
|
||||
<div class="quota-track" role="progressbar" aria-valuemin="0" :class="'quota-' + quota[q?.key].level" :aria-label="(q?.label) + " quota"" :aria-valuenow="(quota[q?.key].used)" :aria-valuemax="(quota[q?.key].unlimited ? quota[q?.key].used : quota[q?.key].total)" :aria-valuetext="(quota[q?.key].unlimited ? 'unlimited' : fmt.number(quota[q?.key].percent, 0) + '% used')">
|
||||
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + filters -->
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter anonymizations" accept-charset="UTF-8" ng-submit="$event.preventDefault()">
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter anonymizations" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () => { $event.preventDefault() })">
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
type="search"
|
||||
id="search"
|
||||
class="form-control"
|
||||
aria-label="Search anonymizations"
|
||||
placeholder="Search by name, source, or conference…"
|
||||
autocomplete="off"
|
||||
ng-model="search"
|
||||
/>
|
||||
<input type="search" id="search" class="form-control" aria-label="Search anonymizations" placeholder="Search by name, source, or conference…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<div class="dashboard-filter-controls">
|
||||
<div class="btn-group" role="group" aria-label="Type">
|
||||
<button type="button" class="btn" ng-class="{'btn-primary': typeFilter === 'all'}" aria-pressed="{{typeFilter === 'all'}}" ng-click="typeFilter = 'all'">All</button>
|
||||
<button type="button" class="btn" ng-class="{'btn-primary': typeFilter === 'repo'}" aria-pressed="{{typeFilter === 'repo'}}" ng-click="typeFilter = 'repo'">Repos</button>
|
||||
<button type="button" class="btn" ng-class="{'btn-primary': typeFilter === 'pr'}" aria-pressed="{{typeFilter === 'pr'}}" ng-click="typeFilter = 'pr'">PRs</button>
|
||||
<button type="button" class="btn" ng-class="{'btn-primary': typeFilter === 'gist'}" aria-pressed="{{typeFilter === 'gist'}}" ng-click="typeFilter = 'gist'">Gists</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'all'}" @click="typeFilter = 'all'" :aria-pressed="(typeFilter === 'all')">All</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'repo'}" @click="typeFilter = 'repo'" :aria-pressed="(typeFilter === 'repo')">Repos</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'pr'}" @click="typeFilter = 'pr'" :aria-pressed="(typeFilter === 'pr')">PRs</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'gist'}" @click="typeFilter = 'gist'" :aria-pressed="(typeFilter === 'gist')">Gists</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownSort"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="filter-btn-label">Sort</span>
|
||||
<span class="filter-btn-value">{{sortLabel()}}</span>
|
||||
<i class="fas" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{sortDesc() ? 'descending' : 'ascending'}}</span>
|
||||
<span class="filter-btn-value">{{ sortLabel() }}</span>
|
||||
<i class="fas" aria-hidden="true" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
<span class="sr-only">{{ sortDesc() ? 'descending' : 'ascending' }}</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownSort">
|
||||
<h6 class="dropdown-header">Order by</h6>
|
||||
<div class="form-check dropdown-item" ng-repeat="(field, def) in sortFields">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sort-{{$index}}" ng-checked="isSortedBy(field)" ng-click="setSort(field, def.defaultDesc)" />
|
||||
<label class="form-check-label" for="sort-{{$index}}">{{def.label}}</label>
|
||||
<div class="form-check dropdown-item" v-for="(def, field, index) in sortFields">
|
||||
<input class="form-check-input" type="radio" name="sort" @click="setSort(field, def.defaultDesc)" :checked="isSortedBy(field)" :id=""sort-" + (index)">
|
||||
<label class="form-check-label" :for=""sort-" + (index)">{{ def?.label }}</label>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="button" class="dropdown-item" ng-click="toggleSortDirection()">
|
||||
<button type="button" class="dropdown-item" @click="toggleSortDirection()">
|
||||
<i class="fas fa-exchange-alt fa-rotate-90" aria-hidden="true"></i>
|
||||
{{sortDesc() ? 'Switch to ascending' : 'Switch to descending'}}
|
||||
{{ sortDesc() ? 'Switch to ascending' : 'Switch to descending' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownStatus"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="filter-btn-label">Status</span>
|
||||
<span class="filter-btn-value" ng-if="!hasHiddenStatus()">All</span>
|
||||
<span class="filter-btn-value" ng-if="hasHiddenStatus()">{{hiddenStatusCount()}} hidden</span>
|
||||
<span class="filter-btn-value" v-if="!hasHiddenStatus()">All</span>
|
||||
<span class="filter-btn-value" v-if="hasHiddenStatus()">{{ hiddenStatusCount() }} hidden</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
|
||||
<h6 class="dropdown-header">Show statuses</h6>
|
||||
<div class="form-check dropdown-item" ng-repeat="(key, label) in statusKeyLabels">
|
||||
<input class="form-check-input" type="checkbox" id="status-{{key}}" ng-model="filters.status[key]" />
|
||||
<label class="form-check-label" for="status-{{key}}">{{label}}</label>
|
||||
<div class="form-check dropdown-item" v-for="(label, key, index) in statusKeyLabels">
|
||||
<input class="form-check-input" type="checkbox" v-field="{ state: viewState, set: value => { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id=""status-" + (key)">
|
||||
<label class="form-check-label" :for=""status-" + (key)">{{ label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,154 +88,140 @@
|
||||
</form>
|
||||
|
||||
<!-- Result count + active filter chips -->
|
||||
<div class="dashboard-meta" ng-if="!loading">
|
||||
<div class="dashboard-meta" v-if="!loading">
|
||||
<span class="dashboard-count">
|
||||
<span ng-if="filteredItems.length === items.length">{{items.length | number}} anonymization<span ng-if="items.length !== 1">s</span></span>
|
||||
<span ng-if="filteredItems.length !== items.length">{{filteredItems.length | number}} of {{items.length | number}} shown</span>
|
||||
<span v-if="filteredItems?.length === items?.length">{{ fmt?.number(items?.length) }} anonymization<span v-if="items?.length !== 1">s</span></span>
|
||||
<span v-if="filteredItems?.length !== items?.length">{{ fmt?.number(filteredItems?.length) }} of {{ fmt?.number(items?.length) }} shown</span>
|
||||
</span>
|
||||
<span class="filter-chip" ng-repeat="(f, v) in filters.status" ng-if="v === false">
|
||||
Hiding {{statusKeyLabels[f]}}
|
||||
<button type="button" class="filter-chip-close" aria-label="Show {{statusKeyLabels[f]}} again" ng-click="filters.status[f] = true;">×</button>
|
||||
</span>
|
||||
<button type="button" class="btn-link-inline" ng-if="hasActiveFilters()" ng-click="clearFilters()">Clear filters</button>
|
||||
<template v-for="(v, f, index) in filters?.status"><span class="filter-chip" v-if="v === false">
|
||||
Hiding {{ statusKeyLabels[f] }}
|
||||
<button type="button" class="filter-chip-close" @click="filters.status[f] = true;" :aria-label=""Show " + (statusKeyLabels[f]) + " again"">×</button>
|
||||
</span></template>
|
||||
<button type="button" class="btn-link-inline" v-if="hasActiveFilters()" @click="clearFilters()">Clear filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="paper-table paper-table-dashboard w-100" role="table" aria-label="Anonymizations" aria-busy="{{loading}}">
|
||||
<div class="paper-table paper-table-dashboard w-100" role="table" aria-label="Anonymizations" :aria-busy="(loading)">
|
||||
<div class="paper-table-head" role="row">
|
||||
<div role="columnheader" aria-sort="{{isSortedBy('_name') ? (sortDesc() ? 'descending' : 'ascending') : 'none'}}">
|
||||
<button type="button" class="sortable" ng-class="{active: isSortedBy('_name')}" ng-click="setSort('_name')">
|
||||
Anonymization <i class="fas" ng-if="isSortedBy('_name')" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i>
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('_name') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('_name')}" @click="setSort('_name')">
|
||||
Anonymization <i class="fas" aria-hidden="true" v-if="isSortedBy('_name')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader" aria-sort="{{isSortedBy('status') ? (sortDesc() ? 'descending' : 'ascending') : 'none'}}">
|
||||
<button type="button" class="sortable" ng-class="{active: isSortedBy('status')}" ng-click="setSort('status')">
|
||||
Status <i class="fas" ng-if="isSortedBy('status')" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i>
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('status') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('status')}" @click="setSort('status')">
|
||||
Status <i class="fas" aria-hidden="true" v-if="isSortedBy('status')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader" class="num" aria-sort="{{isSortedBy('pageView') ? (sortDesc() ? 'descending' : 'ascending') : 'none'}}">
|
||||
<button type="button" class="sortable" ng-class="{active: isSortedBy('pageView')}" ng-click="setSort('pageView')">
|
||||
Views <i class="fas" ng-if="isSortedBy('pageView')" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i>
|
||||
<div role="columnheader" class="num" :aria-sort="(isSortedBy('pageView') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('pageView')}" @click="setSort('pageView')">
|
||||
Views <i class="fas" aria-hidden="true" v-if="isSortedBy('pageView')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader" aria-sort="{{isSortedBy('options.expirationDate') ? (sortDesc() ? 'descending' : 'ascending') : 'none'}}">
|
||||
<button type="button" class="sortable" ng-class="{active: isSortedBy('options.expirationDate')}" ng-click="setSort('options.expirationDate')">
|
||||
Expires <i class="fas" ng-if="isSortedBy('options.expirationDate')" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i>
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('options.expirationDate') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('options.expirationDate')}" @click="setSort('options.expirationDate')">
|
||||
Expires <i class="fas" aria-hidden="true" v-if="isSortedBy('options.expirationDate')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader"><span class="sr-only">Actions</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Loading skeleton: keeps the layout stable while the three lists load -->
|
||||
<div class="paper-table-row paper-table-skeleton" role="row" aria-hidden="true" ng-if="loading" ng-repeat="i in [1, 2, 3, 4]">
|
||||
<template v-for="(i, index) in [1, 2, 3, 4]"><div class="paper-table-row paper-table-skeleton" role="row" aria-hidden="true" v-if="loading">
|
||||
<div class="cell-anon" role="cell"><span class="skeleton skeleton-badge"></span><div class="anon-text"><span class="skeleton skeleton-line" style="width: 38%"></span><span class="skeleton skeleton-line skeleton-line-sm" style="width: 56%"></span></div></div>
|
||||
<div class="cell-status" role="cell"><span class="skeleton skeleton-line" style="width: 60%"></span></div>
|
||||
<div class="cell-views num" role="cell"><span class="skeleton skeleton-line" style="width: 40%"></span></div>
|
||||
<div class="cell-expires" role="cell"><span class="skeleton skeleton-line" style="width: 55%"></span></div>
|
||||
<div class="cell-actions" role="cell"></div>
|
||||
</div>
|
||||
</div></template>
|
||||
|
||||
<div
|
||||
class="paper-table-row"
|
||||
role="row"
|
||||
ng-if="!loading"
|
||||
ng-class="{'repo-inactive': item._statusKey == 'expired' || item._statusKey == 'removed', 'repo-error': item.status == 'error', 'row-clickable': !!item._viewUrl}"
|
||||
ng-repeat="item in items | filter:itemFilter | orderBy:orderBy as filteredItems track by (item._type + ':' + (item._id || $index))"
|
||||
ng-click="openItem(item, $event)"
|
||||
>
|
||||
<template v-for="(item, index) in filteredItems" :key="(item?._type + ':' + (item?._id || index))"><div class="paper-table-row" role="row" v-if="!loading" :class="{'repo-inactive': item?._statusKey == 'expired' || item?._statusKey == 'removed', 'repo-error': item?.status == 'error', 'row-clickable': !!item?._viewUrl}" @click="openItem(item, $event)">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge" ng-class="{'type-repo': item._type === 'repo', 'type-pr': item._type === 'pr', 'type-gist': item._type === 'gist'}">{{item._type === 'repo' ? 'Repo' : item._type === 'pr' ? 'PR' : 'Gist'}}</span>
|
||||
<span class="type-badge" :class="{'type-repo': item?._type === 'repo', 'type-pr': item?._type === 'pr', 'type-gist': item?._type === 'gist'}">{{ item?._type === 'repo' ? 'Repo' : item?._type === 'pr' ? 'PR' : 'Gist' }}</span>
|
||||
<div class="anon-text">
|
||||
<div class="anon-title">
|
||||
<a ng-if="item._viewUrl" ng-href="{{item._viewUrl}}" class="repo-name" ng-bind="item._name"></a>
|
||||
<span ng-if="!item._viewUrl" class="repo-name repo-name-static" ng-bind="item._name"></span>
|
||||
<span class="type-badge type-coauthor" ng-if="item.role === 'coauthor'" title="You are a co-author on this anonymization">Co-author</span>
|
||||
<a class="repo-name" v-if="item?._viewUrl" v-text="item?._name" :href="safeUrl((item?._viewUrl))"></a>
|
||||
<span class="repo-name repo-name-static" v-if="!item?._viewUrl" v-text="item?._name"></span>
|
||||
<span class="type-badge type-coauthor" title="You are a co-author on this anonymization" v-if="item?.role === 'coauthor'">Co-author</span>
|
||||
</div>
|
||||
<div class="anon-sub" ng-if="!item._broken || item.conference">
|
||||
<span class="anon-source" ng-if="item._type === 'repo' && item.source.fullName && !item._broken">
|
||||
<a href="https://github.com/{{item.source.fullName}}/" target="_blank" rel="noopener" ng-bind="item.source.fullName"></a><span ng-if="item.options.update && item.source.branch"> · <a href="https://github.com/{{item.source.fullName}}/tree/{{item.source.branch}}" target="_blank" rel="noopener" ng-bind="item.source.branch"></a></span><span ng-if="item.source.commit"> · <a class="commit-hash" href="https://github.com/{{item.source.fullName}}/tree/{{item.source.commit}}" target="_blank" rel="noopener">@{{item.source.commit.substring(0, 8)}}</a></span>
|
||||
<div class="anon-sub" v-if="!item?._broken || item?.conference">
|
||||
<span class="anon-source" v-if="item?._type === 'repo' && item?.source?.fullName && !item?._broken">
|
||||
<a target="_blank" rel="noopener" v-text="item?.source?.fullName" :href="safeUrl("https://github.com/" + (item?.source?.fullName) + "/")"></a><span v-if="item?.options?.update && item?.source?.branch"> · <a target="_blank" rel="noopener" v-text="item?.source?.branch" :href="safeUrl("https://github.com/" + (item?.source?.fullName) + "/tree/" + (item?.source?.branch))"></a></span><span v-if="item?.source?.commit"> · <a class="commit-hash" target="_blank" rel="noopener" :href="safeUrl("https://github.com/" + (item?.source?.fullName) + "/tree/" + (item?.source?.commit))">@{{ item?.source?.commit?.substring(0, 8) }}</a></span>
|
||||
</span>
|
||||
<span class="anon-source" ng-if="item._type === 'pr' && item.source.repositoryFullName && !item._broken">
|
||||
<a href="https://github.com/{{item.source.repositoryFullName}}/pull/{{item.source.pullRequestId}}" target="_blank" rel="noopener" ng-bind="item._source"></a>
|
||||
<span class="anon-source" v-if="item?._type === 'pr' && item?.source?.repositoryFullName && !item?._broken">
|
||||
<a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl("https://github.com/" + (item?.source?.repositoryFullName) + "/pull/" + (item?.source?.pullRequestId))"></a>
|
||||
</span>
|
||||
<span class="anon-source" ng-if="item._type === 'gist' && item.source.gistId && !item._broken">
|
||||
<a href="https://gist.github.com/{{item.source.gistId}}" target="_blank" rel="noopener" ng-bind="item._source"></a>
|
||||
<span class="anon-source" v-if="item?._type === 'gist' && item?.source?.gistId && !item?._broken">
|
||||
<a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl("https://gist.github.com/" + (item?.source?.gistId))"></a>
|
||||
</span>
|
||||
<span class="cell-conf conf-tag" ng-if="item.conference" title="{{item.conference}}"><i class="fas fa-university" aria-hidden="true"></i> <span ng-bind="item.conference"></span></span>
|
||||
<span class="cell-conf conf-tag" v-if="item?.conference" :title="(item?.conference)"><i class="fas fa-university" aria-hidden="true"></i> <span v-text="item?.conference"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<div class="status-line">
|
||||
<span class="status-dot" ng-class="'status-' + item._statusKey" aria-hidden="true"></span>
|
||||
<span class="status-word" ng-bind="item.status | statusLabel"></span>
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + item?._statusKey"></span>
|
||||
<span class="status-word" v-text="fmt?.statusLabel(item?.status)"></span>
|
||||
</div>
|
||||
<div class="status-sub status-sub-error" ng-if="item.status == 'error' && item.statusMessage" title="{{item.statusMessage}}" ng-bind="item.statusMessage | statusMsg"></div>
|
||||
<div class="status-sub status-sub-warn" ng-if="item._stale" title="Last activity {{item.anonymizeDate || item.lastView | humanTime}}">
|
||||
Last activity {{item.anonymizeDate || item.lastView | humanTime}} · may be stuck
|
||||
<div class="status-sub status-sub-error" v-if="item?.status == 'error' && item?.statusMessage" v-text="fmt?.statusMsg(item?.statusMessage)" :title="(item?.statusMessage)"></div>
|
||||
<div class="status-sub status-sub-warn" v-if="item?._stale" :title=""Last activity " + (fmt?.humanTime(item?.anonymizeDate || item?.lastView))">
|
||||
Last activity {{ fmt?.humanTime(item?.anonymizeDate || item?.lastView) }} · may be stuck
|
||||
</div>
|
||||
<div class="status-sub" ng-if="item.status != 'error' && !item._stale && item.anonymizeDate">
|
||||
anonymized {{item.anonymizeDate | humanTime}}
|
||||
<div class="status-sub" v-if="item?.status != 'error' && !item?._stale && item?.anonymizeDate">
|
||||
anonymized {{ fmt?.humanTime(item?.anonymizeDate) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" ng-bind="item.pageView | number"></div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(item?.pageView)"></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<span ng-if="item._expiry.kind === 'never'" class="expires-never">Never</span>
|
||||
<span ng-if="item._expiry.kind === 'date'" ng-bind="item._expiry.date | humanTime"></span>
|
||||
<span ng-if="item._expiry.kind === 'expired' && item._expiry.date" class="expires-past">Expired {{item._expiry.date | humanTime}}</span>
|
||||
<span ng-if="item._expiry.kind === 'expired' && !item._expiry.date" class="expires-past">Expired</span>
|
||||
<span class="empty-dash" ng-if="item._expiry.kind === 'none'" aria-label="Not applicable">—</span>
|
||||
<span class="expires-never" v-if="item?._expiry?.kind === 'never'">Never</span>
|
||||
<span v-if="item?._expiry?.kind === 'date'" v-text="fmt?.humanTime(item?._expiry?.date)"></span>
|
||||
<span class="expires-past" v-if="item?._expiry?.kind === 'expired' && item?._expiry?.date">Expired {{ fmt?.humanTime(item?._expiry?.date) }}</span>
|
||||
<span class="expires-past" v-if="item?._expiry?.kind === 'expired' && !item?._expiry?.date">Expired</span>
|
||||
<span class="empty-dash" aria-label="Not applicable" v-if="item?._expiry?.kind === 'none'">—</span>
|
||||
</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown" ng-if="!item._broken">
|
||||
<button
|
||||
class="btn btn-icon-dots"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-label="Actions for {{item._name}}"
|
||||
>
|
||||
<div class="dropdown" v-if="!item?._broken">
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :aria-label=""Actions for " + (item?._name)">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" ng-href="{{item._viewUrl}}">
|
||||
<a class="dropdown-item" :href="safeUrl((item?._viewUrl))">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View
|
||||
</a>
|
||||
<a class="dropdown-item" href="/w/{{item.repoId}}/" target="_self" ng-if="item._type === 'repo' && item.options.page && item.status == 'ready'">
|
||||
<a class="dropdown-item" target="_self" v-if="item?._type === 'repo' && item?.options?.page && item?.status == 'ready'" :href="safeUrl("/w/" + (item?.repoId) + "/")">
|
||||
<i class="fas fa-globe" aria-hidden="true"></i> View page
|
||||
</a>
|
||||
<a class="dropdown-item" ng-href="{{item._editUrl}}">
|
||||
<a class="dropdown-item" :href="safeUrl((item?._editUrl))">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
</a>
|
||||
<button type="button" class="dropdown-item" ng-if="item.status == 'ready' || item.status == 'error'" ng-click="refreshItem(item)">
|
||||
<button type="button" class="dropdown-item" v-if="item?.status == 'ready' || item?.status == 'error'" @click="refreshItem(item)">
|
||||
<i class="fas fa-sync" aria-hidden="true"></i> Force update
|
||||
</button>
|
||||
<button type="button" class="dropdown-item" ng-if="item.status == 'removed'" ng-click="refreshItem(item)">
|
||||
<button type="button" class="dropdown-item" v-if="item?.status == 'removed'" @click="refreshItem(item)">
|
||||
<i class="fas fa-check-circle" aria-hidden="true"></i> Enable
|
||||
</button>
|
||||
<button type="button" class="dropdown-item" ng-if="item.status == 'expired'" ng-click="extendItem(item)">
|
||||
<button type="button" class="dropdown-item" v-if="item?.status == 'expired'" @click="extendItem(item)">
|
||||
<i class="fas fa-calendar-plus" aria-hidden="true"></i> Extend 6 months
|
||||
</button>
|
||||
<div ng-if="(item.status == 'ready' || item.status == 'expired' || item.status == 'error') && item.role !== 'coauthor'">
|
||||
<div v-if="(item?.status == 'ready' || item?.status == 'expired' || item?.status == 'error') && item?.role !== 'coauthor'">
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="button" class="dropdown-item dropdown-item-danger" ng-click="removeItem(item)">
|
||||
<button type="button" class="dropdown-item dropdown-item-danger" @click="removeItem(item)">
|
||||
<i class="fas fa-trash-alt" aria-hidden="true"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></template>
|
||||
|
||||
<div class="paper-table-empty" ng-if="!loading && filteredItems.length == 0">
|
||||
<div class="paper-table-empty" v-if="!loading && filteredItems?.length == 0">
|
||||
<i class="fas fa-inbox" aria-hidden="true"></i>
|
||||
<span ng-if="items.length == 0">You have no anonymizations yet.</span>
|
||||
<span ng-if="items.length > 0">Nothing matches the current filters.</span>
|
||||
<span v-if="items?.length == 0">You have no anonymizations yet.</span>
|
||||
<span v-if="items?.length > 0">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions">
|
||||
<button type="button" class="btn btn-outline-ink" ng-if="hasActiveFilters()" ng-click="clearFilters()">Clear filters</button>
|
||||
<a href="/anonymize" class="btn btn-ink" ng-if="items.length == 0"><i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New anonymization</a>
|
||||
<button type="button" class="btn btn-outline-ink" v-if="hasActiveFilters()" @click="clearFilters()">Clear filters</button>
|
||||
<a href="/anonymize" class="btn btn-ink" v-if="items?.length == 0"><i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New anonymization</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+32
-120
@@ -1,159 +1,71 @@
|
||||
<div class="explorer-page" ng-init="sidebarCollapsed = window.matchMedia && window.matchMedia('(max-width: 767px)').matches">
|
||||
<button
|
||||
class="sidebar-toggle"
|
||||
ng-show="files.length"
|
||||
ng-click="sidebarCollapsed = !sidebarCollapsed"
|
||||
aria-label="{{sidebarCollapsed ? 'Show files' : 'Hide files'}}"
|
||||
>
|
||||
<i class="fas" ng-class="sidebarCollapsed ? 'fa-folder-open' : 'fa-times'"></i>
|
||||
<span ng-bind="sidebarCollapsed ? 'Files' : 'Close'"></span>
|
||||
<div class="explorer-page">
|
||||
<button class="sidebar-toggle" v-show="files?.length" @click="sidebarCollapsed = !sidebarCollapsed" :aria-label="(sidebarCollapsed ? 'Show files' : 'Hide files')">
|
||||
<i class="fas" :class="sidebarCollapsed ? 'fa-folder-open' : 'fa-times'"></i>
|
||||
<span v-text="sidebarCollapsed ? 'Files' : 'Close'"></span>
|
||||
</button>
|
||||
|
||||
<div class="leftCol" ng-show="files.length" ng-class="{'collapsed': sidebarCollapsed}">
|
||||
<div class="leftCol" v-show="files?.length" :class="{'collapsed': sidebarCollapsed}">
|
||||
<div class="leftCol-head">
|
||||
<span class="leftCol-eyebrow">Files</span>
|
||||
<button class="leftCol-close" ng-click="sidebarCollapsed = true" aria-label="Close files">
|
||||
<button class="leftCol-close" aria-label="Close files" @click="sidebarCollapsed = true">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="leftCol-search">
|
||||
<div class="tree-search-box">
|
||||
<i class="fas tree-search-icon" ng-class="fileSearchLoading ? 'fa-spinner fa-spin' : 'fa-search'"></i>
|
||||
<input
|
||||
type="text"
|
||||
class="tree-search-input"
|
||||
placeholder="Search files"
|
||||
ng-model="fileSearchQuery"
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
ng-change="onFileSearchChange()"
|
||||
aria-label="Search files"
|
||||
/>
|
||||
<kbd class="tree-search-kbd" ng-hide="fileSearchQuery">{{isMac ? '⌘' : 'Ctrl+'}}K</kbd>
|
||||
<button
|
||||
class="tree-search-clear"
|
||||
ng-show="fileSearchQuery"
|
||||
ng-click="fileSearchQuery = ''; onFileSearchChange()"
|
||||
aria-label="Clear search"
|
||||
>×</button>
|
||||
<i class="fas tree-search-icon" :class="fileSearchLoading ? 'fa-spinner fa-spin' : 'fa-search'"></i>
|
||||
<input type="text" class="tree-search-input" placeholder="Search files" aria-label="Search files" v-field="{ state: viewState, set: value => { fileSearchQuery = value }, value: fileSearchQuery, form: null, options: { debounce: 300 }, change: () => { onFileSearchChange() } }">
|
||||
<kbd class="tree-search-kbd" v-show="!(fileSearchQuery)">{{ isMac ? '⌘' : 'Ctrl+' }}K</kbd>
|
||||
<button class="tree-search-clear" aria-label="Clear search" v-show="fileSearchQuery" @click="fileSearchQuery = ''; onFileSearchChange()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="leftCol-project-header">
|
||||
<span class="project-name" ng-bind="repoId"></span>
|
||||
<span class="project-file-count">{{fileCounts[''] || files.length}} files</span>
|
||||
<span class="project-name" v-text="repoId"></span>
|
||||
<span class="project-file-count">{{ fileCounts?.[''] || files?.length }} files</span>
|
||||
</div>
|
||||
<div class="leftCol-body">
|
||||
<div
|
||||
ng-if="options.truncatedFolders.length > 0"
|
||||
class="paper-inline-warning"
|
||||
role="alert"
|
||||
>
|
||||
<div class="paper-inline-warning" role="alert" v-if="options?.truncatedFolders?.length > 0">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
{{ 'WARNINGS.repo_truncated' | translate }}
|
||||
{{ fmt?.translate('WARNINGS.repo_truncated') }}
|
||||
</div>
|
||||
<div
|
||||
ng-if="options.hasSubmodules"
|
||||
class="paper-inline-warning"
|
||||
role="alert"
|
||||
>
|
||||
<div class="paper-inline-warning" role="alert" v-if="options?.hasSubmodules">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
{{ 'WARNINGS.submodules_not_included' | translate }}
|
||||
{{ fmt?.translate('WARNINGS.submodules_not_included') }}
|
||||
</div>
|
||||
<tree class="files" file="files" search-query="fileSearchQuery" search-results="fileSearchResults"></tree>
|
||||
<tree class="files" :file="files" :search-query="fileSearchQuery" :search-results="fileSearchResults" :page="viewState"></tree>
|
||||
</div>
|
||||
<div class="leftCol-foot">
|
||||
<span
|
||||
class="last-update"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
title="{{options.lastUpdateDate}}"
|
||||
>
|
||||
Updated {{options.lastUpdateDate|date}}
|
||||
<span class="last-update" data-toggle="tooltip" data-placement="top" :title="(options?.lastUpdateDate)">
|
||||
Updated {{ fmt?.date(options?.lastUpdateDate) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="leftCol-backdrop"
|
||||
ng-show="files.length && !sidebarCollapsed"
|
||||
ng-click="sidebarCollapsed = true"
|
||||
></div>
|
||||
<div class="leftCol-backdrop" v-show="files?.length && !sidebarCollapsed" @click="sidebarCollapsed = true"></div>
|
||||
|
||||
<div class="explorer-main">
|
||||
<div class="status-bar">
|
||||
<ol class="breadcrumb paths" aria-label="Path">
|
||||
<li class="breadcrumb-item" ng-repeat="p in paths" ng-bind="p"></li>
|
||||
<li class="breadcrumb-item" v-for="(p, index) in paths" v-text="p"></li>
|
||||
</ol>
|
||||
<div class="status-bar-actions">
|
||||
<a
|
||||
ng-if="options.isAdmin || options.isOwner"
|
||||
ng-href="/anonymize/{{repoId}}"
|
||||
class="btn btn-sm"
|
||||
aria-label="Edit"
|
||||
><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a
|
||||
>
|
||||
<button
|
||||
ng-if="type == 'html-doc' && !showSource"
|
||||
ng-click="toggleAllowScripts()"
|
||||
class="btn btn-sm"
|
||||
ng-class="{'btn-active': allowScripts}"
|
||||
aria-label="Allow this document to run JavaScript"
|
||||
title="Scripts in this file are blocked by default. Enable them only if you trust the repository — the document stays isolated from your session either way."
|
||||
>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/anonymize/" + (repoId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
|
||||
<button class="btn btn-sm" aria-label="Allow this document to run JavaScript" title="Scripts in this file are blocked by default. Enable them only if you trust the repository — the document stays isolated from your session either way." v-if="type == 'html-doc' && !showSource" :class="{'btn-active': allowScripts}" @click="toggleAllowScripts()">
|
||||
<i class="fab fa-js"></i><span class="d-none d-md-inline">
|
||||
{{ allowScripts ? "JS on" : "JS off" }}</span
|
||||
>
|
||||
{{ allowScripts ? "JS on" : "JS off" }}</span>
|
||||
</button>
|
||||
<button
|
||||
ng-if="type == 'html-doc'"
|
||||
ng-click="toggleSource()"
|
||||
class="btn btn-sm"
|
||||
aria-label="Toggle between the rendered document and its source"
|
||||
title="Toggle between the rendered document and its source"
|
||||
>
|
||||
<i class="fas" ng-class="showSource ? 'fa-eye' : 'fa-code'"></i
|
||||
><span class="d-none d-md-inline">
|
||||
{{ showSource ? "Rendered" : "Source" }}</span
|
||||
>
|
||||
<button class="btn btn-sm" aria-label="Toggle between the rendered document and its source" title="Toggle between the rendered document and its source" v-if="type == 'html-doc'" @click="toggleSource()">
|
||||
<i class="fas" :class="showSource ? 'fa-eye' : 'fa-code'"></i><span class="d-none d-md-inline">
|
||||
{{ showSource ? "Rendered" : "Source" }}</span>
|
||||
</button>
|
||||
<a
|
||||
ng-show="content != null"
|
||||
ng-href="{{url}}"
|
||||
target="_self"
|
||||
class="btn btn-sm"
|
||||
aria-label="View raw current file"
|
||||
title="View the raw content of the current file"
|
||||
><i class="fas fa-file-alt"></i><span class="d-none d-md-inline"> Raw</span></a
|
||||
>
|
||||
<a
|
||||
ng-show="content != null"
|
||||
ng-href="{{url}}&download=true"
|
||||
target="_self"
|
||||
class="btn btn-sm"
|
||||
aria-label="Download current file"
|
||||
title="Download the current file"
|
||||
><i class="fas fa-download"></i><span class="d-none d-md-inline"> Download</span></a
|
||||
>
|
||||
<a
|
||||
ng-if="options.download"
|
||||
ng-href="/api/repo/{{repoId}}/zip"
|
||||
target="_self"
|
||||
class="btn btn-sm"
|
||||
aria-label="Download full repository as ZIP"
|
||||
title="Download the full repository as a ZIP archive"
|
||||
><i class="fas fa-file-archive"></i><span class="d-none d-md-inline"> Full repo ZIP</span></a
|
||||
>
|
||||
<a
|
||||
ng-if="options.hasWebsite"
|
||||
ng-href="/w/{{repoId}}/"
|
||||
target="_self"
|
||||
class="btn btn-sm"
|
||||
aria-label="Website"
|
||||
><i class="fas fa-globe"></i><span class="d-none d-md-inline"> Website</span></a
|
||||
>
|
||||
<a target="_self" class="btn btn-sm" aria-label="View raw current file" title="View the raw content of the current file" v-show="content != null" :href="safeUrl((url))"><i class="fas fa-file-alt"></i><span class="d-none d-md-inline"> Raw</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Download current file" title="Download the current file" v-show="content != null" :href="safeUrl((url) + "&download=true")"><i class="fas fa-download"></i><span class="d-none d-md-inline"> Download</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Download full repository as ZIP" title="Download the full repository as a ZIP archive" v-if="options?.download" :href="safeUrl("/api/repo/" + (repoId) + "/zip")"><i class="fas fa-file-archive"></i><span class="d-none d-md-inline"> Full repo ZIP</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Website" v-if="options?.hasWebsite" :href="safeUrl("/w/" + (repoId) + "/")"><i class="fas fa-globe"></i><span class="d-none d-md-inline"> Website</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="explorer-content">
|
||||
<ng-include src="'./partials/pageView.htm'"></ng-include>
|
||||
<partial-view name="partials/pageView.htm" :state="viewState"></partial-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+37
-271
@@ -25,34 +25,16 @@
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-info-circle mr-2"></i>General
|
||||
</h4>
|
||||
<div
|
||||
class="panel-group"
|
||||
id="faq-general"
|
||||
role="tablist"
|
||||
aria-multiselectable="true"
|
||||
>
|
||||
<div class="panel-group" id="faq-general" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingWhatIs">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-general"
|
||||
href="#whatIs"
|
||||
aria-expanded="false"
|
||||
aria-controls="whatIs"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-general" href="#whatIs" aria-expanded="false" aria-controls="whatIs">
|
||||
What is Anonymous GitHub?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="whatIs"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingWhatIs"
|
||||
>
|
||||
<div id="whatIs" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingWhatIs">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Anonymous GitHub is a free, open-source tool that anonymizes GitHub repositories and pull requests
|
||||
@@ -67,25 +49,12 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingHowWork">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-general"
|
||||
href="#howWork"
|
||||
aria-expanded="false"
|
||||
aria-controls="howWork"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-general" href="#howWork" aria-expanded="false" aria-controls="howWork">
|
||||
How does Anonymous GitHub work?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="howWork"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingHowWork"
|
||||
>
|
||||
<div id="howWork" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingHowWork">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Anonymous GitHub either downloads the complete repository and anonymizes the content, or proxies
|
||||
@@ -101,25 +70,12 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingScope">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-general"
|
||||
href="#scope"
|
||||
aria-expanded="false"
|
||||
aria-controls="scope"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-general" href="#scope" aria-expanded="false" aria-controls="scope">
|
||||
What is the scope of anonymization?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="scope"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingScope"
|
||||
>
|
||||
<div id="scope" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingScope">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
In double-anonymous peer review, the boundary of anonymization is the paper plus its online
|
||||
@@ -135,25 +91,12 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingCost">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-general"
|
||||
href="#cost"
|
||||
aria-expanded="false"
|
||||
aria-controls="cost"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-general" href="#cost" aria-expanded="false" aria-controls="cost">
|
||||
How much does it cost?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="cost"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingCost"
|
||||
>
|
||||
<div id="cost" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingCost">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Anonymous GitHub is completely free to use. However, the server costs hundreds of dollars per year
|
||||
@@ -172,34 +115,16 @@
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-cogs mr-2"></i>Features
|
||||
</h4>
|
||||
<div
|
||||
class="panel-group"
|
||||
id="faq-features"
|
||||
role="tablist"
|
||||
aria-multiselectable="true"
|
||||
>
|
||||
<div class="panel-group" id="faq-features" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingFormats">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-features"
|
||||
href="#formats"
|
||||
aria-expanded="false"
|
||||
aria-controls="formats"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#formats" aria-expanded="false" aria-controls="formats">
|
||||
Which file formats are supported?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="formats"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingFormats"
|
||||
>
|
||||
<div id="formats" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingFormats">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Anonymous GitHub can display and render a wide variety of file types:
|
||||
@@ -222,25 +147,12 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingPR">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-features"
|
||||
href="#pullRequests"
|
||||
aria-expanded="false"
|
||||
aria-controls="pullRequests"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#pullRequests" aria-expanded="false" aria-controls="pullRequests">
|
||||
Can I anonymize pull requests?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="pullRequests"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingPR"
|
||||
>
|
||||
<div id="pullRequests" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingPR">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Yes. In addition to full repositories, Anonymous GitHub supports anonymizing individual pull
|
||||
@@ -254,25 +166,12 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingConference">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-features"
|
||||
href="#conference"
|
||||
aria-expanded="false"
|
||||
aria-controls="conference"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#conference" aria-expanded="false" aria-controls="conference">
|
||||
What is the Conference ID feature?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="conference"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingConference"
|
||||
>
|
||||
<div id="conference" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingConference">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
When creating an anonymized repository, you can associate it with a Conference ID. This allows
|
||||
@@ -287,25 +186,12 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingExpiration">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-features"
|
||||
href="#expiration"
|
||||
aria-expanded="false"
|
||||
aria-controls="expiration"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#expiration" aria-expanded="false" aria-controls="expiration">
|
||||
What happens when an anonymized repository expires?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="expiration"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingExpiration"
|
||||
>
|
||||
<div id="expiration" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingExpiration">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
You can configure one of three expiration strategies when creating your anonymized repository:
|
||||
@@ -322,25 +208,12 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingDownload">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-features"
|
||||
href="#download"
|
||||
aria-expanded="false"
|
||||
aria-controls="download"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#download" aria-expanded="false" aria-controls="download">
|
||||
Can I download an anonymized repository?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="download"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingDownload"
|
||||
>
|
||||
<div id="download" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingDownload">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Yes. Anonymized repositories can be downloaded as a ZIP file. This is useful if reviewers
|
||||
@@ -354,25 +227,12 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingUpdates">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-features"
|
||||
href="#updates"
|
||||
aria-expanded="false"
|
||||
aria-controls="updates"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#updates" aria-expanded="false" aria-controls="updates">
|
||||
Are updates to the original repository reflected in the anonymized version?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="updates"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingUpdates"
|
||||
>
|
||||
<div id="updates" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingUpdates">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Yes. The anonymized repository tracks the source on GitHub: when you push new commits to the
|
||||
@@ -405,25 +265,12 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingCLI">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-features"
|
||||
href="#cli"
|
||||
aria-expanded="false"
|
||||
aria-controls="cli"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#cli" aria-expanded="false" aria-controls="cli">
|
||||
Is there a command-line tool?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="cli"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingCLI"
|
||||
>
|
||||
<div id="cli" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingCLI">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Yes. Anonymous GitHub provides a CLI tool that lets you anonymize repositories locally,
|
||||
@@ -440,34 +287,16 @@ anonymous_github</code></pre>
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>Limitations
|
||||
</h4>
|
||||
<div
|
||||
class="panel-group"
|
||||
id="faq-limitations"
|
||||
role="tablist"
|
||||
aria-multiselectable="true"
|
||||
>
|
||||
<div class="panel-group" id="faq-limitations" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingLimitation">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-limitations"
|
||||
href="#limitation"
|
||||
aria-expanded="false"
|
||||
aria-controls="limitation"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-limitations" href="#limitation" aria-expanded="false" aria-controls="limitation">
|
||||
What are the limitations of Anonymous GitHub?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="limitation"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingLimitation"
|
||||
>
|
||||
<div id="limitation" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingLimitation">
|
||||
<div class="panel-body p-3">
|
||||
<ul>
|
||||
<li>
|
||||
@@ -495,34 +324,16 @@ anonymous_github</code></pre>
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-shield-alt mr-2"></i>Privacy & Security
|
||||
</h4>
|
||||
<div
|
||||
class="panel-group"
|
||||
id="faq-privacy"
|
||||
role="tablist"
|
||||
aria-multiselectable="true"
|
||||
>
|
||||
<div class="panel-group" id="faq-privacy" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingData">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-privacy"
|
||||
href="#data"
|
||||
aria-expanded="false"
|
||||
aria-controls="data"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-privacy" href="#data" aria-expanded="false" aria-controls="data">
|
||||
How is my data handled?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="data"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingData"
|
||||
>
|
||||
<div id="data" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingData">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Data stored on Anonymous GitHub is never shared or used for any purpose beyond providing the
|
||||
@@ -537,25 +348,12 @@ anonymous_github</code></pre>
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingPermissions">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-privacy"
|
||||
href="#permissions"
|
||||
aria-expanded="false"
|
||||
aria-controls="permissions"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-privacy" href="#permissions" aria-expanded="false" aria-controls="permissions">
|
||||
Why does GitHub say Anonymous GitHub asks for write access?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="permissions"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingPermissions"
|
||||
>
|
||||
<div id="permissions" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingPermissions">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Anonymous GitHub only reads your repositories — it
|
||||
@@ -586,25 +384,12 @@ anonymous_github</code></pre>
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingViewer">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-privacy"
|
||||
href="#viewer"
|
||||
aria-expanded="false"
|
||||
aria-controls="viewer"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-privacy" href="#viewer" aria-expanded="false" aria-controls="viewer">
|
||||
Can repository owners see who viewed their anonymized repository?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="viewer"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingViewer"
|
||||
>
|
||||
<div id="viewer" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingViewer">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
No. Only the total number of views is tracked as an incremental counter. It is not possible
|
||||
@@ -620,34 +405,16 @@ anonymous_github</code></pre>
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-server mr-2"></i>Self-Hosting
|
||||
</h4>
|
||||
<div
|
||||
class="panel-group"
|
||||
id="faq-hosting"
|
||||
role="tablist"
|
||||
aria-multiselectable="true"
|
||||
>
|
||||
<div class="panel-group" id="faq-hosting" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingDeploy">
|
||||
<h3 class="panel-title">
|
||||
<a
|
||||
class="collapsed"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-parent="#faq-hosting"
|
||||
href="#deploy"
|
||||
aria-expanded="false"
|
||||
aria-controls="deploy"
|
||||
>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-hosting" href="#deploy" aria-expanded="false" aria-controls="deploy">
|
||||
Can I deploy my own instance?
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="deploy"
|
||||
class="panel-collapse collapse"
|
||||
role="tabpanel"
|
||||
aria-labelledby="headingDeploy"
|
||||
>
|
||||
<div id="deploy" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingDeploy">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Yes. Anonymous GitHub is fully open source (GPL-3.0) and supports Docker-based deployment.
|
||||
@@ -683,4 +450,3 @@ anonymous_github</code></pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+31
-57
@@ -1,4 +1,4 @@
|
||||
<div class="pr-page" ng-init="tabState = { active: (details && details.files) ? 'files' : 'comments' }">
|
||||
<div class="pr-page">
|
||||
<div class="container paper-page pr-page-inner">
|
||||
<div class="paper-crumbs">
|
||||
<a href="/dashboard">Reviewer</a> /
|
||||
@@ -7,94 +7,68 @@
|
||||
|
||||
<header class="pr-header">
|
||||
<h1 class="paper-page-title pr-title">
|
||||
<span ng-if="details.description" ng-bind="details.description"></span>
|
||||
<span ng-if="!details.description" class="text-muted">Untitled gist</span>
|
||||
<a
|
||||
ng-if="options.isAdmin || options.isOwner"
|
||||
ng-href="/gist-anonymize/{{gistId}}"
|
||||
class="btn btn-sm"
|
||||
aria-label="Edit"
|
||||
><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a
|
||||
>
|
||||
<span v-if="details?.description" v-text="details?.description"></span>
|
||||
<span class="text-muted" v-if="!details?.description">Untitled gist</span>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/gist-anonymize/" + (gistId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</h1>
|
||||
<div class="pr-header-meta">
|
||||
<span class="paper-pill" ng-class="{'good': details.isPublic, 'warn': !details.isPublic}">
|
||||
{{ details.isPublic ? 'Public' : 'Secret' }}
|
||||
<span class="paper-pill" :class="{'good': details?.isPublic, 'warn': !details?.isPublic}">
|
||||
{{ details?.isPublic ? 'Public' : 'Secret' }}
|
||||
</span>
|
||||
<span class="pr-meta-item" ng-if="details.ownerLogin">
|
||||
<i class="far fa-user"></i> @<span ng-bind="details.ownerLogin"></span>
|
||||
<span class="pr-meta-item" v-if="details?.ownerLogin">
|
||||
<i class="far fa-user"></i> @<span v-text="details?.ownerLogin"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" ng-if="details.updatedDate">
|
||||
<i class="far fa-clock"></i> <span ng-bind="details.updatedDate | date"></span>
|
||||
<span class="pr-meta-item" v-if="details?.updatedDate">
|
||||
<i class="far fa-clock"></i> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" ng-if="details.anonymizeDate">
|
||||
<i class="fas fa-user-secret"></i> Anonymized <span ng-bind="details.anonymizeDate | date"></span>
|
||||
<span class="pr-meta-item" v-if="details?.anonymizeDate">
|
||||
<i class="fas fa-user-secret"></i> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="paper-tabs" ng-if="(details.files && details.files.length) || (details.comments && details.comments.length)" role="tablist">
|
||||
<button
|
||||
class="paper-tab"
|
||||
ng-if="details.files"
|
||||
ng-class="{'active': tabState.active == 'files'}"
|
||||
ng-click="tabState.active = 'files'"
|
||||
type="button"
|
||||
role="tab"
|
||||
>
|
||||
<nav class="paper-tabs" role="tablist" v-if="(details?.files && details?.files?.length) || (details?.comments && details?.comments?.length)">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.files" :class="{'active': tabState?.active == 'files'}" @click="tabState.active = 'files'">
|
||||
<i class="fas fa-file-code"></i>
|
||||
<ng-pluralize
|
||||
count="details.files.length"
|
||||
when="{'0': 'No files', 'one': '1 file', 'other': '{} files'}"
|
||||
></ng-pluralize>
|
||||
<span v-text="fmt.plural(details?.files?.length, {'0': 'No files', 'one': '1 file', 'other': '{} files'})"></span>
|
||||
</button>
|
||||
<button
|
||||
class="paper-tab"
|
||||
ng-if="details.comments"
|
||||
ng-class="{'active': tabState.active == 'comments'}"
|
||||
ng-click="tabState.active = 'comments'"
|
||||
type="button"
|
||||
role="tab"
|
||||
>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" @click="tabState.active = 'comments'">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<ng-pluralize
|
||||
count="details.comments.length"
|
||||
when="{'0': 'No comments', 'one': '1 comment', 'other': '{} comments'}"
|
||||
></ng-pluralize>
|
||||
<span v-text="fmt.plural(details?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="paper-tab-content">
|
||||
<div ng-if="details.files && tabState.active =='files'">
|
||||
<div v-if="details?.files && tabState?.active =='files'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" ng-repeat="file in details.files">
|
||||
<li class="pr-comment" v-for="(file, index) in details?.files">
|
||||
<div class="pr-comment-head">
|
||||
<strong ng-bind="file.filename"></strong>
|
||||
<span class="pr-comment-date" ng-if="file.language">{{file.language}}</span>
|
||||
<strong v-text="file?.filename"></strong>
|
||||
<span class="pr-comment-date" v-if="file?.language">{{ file?.language }}</span>
|
||||
</div>
|
||||
<gist-file file="file"></gist-file>
|
||||
<gist-file :file="file"></gist-file>
|
||||
</li>
|
||||
<li class="paper-table-empty" ng-if="!details.files.length">
|
||||
<li class="paper-table-empty" v-if="!details?.files?.length">
|
||||
<i class="fas fa-file"></i>
|
||||
<span>No files in this gist.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div ng-if="details.comments && tabState.active =='comments'">
|
||||
<div v-if="details?.comments && tabState?.active =='comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" ng-repeat="comment in details.comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" ng-if="comment.author">
|
||||
<i class="far fa-user"></i> @<span ng-bind="comment.author"></span>
|
||||
<span class="pr-comment-author" v-if="comment?.author">
|
||||
<i class="far fa-user"></i> @<span v-text="comment?.author"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" ng-if="comment.updatedDate" ng-bind="comment.updatedDate | date"></span>
|
||||
<span class="pr-comment-date" v-if="comment?.updatedDate" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
<div class="pr-comment-body" ng-if="comment.body">
|
||||
<markdown content="comment.body"></markdown>
|
||||
<div class="pr-comment-body" v-if="comment?.body">
|
||||
<markdown :content="comment?.body"></markdown>
|
||||
</div>
|
||||
</li>
|
||||
<li class="paper-table-empty" ng-if="!details.comments.length">
|
||||
<li class="paper-table-empty" v-if="!details?.comments?.length">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<span>No comments on this gist.</span>
|
||||
</li>
|
||||
|
||||
+27
-91
@@ -1,68 +1,39 @@
|
||||
<nav
|
||||
class="navbar navbar-expand-lg"
|
||||
ng-class="{'navbar-dark': isDarkMode}"
|
||||
>
|
||||
<nav class="navbar navbar-expand-lg" :class="{'navbar-dark': isDarkMode}">
|
||||
<a class="navbar-brand" href="/">Anonymous <em>GitHub</em></a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto smooth-scroll">
|
||||
<li class="nav-item" ng-if="!user">
|
||||
<a class="nav-link" ng-class="{'active': path == '/'}" href="/">
|
||||
<li class="nav-item" v-if="!user">
|
||||
<a class="nav-link" href="/" :class="{'active': path == '/'}">
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" ng-if="user">
|
||||
<a
|
||||
class="nav-link"
|
||||
ng-class="{'active': path == '/dashboard'}"
|
||||
href="/dashboard"
|
||||
>
|
||||
<li class="nav-item" v-if="user">
|
||||
<a class="nav-link" href="/dashboard" :class="{'active': path == '/dashboard'}">
|
||||
<i class="fas fa-th-large d-lg-none mr-1"></i>
|
||||
My work
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" ng-if="user">
|
||||
<a
|
||||
class="nav-link"
|
||||
ng-class="{'active':path == '/anonymize' || path == '/pull-request-anonymize'}"
|
||||
href="/anonymize"
|
||||
>
|
||||
<li class="nav-item" v-if="user">
|
||||
<a class="nav-link" href="/anonymize" :class="{'active':path == '/anonymize' || path == '/pull-request-anonymize'}">
|
||||
<i class="fas fa-user-secret d-lg-none mr-1"></i>
|
||||
Anonymize
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" ng-if="user">
|
||||
<a
|
||||
class="nav-link"
|
||||
ng-class="{'active':path == '/conferences'}"
|
||||
href="/conferences"
|
||||
>
|
||||
<li class="nav-item" v-if="user">
|
||||
<a class="nav-link" href="/conferences" :class="{'active':path == '/conferences'}">
|
||||
<i class="fas fa-chalkboard-teacher d-lg-none mr-1"></i>
|
||||
Conferences
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" ng-class="{'active':path == '/faq'}" href="/faq"
|
||||
><i class="fas fa-question-circle d-lg-none mr-1"></i>FAQ</a
|
||||
>
|
||||
<a class="nav-link" href="/faq" :class="{'active':path == '/faq'}"><i class="fas fa-question-circle d-lg-none mr-1"></i>FAQ</a>
|
||||
</li>
|
||||
<li class="nav-item" ng-if="user && user.isAdmin">
|
||||
<a
|
||||
class="nav-link"
|
||||
ng-class="{'active':path.indexOf('/admin') === 0}"
|
||||
href="/admin/"
|
||||
>
|
||||
<li class="nav-item" v-if="user && user?.isAdmin">
|
||||
<a class="nav-link" href="/admin/" :class="{'active':path?.indexOf('/admin') === 0}">
|
||||
<i class="fas fa-cog d-lg-none mr-1"></i>
|
||||
Admin
|
||||
</a>
|
||||
@@ -70,71 +41,40 @@
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link nav-icon"
|
||||
target="_blank"
|
||||
href="https://github.com/tdurieux/anonymous_github/"
|
||||
title="Anonymous GitHub source code"
|
||||
aria-label="Anonymous GitHub source code on GitHub"
|
||||
rel="noopener"
|
||||
data-offset="30"
|
||||
><i class="fab fa-github" aria-hidden="true"></i
|
||||
></a>
|
||||
<a class="nav-link nav-icon" target="_blank" href="https://github.com/tdurieux/anonymous_github/" title="Anonymous GitHub source code" aria-label="Anonymous GitHub source code on GitHub" rel="noopener" data-offset="30"><i class="fab fa-github" aria-hidden="true"></i></a>
|
||||
</li>
|
||||
<!-- "Report a bug" is about this service. Reviewers who land on an
|
||||
anonymized repository often think it is about the paper, so the
|
||||
label names the service and the tooltip says who to contact. -->
|
||||
<li class="nav-item d-none d-lg-block">
|
||||
<a
|
||||
class="nav-link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml"
|
||||
title="Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors."
|
||||
data-offset="30"
|
||||
><i class="fas fa-bug d-lg-none mr-1" aria-hidden="true"></i>Report a bug
|
||||
<a class="nav-link" target="_blank" rel="noopener" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" title="Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors." data-offset="30"><i class="fas fa-bug d-lg-none mr-1" aria-hidden="true"></i>Report a bug
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" ng-if="!isDarkMode">
|
||||
<a class="nav-link nav-icon" href="#" ng-click="darkMode(true);" title="Switch to dark mode" aria-label="Switch to dark mode" role="button">
|
||||
<li class="nav-item" v-if="!isDarkMode">
|
||||
<a class="nav-link nav-icon" href="#" title="Switch to dark mode" aria-label="Switch to dark mode" role="button" @click.prevent="darkMode(true);">
|
||||
<i class="fas fa-moon" aria-hidden="true"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" ng-if="isDarkMode">
|
||||
<a class="nav-link nav-icon" href="#" ng-click="darkMode(false);" title="Switch to light mode" aria-label="Switch to light mode" role="button">
|
||||
<li class="nav-item" v-if="isDarkMode">
|
||||
<a class="nav-link nav-icon" href="#" title="Switch to light mode" aria-label="Switch to light mode" role="button" @click.prevent="darkMode(false);">
|
||||
<i class="fas fa-sun" aria-hidden="true"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" ng-if="!user">
|
||||
<a class="nav-link btn-signin" target="_self" href="/github/login" data-offset="30"
|
||||
><i class="fab fa-github mr-1"></i>
|
||||
<li class="nav-item" v-if="!user">
|
||||
<a class="nav-link btn-signin" target="_self" href="/github/login" data-offset="30"><i class="fab fa-github mr-1"></i>
|
||||
Sign in
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown user-chip-wrap" ng-if="user">
|
||||
<a
|
||||
class="nav-link user-chip dropdown-toggle"
|
||||
href="#"
|
||||
id="navbarDropdownMenuLink"
|
||||
role="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img
|
||||
ng-src="{{user.photo}}"
|
||||
ng-if="user.photo"
|
||||
width="22"
|
||||
height="22"
|
||||
class="rounded-circle"
|
||||
/>
|
||||
<span class="user-chip-name">{{user.username}}</span>
|
||||
<li class="nav-item dropdown user-chip-wrap" v-if="user">
|
||||
<a class="nav-link user-chip dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<img width="22" height="22" class="rounded-circle" v-if="user?.photo" :src="safeUrl((user?.photo))">
|
||||
<span class="user-chip-name">{{ user?.username }}</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right user-menu" aria-labelledby="navbarDropdownMenuLink">
|
||||
<h6 class="dropdown-header">Signed in as @{{user.username}}</h6>
|
||||
<h6 class="dropdown-header">Signed in as @{{ user?.username }}</h6>
|
||||
<a class="dropdown-item" href="/profile">
|
||||
<i class="fas fa-sliders-h" aria-hidden="true"></i>
|
||||
<span>Settings<small>Defaults, quotas, account</small></span>
|
||||
@@ -162,8 +102,4 @@
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
class="navbar shadow generalMessage"
|
||||
ng-bind-html="generalMessage"
|
||||
ng-if="generalMessage"
|
||||
></div>
|
||||
<div class="navbar shadow generalMessage" v-if="generalMessage" v-html="sanitize(generalMessage)"></div>
|
||||
|
||||
+26
-62
@@ -4,7 +4,7 @@
|
||||
<div class="paper-hero-copy">
|
||||
<div class="paper-eyebrow">For double-blind peer review</div>
|
||||
<h1 class="hero-title">
|
||||
Share the code,<br />
|
||||
Share the code,<br>
|
||||
<span class="accent">not the author.</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
@@ -14,18 +14,13 @@
|
||||
</p>
|
||||
|
||||
<div class="paper-cta-row">
|
||||
<a ng-if="!user" href="/github/login" target="_self" class="btn-hero">
|
||||
<a href="/github/login" target="_self" class="btn-hero" v-if="!user">
|
||||
<i class="fab fa-github mr-2" aria-hidden="true"></i>Sign in with GitHub
|
||||
</a>
|
||||
<a ng-if="user" href="/anonymize" class="btn-hero">
|
||||
<a href="/anonymize" class="btn-hero" v-if="user">
|
||||
Anonymize a repository →
|
||||
</a>
|
||||
<a
|
||||
class="paper-link-arrow"
|
||||
target="_self"
|
||||
href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/"
|
||||
>See an anonymized repository →</a
|
||||
>
|
||||
<a class="paper-link-arrow" target="_self" href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/">See an anonymized repository →</a>
|
||||
</div>
|
||||
<ul class="paper-reassure">
|
||||
<li>Free and open source</li>
|
||||
@@ -39,12 +34,7 @@
|
||||
<span class="dots"><i></i><i></i><i></i></span>
|
||||
<span class="url">anonymous.4open.science/anonymize</span>
|
||||
</div>
|
||||
<img
|
||||
src="/imgs/anonymize.png"
|
||||
width="2880"
|
||||
height="1800"
|
||||
alt="The anonymize form with a live README preview showing the redactions"
|
||||
/>
|
||||
<img src="/imgs/anonymize.png" width="2880" height="1800" alt="The anonymize form with a live README preview showing the redactions">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -68,19 +58,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-stats-grid">
|
||||
<div class="paper-stat-card" ng-repeat="card in cards track by card.key">
|
||||
<div class="paper-stat-value">{{card.total | bigNum}}</div>
|
||||
<div class="paper-stat-label">{{card.label}}</div>
|
||||
<svg class="paper-stat-bars" preserveAspectRatio="none" aria-hidden="true"
|
||||
ng-attr-viewBox="0 0 {{history[card.key].viewW}} 36"
|
||||
ng-if="history[card.key].bars.length > 1">
|
||||
<rect ng-repeat="b in history[card.key].bars track by $index"
|
||||
ng-attr-x="{{b.x}}" ng-attr-y="{{b.y}}"
|
||||
ng-attr-width="{{b.w}}" ng-attr-height="{{b.h}}"
|
||||
ng-class="{'is-latest': $last}"/>
|
||||
<div class="paper-stat-card" v-for="(card, index) in cards" :key="card?.key">
|
||||
<div class="paper-stat-value">{{ fmt?.bigNum(card?.total) }}</div>
|
||||
<div class="paper-stat-label">{{ card?.label }}</div>
|
||||
<svg class="paper-stat-bars" preserveAspectRatio="none" aria-hidden="true" v-if="history[card?.key].bars?.length > 1" :viewBox=""0 0 " + (history[card?.key].viewW) + " 36"">
|
||||
<rect v-for="(b, index) in history[card?.key].bars" :key="index" :class="{'is-latest': index === history[card.key].bars.length - 1}" :x="(b?.x)" :y="(b?.y)" :width="(b?.w)" :height="(b?.h)"></rect>
|
||||
</svg>
|
||||
<div class="paper-stat-delta" ng-if="history[card.key].bars.length > 1">
|
||||
+{{history[card.key].deltaToday | number}} today
|
||||
<div class="paper-stat-delta" v-if="history[card?.key].bars?.length > 1">
|
||||
+{{ fmt?.number(history[card?.key].deltaToday) }} today
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,48 +124,27 @@
|
||||
</section>
|
||||
<section class="paper-features paper-wrap" aria-label="Product features">
|
||||
<div class="paper-features-list" role="tablist" aria-label="Features" aria-orientation="vertical">
|
||||
<div
|
||||
class="paper-feature"
|
||||
ng-repeat="f in features track by f.key"
|
||||
ng-class="{'is-on': feature === f.key}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="paper-feature-tab"
|
||||
role="tab"
|
||||
id="feature-tab-{{f.key}}"
|
||||
aria-controls="feature-panel-{{f.key}}"
|
||||
aria-selected="{{feature === f.key}}"
|
||||
tabindex="{{feature === f.key ? 0 : -1}}"
|
||||
ng-click="selectFeature(f.key)"
|
||||
ng-keydown="featureKeydown($event, $index)"
|
||||
>
|
||||
<span class="paper-eyebrow">{{f.num}} · {{f.eyebrow}}</span>
|
||||
<span class="paper-feature-title">{{f.title}} <em>{{f.accent}}</em></span>
|
||||
<div class="paper-feature" v-for="(f, index) in features" :key="f?.key" :class="{'is-on': feature === f?.key}">
|
||||
<button type="button" class="paper-feature-tab" role="tab" @click="selectFeature(f.key)" @keydown="featureKeydown($event, index)" :id=""feature-tab-" + (f?.key)" :aria-controls=""feature-panel-" + (f?.key)" :aria-selected="(feature === f?.key)" :tabindex="(feature === f?.key ? 0 : -1)">
|
||||
<span class="paper-eyebrow">{{ f?.num }} · {{ f?.eyebrow }}</span>
|
||||
<span class="paper-feature-title">{{ f?.title }} <em>{{ f?.accent }}</em></span>
|
||||
</button>
|
||||
<div class="paper-feature-more">
|
||||
<span class="paper-feature-title paper-feature-title-m" aria-hidden="true">{{f.title}} <em>{{f.accent}}</em></span>
|
||||
<p>{{f.text}}</p>
|
||||
<a class="paper-link-arrow" ng-href="{{featureHref(f)}}" ng-attr-target="{{featureTarget(f)}}">{{f.cta}} →</a>
|
||||
<span class="paper-feature-title paper-feature-title-m" aria-hidden="true">{{ f?.title }} <em>{{ f?.accent }}</em></span>
|
||||
<p>{{ f?.text }}</p>
|
||||
<a class="paper-link-arrow" :href="safeUrl((featureHref(f)))" :target="(featureTarget(f))">{{ f?.cta }} →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-features-stage">
|
||||
<div
|
||||
class="paper-feature-panel"
|
||||
ng-repeat="f in features track by f.key"
|
||||
ng-show="feature === f.key"
|
||||
id="feature-panel-{{f.key}}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="feature-tab-{{f.key}}"
|
||||
>
|
||||
<div class="paper-feature-panel" role="tabpanel" v-for="(f, index) in features" :key="f?.key" v-show="feature === f?.key" :id=""feature-panel-" + (f?.key)" :aria-labelledby=""feature-tab-" + (f?.key)">
|
||||
<div class="paper-frame">
|
||||
<div class="paper-frame-bar" aria-hidden="true">
|
||||
<span class="dots"><i></i><i></i><i></i></span>
|
||||
<span class="url">{{f.url}}</span>
|
||||
<span class="url">{{ f?.url }}</span>
|
||||
</div>
|
||||
<div class="paper-crop paper-crop-{{f.key}}">
|
||||
<img ng-src="{{f.img}}" alt="{{f.alt}}" loading="lazy" width="2880" height="1800" />
|
||||
<div :class=""paper-crop paper-crop-" + (f?.key)">
|
||||
<img loading="lazy" width="2880" height="1800" :src="safeUrl((f?.img))" :alt="(f?.alt)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,8 +197,8 @@
|
||||
<div class="paper-footer-col">
|
||||
<div class="paper-footer-head">Product</div>
|
||||
<a href="/anonymize">Anonymize</a>
|
||||
<a href="/dashboard" ng-if="user">My work</a>
|
||||
<a href="/conferences" ng-if="user">Conferences</a>
|
||||
<a href="/dashboard" v-if="user">My work</a>
|
||||
<a href="/conferences" v-if="user">Conferences</a>
|
||||
<a href="/faq">FAQ</a>
|
||||
</div>
|
||||
<div class="paper-footer-col">
|
||||
@@ -251,7 +215,7 @@
|
||||
</div>
|
||||
<div class="paper-footer-rule"></div>
|
||||
<div class="paper-footer-foot">
|
||||
<span>© {{year || 2026}} Anonymous GitHub · MIT licensed</span>
|
||||
<span>© {{ year || 2026 }} Anonymous GitHub · MIT licensed</span>
|
||||
<span class="paper-footer-meta">Built for reviewers, by researchers.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="paper-empty">
|
||||
<div class="paper-empty-inner">
|
||||
<div class="paper-eyebrow" ng-if="!error">Working</div>
|
||||
<div class="paper-eyebrow" ng-if="error">Error</div>
|
||||
<h1 class="paper-empty-title" ng-if="!error">
|
||||
<div class="paper-eyebrow" v-if="!error">Working</div>
|
||||
<div class="paper-eyebrow" v-if="error">Error</div>
|
||||
<h1 class="paper-empty-title" v-if="!error">
|
||||
Loading<span class="dot-anim">…</span>
|
||||
</h1>
|
||||
<h1 class="paper-empty-title" ng-if="error" translate="{{error}}"></h1>
|
||||
<h1 class="paper-empty-title" v-text="fmt.translate((error))" v-if="error"></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div class="container paper-page paper-settings">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <a href="/conferences">Conferences</a> / <span class="here">{{editionMode ? 'Edit' : 'New'}}</span></div>
|
||||
<h1 class="paper-page-title">{{editionMode ? 'Edit' : 'Create a'}} <em>conference</em></h1>
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <a href="/conferences">Conferences</a> / <span class="here">{{ editionMode ? 'Edit' : 'New' }}</span></div>
|
||||
<h1 class="paper-page-title">{{ editionMode ? 'Edit' : 'Create a' }} <em>conference</em></h1>
|
||||
<p class="paper-page-lede">
|
||||
Give chairs access to every anonymization submitted to a venue, and lift
|
||||
author quotas during the review window.
|
||||
</p>
|
||||
|
||||
<div class="paper-settings-body">
|
||||
<aside class="paper-settings-toc" paper-scrollspy>
|
||||
<aside class="paper-settings-toc" v-paper-scrollspy>
|
||||
<div class="paper-settings-toc-head">On this page</div>
|
||||
<nav>
|
||||
<a href="#conf-basics">Basics</a>
|
||||
@@ -15,71 +15,44 @@
|
||||
<a href="#conf-rendering">Rendering defaults</a>
|
||||
<a href="#conf-features">Features</a>
|
||||
<a href="#conf-plan">Plan</a>
|
||||
<a href="#conf-billing" ng-show="plan.pricePerRepo > 0">Billing</a>
|
||||
<a href="#conf-billing" v-show="plan?.pricePerRepo > 0">Billing</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<form class="form needs-validation paper-settings-main" name="conference" novalidate>
|
||||
<form class="form needs-validation paper-settings-main" name="conference" novalidate v-form="viewState">
|
||||
<section id="conf-basics" class="paper-settings-section">
|
||||
<div class="paper-section-eyebrow">Basics</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="name">Conference name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="name"
|
||||
id="name"
|
||||
required
|
||||
ng-class="{'is-invalid': conference.name.$invalid && (conference.name.$touched || conference.$submitted)}"
|
||||
ng-model="options.name"
|
||||
placeholder="e.g. International Conference on Software Engineering"
|
||||
/>
|
||||
<div class="invalid-feedback" ng-show="conference.name.$error.invalid">
|
||||
<input type="text" class="form-control" name="name" id="name" required placeholder="e.g. International Conference on Software Engineering" v-field="{ state: viewState, set: value => { options.name = value }, value: options?.name, form: "conference", options: {} }" :class="{'is-invalid': conference?.name?.invalid && (conference?.name?.touched || conference?.submitted)}">
|
||||
<div class="invalid-feedback" v-show="conference?.name?.errors?.invalid">
|
||||
The name of the conference is invalid.
|
||||
</div>
|
||||
<div class="invalid-feedback" ng-show="conference.name.$error.required">
|
||||
<div class="invalid-feedback" v-show="conference?.name?.errors?.required">
|
||||
The name of the conference is required.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="url">Website</label>
|
||||
<input
|
||||
type="url"
|
||||
class="form-control"
|
||||
name="url"
|
||||
id="url"
|
||||
ng-class="{'is-invalid': conference.url.$invalid && (conference.url.$touched || conference.$submitted)}"
|
||||
ng-model="options.url"
|
||||
placeholder="https://example.org"
|
||||
/>
|
||||
<div class="invalid-feedback" ng-show="conference.url.$error.invalid">
|
||||
<input type="url" class="form-control" name="url" id="url" placeholder="https://example.org" v-field="{ state: viewState, set: value => { options.url = value }, value: options?.url, form: "conference", options: {} }" :class="{'is-invalid': conference?.url?.invalid && (conference?.url?.touched || conference?.submitted)}">
|
||||
<div class="invalid-feedback" v-show="conference?.url?.errors?.invalid">
|
||||
The url of the conference is invalid.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="conferenceID">Conference ID</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="conferenceID"
|
||||
id="conferenceID"
|
||||
required
|
||||
pattern="[a-zA-Z0-9\-_]{3,10}"
|
||||
ng-class="{'is-invalid': conference.conferenceID.$invalid && (conference.conferenceID.$touched || conference.$submitted)}"
|
||||
ng-model="options.conferenceID"
|
||||
placeholder="ICSE26"
|
||||
/>
|
||||
<input type="text" class="form-control" name="conferenceID" id="conferenceID" required pattern="[a-zA-Z0-9\-_]{3,10}" placeholder="ICSE26" v-field="{ state: viewState, set: value => { options.conferenceID = value }, value: options?.conferenceID, form: "conference", options: {} }" :class="{'is-invalid': conference?.conferenceID?.invalid && (conference?.conferenceID?.touched || conference?.submitted)}">
|
||||
<small class="form-text text-muted">3–10 characters, letters, numbers, <code>-</code> and <code>_</code>. Authors reference this when they anonymize.</small>
|
||||
<div class="invalid-feedback" ng-show="conference.conferenceID.$error.used">
|
||||
The conference ID '{{options.conferenceID}}' is already used.
|
||||
<div class="invalid-feedback" v-show="conference?.conferenceID?.errors?.used">
|
||||
The conference ID '{{ options?.conferenceID }}' is already used.
|
||||
</div>
|
||||
<div class="invalid-feedback" ng-show="conference.conferenceID.$error.required">
|
||||
<div class="invalid-feedback" v-show="conference?.conferenceID?.errors?.required">
|
||||
The conference ID is required.
|
||||
</div>
|
||||
<div class="invalid-feedback" ng-show="conference.conferenceID.$error.pattern">
|
||||
<div class="invalid-feedback" v-show="conference?.conferenceID?.errors?.pattern">
|
||||
The format of the conference ID is incorrect ([a-zA-Z0-9-_]{3,10}).
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,40 +64,24 @@
|
||||
<div class="form-grid-2">
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="startDate">Start date</label>
|
||||
<input
|
||||
type="date"
|
||||
class="form-control"
|
||||
name="startDate"
|
||||
id="startDate"
|
||||
required
|
||||
ng-class="{'is-invalid': conference.startDate.$invalid && (conference.startDate.$touched || conference.$submitted)}"
|
||||
ng-model="options.startDate"
|
||||
/>
|
||||
<input type="date" class="form-control" name="startDate" id="startDate" required v-field="{ state: viewState, set: value => { options.startDate = value }, value: options?.startDate, form: "conference", options: {} }" :class="{'is-invalid': conference?.startDate?.invalid && (conference?.startDate?.touched || conference?.submitted)}">
|
||||
<small class="form-text text-muted">Beginning of the review process.</small>
|
||||
<div class="invalid-feedback" ng-show="conference.startDate.$error.required">
|
||||
<div class="invalid-feedback" v-show="conference?.startDate?.errors?.required">
|
||||
Start date is required.
|
||||
</div>
|
||||
<div class="invalid-feedback" ng-show="conference.startDate.$error.invalid">
|
||||
<div class="invalid-feedback" v-show="conference?.startDate?.errors?.invalid">
|
||||
Start date must be before end date.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="endDate">End date</label>
|
||||
<input
|
||||
type="date"
|
||||
class="form-control"
|
||||
name="endDate"
|
||||
id="endDate"
|
||||
required
|
||||
ng-class="{'is-invalid': conference.endDate.$invalid && (conference.endDate.$touched || conference.$submitted)}"
|
||||
ng-model="options.endDate"
|
||||
/>
|
||||
<input type="date" class="form-control" name="endDate" id="endDate" required v-field="{ state: viewState, set: value => { options.endDate = value }, value: options?.endDate, form: "conference", options: {} }" :class="{'is-invalid': conference?.endDate?.invalid && (conference?.endDate?.touched || conference?.submitted)}">
|
||||
<small class="form-text text-muted">All repositories expire on this date.</small>
|
||||
<div class="invalid-feedback" ng-show="conference.endDate.$error.required">
|
||||
<div class="invalid-feedback" v-show="conference?.endDate?.errors?.required">
|
||||
End date is required.
|
||||
</div>
|
||||
<div class="invalid-feedback" ng-show="conference.endDate.$error.invalid">
|
||||
<div class="invalid-feedback" v-show="conference?.endDate?.errors?.invalid">
|
||||
End date is invalid.
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,22 +92,22 @@
|
||||
<div class="paper-section-eyebrow">Rendering defaults</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="link" name="link" ng-model="options.options.link" />
|
||||
<input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value => { options.options.link = value }, value: options?.options?.link, form: "conference", options: {} }">
|
||||
<label class="form-check-label" for="link">Keep links</label>
|
||||
<small class="form-text text-muted">Keep or remove all links from text files.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="image" name="image" ng-model="options.options.image" />
|
||||
<input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value => { options.options.image = value }, value: options?.options?.image, form: "conference", options: {} }">
|
||||
<label class="form-check-label" for="image">Display images</label>
|
||||
<small class="form-text text-muted">Images are not anonymized.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" ng-model="options.options.pdf" />
|
||||
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value => { options.options.pdf = value }, value: options?.options?.pdf, form: "conference", options: {} }">
|
||||
<label class="form-check-label" for="pdf">Display PDFs</label>
|
||||
<small class="form-text text-muted">PDFs are not anonymized.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" ng-model="options.options.notebook" />
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.options.notebook = value }, value: options?.options?.notebook, form: "conference", options: {} }">
|
||||
<label class="form-check-label" for="notebook">Display Notebooks</label>
|
||||
</div>
|
||||
</section>
|
||||
@@ -159,12 +116,12 @@
|
||||
<div class="paper-section-eyebrow">Features</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="update" name="update" ng-model="options.options.update" />
|
||||
<input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value => { options.options.update = value }, value: options?.options?.update, form: "conference", options: {} }">
|
||||
<label class="form-check-label" for="update">Auto-update from GitHub</label>
|
||||
<small class="form-text text-muted">Pull the latest commit automatically (hourly maximum).</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="page" name="page" ng-model="options.options.page" />
|
||||
<input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value => { options.options.page = value }, value: options?.options?.page, form: "conference", options: {} }">
|
||||
<label class="form-check-label" for="page">GitHub Pages</label>
|
||||
<small class="form-text text-muted">Enable anonymized GitHub pages.</small>
|
||||
</div>
|
||||
@@ -175,92 +132,65 @@
|
||||
<p class="paper-section-copy">Billed per repository per month, prorated by the day for as long as a repository stays in the conference.</p>
|
||||
|
||||
<div class="paper-plan-grid">
|
||||
<label
|
||||
class="paper-plan-card"
|
||||
ng-class="{'selected': options.plan.planID == plan.id}"
|
||||
ng-repeat="plan in plans"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="planPicker"
|
||||
ng-value="plan.id"
|
||||
ng-model="options.plan.planID"
|
||||
/>
|
||||
<div class="paper-plan-head" ng-bind="plan.name"></div>
|
||||
<label class="paper-plan-card" v-for="(plan, index) in plans" :class="{'selected': options?.plan?.planID == plan?.id}">
|
||||
<input type="radio" name="planPicker" v-field="{ state: viewState, set: value => { options.plan.planID = value }, value: options?.plan?.planID, form: "conference", options: {} }" :value="plan?.id">
|
||||
<div class="paper-plan-head" v-text="plan?.name"></div>
|
||||
<div class="paper-plan-price">
|
||||
{{plan.pricePerRepo | number}}€
|
||||
{{ fmt?.number(plan?.pricePerRepo) }}€
|
||||
<span class="paper-plan-per">/ repo / month</span>
|
||||
</div>
|
||||
<div class="paper-plan-desc" ng-bind-html="plan.description"></div>
|
||||
<div class="paper-plan-desc" v-html="sanitize(plan?.description)"></div>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="conf-billing" class="paper-settings-section" ng-if="plan.pricePerRepo > 0">
|
||||
<section id="conf-billing" class="paper-settings-section" v-if="plan?.pricePerRepo > 0">
|
||||
<div class="paper-section-eyebrow">Billing</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="billing_name">Name</label>
|
||||
<input type="text" class="form-control" name="billing_name" id="billing_name" required
|
||||
ng-class="{'is-invalid': conference.billing_name.$invalid && (conference.billing_name.$touched || conference.$submitted)}"
|
||||
ng-model="options.billing.name" placeholder="First & last name" />
|
||||
<input type="text" class="form-control" name="billing_name" id="billing_name" required placeholder="First & last name" v-field="{ state: viewState, set: value => { options.billing.name = value }, value: options?.billing?.name, form: "conference", options: {} }" :class="{'is-invalid': conference?.billing_name?.invalid && (conference?.billing_name?.touched || conference?.submitted)}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="email">Email</label>
|
||||
<input type="email" class="form-control" name="email" id="email" required
|
||||
ng-class="{'is-invalid': conference.email.$invalid && (conference.email.$touched || conference.$submitted)}"
|
||||
ng-model="options.billing.email" placeholder="you@example.org" />
|
||||
<input type="email" class="form-control" name="email" id="email" required placeholder="you@example.org" v-field="{ state: viewState, set: value => { options.billing.email = value }, value: options?.billing?.email, form: "conference", options: {} }" :class="{'is-invalid': conference?.email?.invalid && (conference?.email?.touched || conference?.submitted)}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="inputAddress">Address</label>
|
||||
<input type="text" class="form-control" name="inputAddress" id="inputAddress" required
|
||||
placeholder="1234 Main St"
|
||||
ng-model="options.billing.address"
|
||||
ng-class="{'is-invalid': conference.inputAddress.$invalid && (conference.inputAddress.$touched || conference.$submitted)}" />
|
||||
<input type="text" class="form-control" name="inputAddress" id="inputAddress" required placeholder="1234 Main St" v-field="{ state: viewState, set: value => { options.billing.address = value }, value: options?.billing?.address, form: "conference", options: {} }" :class="{'is-invalid': conference?.inputAddress?.invalid && (conference?.inputAddress?.touched || conference?.submitted)}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="inputAddress2" id="inputAddress2"
|
||||
ng-model="options.billing.address2"
|
||||
ng-class="{'is-invalid': conference.inputAddress2.$invalid && (conference.inputAddress2.$touched || conference.$submitted)}"
|
||||
placeholder="Apartment, studio, or floor" />
|
||||
<input type="text" class="form-control" name="inputAddress2" id="inputAddress2" placeholder="Apartment, studio, or floor" v-field="{ state: viewState, set: value => { options.billing.address2 = value }, value: options?.billing?.address2, form: "conference", options: {} }" :class="{'is-invalid': conference?.inputAddress2?.invalid && (conference?.inputAddress2?.touched || conference?.submitted)}">
|
||||
</div>
|
||||
|
||||
<div class="form-grid-3">
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="city">City</label>
|
||||
<input type="text" class="form-control" name="city" id="city"
|
||||
ng-model="options.billing.city" required
|
||||
ng-class="{'is-invalid': conference.city.$invalid && (conference.city.$touched || conference.$submitted)}" placeholder="City" />
|
||||
<input type="text" class="form-control" name="city" id="city" required placeholder="City" v-field="{ state: viewState, set: value => { options.billing.city = value }, value: options?.billing?.city, form: "conference", options: {} }" :class="{'is-invalid': conference?.city?.invalid && (conference?.city?.touched || conference?.submitted)}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="country">Country</label>
|
||||
<input type="text" class="form-control" name="country" id="country"
|
||||
ng-model="options.billing.country" required
|
||||
ng-class="{'is-invalid': conference.country.$invalid && (conference.country.$touched || conference.$submitted)}" placeholder="Country" />
|
||||
<input type="text" class="form-control" name="country" id="country" required placeholder="Country" v-field="{ state: viewState, set: value => { options.billing.country = value }, value: options?.billing?.country, form: "conference", options: {} }" :class="{'is-invalid': conference?.country?.invalid && (conference?.country?.touched || conference?.submitted)}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="zip">Zip</label>
|
||||
<input type="text" class="form-control" name="zip" id="zip"
|
||||
ng-model="options.billing.zip" required
|
||||
ng-class="{'is-invalid': conference.zip.$invalid && (conference.zip.$touched || conference.$submitted)}" placeholder="Zip" />
|
||||
<input type="text" class="form-control" name="zip" id="zip" required placeholder="Zip" v-field="{ state: viewState, set: value => { options.billing.zip = value }, value: options?.billing?.zip, form: "conference", options: {} }" :class="{'is-invalid': conference?.zip?.invalid && (conference?.zip?.touched || conference?.submitted)}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="vat">VAT number</label>
|
||||
<input type="text" class="form-control" name="vat" id="vat"
|
||||
ng-class="{'is-invalid': conference.vat.$invalid && (conference.vat.$touched || conference.$submitted)}"
|
||||
ng-model="options.billing.vat" placeholder="VAT Number" />
|
||||
<input type="text" class="form-control" name="vat" id="vat" placeholder="VAT Number" v-field="{ state: viewState, set: value => { options.billing.vat = value }, value: options?.billing?.vat, form: "conference", options: {} }" :class="{'is-invalid': conference?.vat?.invalid && (conference?.vat?.touched || conference?.submitted)}">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="paper-settings-footer">
|
||||
<div class="alert alert-danger" role="alert" ng-if="error" ng-bind="error"></div>
|
||||
<div class="alert alert-success" role="alert" ng-if="message" ng-bind="message"></div>
|
||||
<button id="send" type="submit" class="btn btn-ink" ng-click="submit($event)">
|
||||
{{editionMode ? "Update" : "Create"}} conference
|
||||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||||
<div class="alert alert-success" role="alert" v-if="message" v-text="message"></div>
|
||||
<button id="send" type="submit" class="btn btn-ink" @click.prevent="submitForm($event, () => { submit($event) })">
|
||||
{{ editionMode ? "Update" : "Create" }} conference
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
<div ng-if="type == 'text'" ng-model="content" ui-ace="aceOption"></div>
|
||||
<div ng-if="type == 'html'" ng-bind-html="content" class="file-content markdown-body"></div>
|
||||
<html-doc
|
||||
ng-if="type == 'html-doc' && !showSource && content != null"
|
||||
content="content"
|
||||
base-url="{{fileBaseUrl}}"
|
||||
allow-scripts="allowScripts"
|
||||
></html-doc>
|
||||
<div
|
||||
ng-if="(type == 'code' || (type == 'html-doc' && showSource)) && content != null"
|
||||
ui-ace="aceOption"
|
||||
ng-model="content"
|
||||
></div>
|
||||
<img ng-if="type == 'image'" class="image-content" ng-src="{{url}}"></img>
|
||||
<iframe class="h-100 overflow-auto w-100 b-0" ng-if="type == 'media'" ng-src="{{url}}"></iframe>
|
||||
<pdfviewer ng-if="type == 'pdf'" src="{{url}}"></pdfviewer>
|
||||
<div ng-if="type == 'audio'"><audio controls="controls"><source ng-src="{{url}}" /></audio></div>
|
||||
<div ng-if="type == 'IPython'"><notebook file="url"></notebook></div>
|
||||
<div ng-if="type == 'rate_limited'" class="file-error container d-flex h-100">
|
||||
<div v-code-editor="{ content: content, options: aceOption }" v-if="type == 'text'"></div>
|
||||
<div class="file-content markdown-body" v-if="type == 'html'" v-html="sanitize(content)"></div>
|
||||
<html-doc v-if="type == 'html-doc' && !showSource && content != null" :content="content" :base-url="(fileBaseUrl)" :allow-scripts="allowScripts"></html-doc>
|
||||
<div v-code-editor="{ content: content, options: aceOption }" v-if="(type == 'code' || (type == 'html-doc' && showSource)) && content != null"></div>
|
||||
<img class="image-content" v-if="type == 'image'" :src="safeUrl((url))">
|
||||
<iframe class="h-100 overflow-auto w-100 b-0" v-if="type == 'media'" :src="safeUrl((url))"></iframe>
|
||||
<pdfviewer v-if="type == 'pdf'" :src="safeUrl((url))"></pdfviewer>
|
||||
<div v-if="type == 'audio'"><audio controls="controls"><source :src="safeUrl((url))"></audio></div>
|
||||
<div v-if="type == 'IPython'"><notebook :file="url" :content="content"></notebook></div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'rate_limited'">
|
||||
<div class="paper-ratelimit-card m-auto" style="max-width:520px;">
|
||||
<div class="paper-ratelimit-head">
|
||||
<i class="fas fa-hourglass-half"></i>
|
||||
@@ -25,11 +16,11 @@
|
||||
<div class="paper-error-title">GitHub API rate limit reached</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="paper-error-msg">This repository will be available in <strong>{{rateLimitCountdown}}</strong>. The page will reload automatically.</p>
|
||||
<p class="paper-error-msg">This repository will be available in <strong>{{ rateLimitCountdown }}</strong>. The page will reload automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="type == 'error'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto" translate="ERRORS.{{content}}">Error</h1></div>
|
||||
<div ng-if="type == 'loading' && !error" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Loading…</h1></div>
|
||||
<div ng-if="type == 'empty'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Empty <em>repository</em>.</h1></div>
|
||||
<div ng-if="content == null && type != 'empty'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Empty <em>file</em>.</h1></div>
|
||||
<div ng-if="type == 'binary'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Unsupported <em>binary file</em>. You can <a target="_blank" ng-href="{{url}}&download=true">download it</a>.</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'error'"><h1 class="paper-empty-title m-auto" v-text="fmt.translate("ERRORS." + (content))">Error</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'loading' && !error"><h1 class="paper-empty-title m-auto">Loading…</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'empty'"><h1 class="paper-empty-title m-auto">Empty <em>repository</em>.</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="content == null && type != 'empty'"><h1 class="paper-empty-title m-auto">Empty <em>file</em>.</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'binary'"><h1 class="paper-empty-title m-auto">Unsupported <em>binary file</em>. You can <a target="_blank" :href="safeUrl((url) + "&download=true")">download it</a>.</h1></div>
|
||||
|
||||
@@ -14,92 +14,64 @@
|
||||
<a href="/pull-request-anonymize" class="btn btn-ink">
|
||||
<i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> Anonymize PR
|
||||
</a>
|
||||
<a
|
||||
title="Claim the ownership of an existing anonymized repository."
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
href="/claim"
|
||||
class="btn"
|
||||
>
|
||||
<a title="Claim the ownership of an existing anonymized repository." data-toggle="tooltip" data-placement="bottom" href="/claim" class="btn">
|
||||
Claim
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + filters row -->
|
||||
<form class="w-100" aria-label="Pull Requests" accept-charset="UTF-8">
|
||||
<form class="w-100" aria-label="Pull Requests" accept-charset="UTF-8" v-form="viewState">
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-center" style="gap: 8px">
|
||||
<div class="flex-grow-1">
|
||||
<input
|
||||
type="search"
|
||||
id="search"
|
||||
class="form-control"
|
||||
aria-label="Find a pull request..."
|
||||
placeholder="Find a pull request..."
|
||||
autocomplete="off"
|
||||
ng-model="search"
|
||||
/>
|
||||
<input type="search" id="search" class="form-control" aria-label="Find a pull request..." placeholder="Find a pull request..." autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<div class="d-flex flex-wrap" style="gap: 6px">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn btn-sm dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownSort"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<button class="btn btn-sm dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Sort
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownSort">
|
||||
<h6 class="dropdown-header">Select order</h6>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortFullName" value="fullName" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortFullName" value="fullName" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortFullName">Pull Request</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortAnonymizeDate" value="-anonymizeDate" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortAnonymizeDate" value="-anonymizeDate" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortAnonymizeDate">Anonymize Date</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortStatus">Status</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortLastView" value="-lastView" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortLastView" value="-lastView" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortLastView">Last View</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortPageView" value="-pageView" ng-model="orderBy" />
|
||||
<input class="form-check-input" type="radio" name="sort" id="sortPageView" value="-pageView" v-field="{ state: viewState, set: value => { orderBy = value }, value: orderBy, form: null, options: {} }">
|
||||
<label class="form-check-label" for="sortPageView">Page View</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn btn-sm dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownStatus"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<button class="btn btn-sm dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Status
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
|
||||
<h6 class="dropdown-header">Select status</h6>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" name="sort" id="statusReady" value="ready" ng-model="filters.status.ready" />
|
||||
<input class="form-check-input" type="checkbox" name="sort" id="statusReady" value="ready" v-field="{ state: viewState, set: value => { filters.status.ready = value }, value: filters?.status?.ready, form: null, options: {} }">
|
||||
<label class="form-check-label" for="statusReady">Ready</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" name="sort" id="statusExpired" value="expired" ng-model="filters.status.expired" />
|
||||
<input class="form-check-input" type="checkbox" name="sort" id="statusExpired" value="expired" v-field="{ state: viewState, set: value => { filters.status.expired = value }, value: filters?.status?.expired, form: null, options: {} }">
|
||||
<label class="form-check-label" for="statusExpired">Expired</label>
|
||||
</div>
|
||||
<div class="form-check dropdown-item">
|
||||
<input class="form-check-input" type="checkbox" name="sort" id="statusRemoved" value="removed" ng-model="filters.status.removed" />
|
||||
<input class="form-check-input" type="checkbox" name="sort" id="statusRemoved" value="removed" v-field="{ state: viewState, set: value => { filters.status.removed = value }, value: filters?.status?.removed, form: null, options: {} }">
|
||||
<label class="form-check-label" for="statusRemoved">Removed</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,10 +81,10 @@
|
||||
</form>
|
||||
|
||||
<!-- Active filter chips -->
|
||||
<div class="d-flex flex-wrap mt-2" style="gap: 6px" ng-show="filters.status.ready === false || filters.status.expired === false || filters.status.removed === false">
|
||||
<span class="filter-chip" ng-show="!v" ng-repeat="(f, v) in filters.status">
|
||||
{{f | title}}
|
||||
<button type="button" class="filter-chip-close" aria-label="Remove filter" ng-click="filters.status[f] = true;">
|
||||
<div class="d-flex flex-wrap mt-2" style="gap: 6px" v-show="filters?.status?.ready === false || filters?.status?.expired === false || filters?.status?.removed === false">
|
||||
<span class="filter-chip" v-for="(v, f, index) in filters?.status" v-show="!v">
|
||||
{{ fmt?.title(f) }}
|
||||
<button type="button" class="filter-chip-close" aria-label="Remove filter" @click="filters.status[f] = true;">
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
@@ -121,88 +93,68 @@
|
||||
|
||||
<!-- Pull request list -->
|
||||
<ul class="repo-list w-100">
|
||||
<li
|
||||
class="repo-list-item"
|
||||
ng-class="{'repo-inactive': pr.status == 'expired' || pr.status == 'removed' || pr.status == 'error'}"
|
||||
ng-repeat="pr in pullRequests| filter:pullRequestFilter| orderBy:orderBy as filteredPullRequests"
|
||||
>
|
||||
<li class="repo-list-item" v-for="(pr, index) in filteredPullRequests" :class="{'repo-inactive': pr?.status == 'expired' || pr?.status == 'removed' || pr?.status == 'error'}">
|
||||
<div class="repo-list-item-content">
|
||||
<div class="repo-list-item-main">
|
||||
<div class="repo-list-item-header">
|
||||
<a ng-href="/pr/{{pr.pullRequestId}}" class="repo-name" ng-bind="pr.pullRequestId"></a>
|
||||
<span
|
||||
class="status-badge"
|
||||
ng-class="{'status-removed': pr.status == 'removed' || pr.status == 'expired' || pr.status == 'removing' || pr.status == 'expiring', 'status-preparing': pr.status == 'preparing' || pr.status == 'download', 'status-ready': pr.status == 'ready', 'status-error': pr.status == 'error'}"
|
||||
><span ng-bind="pr.status | title"></span><span
|
||||
ng-if="pr.status == 'error'"
|
||||
ng-bind="': ' + pr.statusMessage"
|
||||
></span></span>
|
||||
<a class="repo-name" v-text="pr?.pullRequestId" :href="safeUrl("/pr/" + (pr?.pullRequestId))"></a>
|
||||
<span class="status-badge" :class="{'status-removed': pr?.status == 'removed' || pr?.status == 'expired' || pr?.status == 'removing' || pr?.status == 'expiring', 'status-preparing': pr?.status == 'preparing' || pr?.status == 'download', 'status-ready': pr?.status == 'ready', 'status-error': pr?.status == 'error'}"><span v-text="fmt?.title(pr?.status)"></span><span v-if="pr?.status == 'error'" v-text="': ' + pr?.statusMessage"></span></span>
|
||||
</div>
|
||||
<div class="repo-source">
|
||||
<span>
|
||||
<i class="fab fa-github" aria-hidden="true"></i>
|
||||
<a href="https://github.com/{{pr.source.repositoryFullName}}/pull/{{pr.source.pullRequestId}}">{{pr.source.repositoryFullName}}#{{pr.source.pullRequestId}}</a>
|
||||
<a :href="safeUrl("https://github.com/" + (pr?.source?.repositoryFullName) + "/pull/" + (pr?.source?.pullRequestId))">{{ pr?.source?.repositoryFullName }}#{{ pr?.source?.pullRequestId }}</a>
|
||||
</span>
|
||||
<span
|
||||
class="status-badge"
|
||||
ng-class="{'status-ready': pr.merged, 'status-removed': pr.state=='open', 'status-error': pr.state=='closed' && !pr.merged}"
|
||||
style="font-size: 10px;"
|
||||
>
|
||||
{{pr.merged ? "merged" : pr.state | title}}
|
||||
<span class="status-badge" style="font-size: 10px;" :class="{'status-ready': pr?.merged, 'status-removed': pr?.state=='open', 'status-error': pr?.state=='closed' && !pr?.merged}">
|
||||
{{ fmt?.title(pr?.merged ? "merged" : pr?.state) }}
|
||||
</span>
|
||||
<span class="repo-date">anonymized {{pr.anonymizeDate | humanTime}}</span>
|
||||
<span class="repo-date">anonymized {{ fmt?.humanTime(pr?.anonymizeDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-meta">
|
||||
<span ng-if="::pr.conference" title="Conference">
|
||||
<i class="fas fa-chalkboard-teacher"></i> {{pr.conference}}
|
||||
<span title="Conference" v-if="pr?.conference">
|
||||
<i class="fas fa-chalkboard-teacher"></i> {{ pr?.conference }}
|
||||
</span>
|
||||
<span title="Terms: {{::pr.options.terms.join(', ')}}" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="fas fa-shield-alt"></i> {{::pr.options.terms.length | number}}
|
||||
<span data-toggle="tooltip" data-placement="bottom" :title=""Terms: " + (pr?.options?.terms?.join(', '))">
|
||||
<i class="fas fa-shield-alt"></i> {{ fmt?.number(pr?.options?.terms?.length) }}
|
||||
</span>
|
||||
<span title="Views: {{::pr.pageView | number}}" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="far fa-eye"></i> {{::pr.pageView | number}}
|
||||
<span data-toggle="tooltip" data-placement="bottom" :title=""Views: " + (fmt?.number(pr?.pageView))">
|
||||
<i class="far fa-eye"></i> {{ fmt?.number(pr?.pageView) }}
|
||||
</span>
|
||||
<span title="Last view: {{::pr.lastView | date}}" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="far fa-calendar-alt"></i> {{::pr.lastView | humanTime}}
|
||||
<span data-toggle="tooltip" data-placement="bottom" :title=""Last view: " + (fmt?.date(pr?.lastView))">
|
||||
<i class="far fa-calendar-alt"></i> {{ fmt?.humanTime(pr?.lastView) }}
|
||||
</span>
|
||||
<span ng-if="pr.options.expirationMode!='never' && pr.status == 'ready'">
|
||||
<i class="far fa-clock"></i> Expire: {{pr.options.expirationDate | humanTime}}
|
||||
<span v-if="pr?.options?.expirationMode!='never' && pr?.status == 'ready'">
|
||||
<i class="far fa-clock"></i> Expire: {{ fmt?.humanTime(pr?.options?.expirationDate) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-list-item-actions">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn btn-sm dropdown-toggle"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<button class="btn btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="/pull-request-anonymize/{{pr.pullRequestId}}">
|
||||
<a class="dropdown-item" :href="safeUrl("/pull-request-anonymize/" + (pr?.pullRequestId))">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" ng-show="pr.status == 'ready' || pr.status == 'error'" ng-click="updatePullRequest(pr)">
|
||||
<a class="dropdown-item" href="#" v-show="pr?.status == 'ready' || pr?.status == 'error'" @click.prevent="updatePullRequest(pr)">
|
||||
<i class="fas fa-sync"></i> Force update
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" ng-show="pr.status == 'removed'" ng-click="updatePullRequest(pr)">
|
||||
<a class="dropdown-item" href="#" v-show="pr?.status == 'removed'" @click.prevent="updatePullRequest(pr)">
|
||||
<i class="fas fa-check-circle"></i> Enable
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" ng-show="pr.status == 'ready'" ng-click="removePullRequest(pr)">
|
||||
<a class="dropdown-item" href="#" v-show="pr?.status == 'ready'" @click.prevent="removePullRequest(pr)">
|
||||
<i class="fas fa-trash-alt"></i> Remove
|
||||
</a>
|
||||
<a class="dropdown-item" href="/pr/{{pr.pullRequestId}}/">
|
||||
<a class="dropdown-item" :href="safeUrl("/pr/" + (pr?.pullRequestId) + "/")">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View PR
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="repo-list-empty" ng-if="filteredPullRequests.length == 0">
|
||||
<li class="repo-list-empty" v-if="filteredPullRequests?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<span>No pull requests to display.</span>
|
||||
</li>
|
||||
|
||||
+40
-55
@@ -4,7 +4,7 @@
|
||||
<p class="paper-page-lede">Defaults applied to every new anonymization, your quotas, and your account.</p>
|
||||
|
||||
<div class="paper-settings-body">
|
||||
<aside class="paper-settings-toc" paper-scrollspy>
|
||||
<aside class="paper-settings-toc" v-paper-scrollspy>
|
||||
<div class="paper-settings-toc-head">On this page</div>
|
||||
<nav>
|
||||
<a href="#settings-quota">Quota</a>
|
||||
@@ -21,65 +21,50 @@
|
||||
<section id="settings-quota" class="paper-settings-section">
|
||||
<div class="paper-section-eyebrow">Quota</div>
|
||||
<p class="paper-section-copy">What you are using across all your anonymizations. Conferences can lift these limits during a review window.</p>
|
||||
<div class="quota-row" ng-if="quota">
|
||||
<div class="quota-item" ng-repeat="q in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes'},
|
||||
{key: 'file', label: 'Files', kind: 'count'}
|
||||
<div class="quota-row" v-if="quota">
|
||||
<div class="quota-item" v-for="(q, index) in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes'},
|
||||
{key: 'file', label: 'Files', kind: 'count'}
|
||||
]">
|
||||
<div class="quota-header">
|
||||
<span class="quota-label">{{q.label}}</span>
|
||||
<span class="quota-value" ng-if="q.kind === 'count'">
|
||||
{{quota[q.key].used | number}}<span ng-if="!quota[q.key].unlimited"> / {{quota[q.key].total | number}}</span>
|
||||
<span class="quota-unlimited-tag" ng-if="quota[q.key].unlimited">Unlimited</span>
|
||||
<span class="quota-label">{{ q?.label }}</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'count'">
|
||||
{{ fmt?.number(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.number(quota[q?.key].total) }}</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
<span class="quota-value" ng-if="q.kind === 'bytes'">
|
||||
{{quota[q.key].used | humanFileSize}}<span ng-if="!quota[q.key].unlimited"> / {{quota[q.key].total | humanFileSize}}</span>
|
||||
<span class="quota-unlimited-tag" ng-if="quota[q.key].unlimited">Unlimited</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'bytes'">
|
||||
{{ fmt?.humanFileSize(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.humanFileSize(quota[q?.key].total) }}</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="quota-track"
|
||||
ng-class="'quota-' + quota[q.key].level"
|
||||
role="progressbar"
|
||||
aria-label="{{q.label}} quota"
|
||||
aria-valuenow="{{quota[q.key].used}}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="{{quota[q.key].unlimited ? quota[q.key].used : quota[q.key].total}}"
|
||||
aria-valuetext="{{quota[q.key].unlimited ? 'unlimited' : (quota[q.key].percent | number:0) + '% used'}}"
|
||||
>
|
||||
<div class="quota-fill" ng-if="!quota[q.key].unlimited" ng-style="{width: quota[q.key].percent + '%'}"></div>
|
||||
<div class="quota-track" role="progressbar" aria-valuemin="0" :class="'quota-' + quota[q?.key].level" :aria-label="(q?.label) + " quota"" :aria-valuenow="(quota[q?.key].used)" :aria-valuemax="(quota[q?.key].unlimited ? quota[q?.key].used : quota[q?.key].total)" :aria-valuetext="(quota[q?.key].unlimited ? 'unlimited' : fmt.number(quota[q?.key].percent, 0) + '% used')">
|
||||
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quota-row" ng-if="!quota" aria-hidden="true">
|
||||
<div class="quota-item" ng-repeat="i in [1, 2, 3]">
|
||||
<div class="quota-row" aria-hidden="true" v-if="!quota">
|
||||
<div class="quota-item" v-for="(i, index) in [1, 2, 3]">
|
||||
<div class="quota-header"><span class="skeleton skeleton-line" style="width: 40%"></span><span class="skeleton skeleton-line" style="width: 25%"></span></div>
|
||||
<div class="quota-track"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form class="form needs-validation" name="defaultsForm" novalidate ng-submit="saveDefault($event)">
|
||||
<form class="form needs-validation" name="defaultsForm" novalidate v-form="viewState" @submit.prevent="submitForm($event, () => { saveDefault($event) })">
|
||||
<section id="settings-terms" class="paper-settings-section">
|
||||
<div class="paper-section-eyebrow">Terms to redact</div>
|
||||
<p class="paper-section-copy">Pre-filled in every new anonymization. You can still edit the list per anonymization.</p>
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="terms">Default terms</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="terms"
|
||||
name="terms"
|
||||
rows="4"
|
||||
placeholder="Jane Smith MIT CSAIL jane.smith@example.org"
|
||||
ng-model="terms"
|
||||
ng-model-options="{ debounce: 250 }"
|
||||
></textarea>
|
||||
<textarea class="form-control" id="terms" name="terms" rows="4" placeholder="Jane Smith
|
||||
MIT CSAIL
|
||||
jane.smith@example.org" v-field="{ state: viewState, set: value => { terms = value }, value: terms, form: "defaultsForm", options: { debounce: 250 } }"></textarea>
|
||||
<small id="termsHelp" class="form-text text-muted">
|
||||
One term per line (regex allowed). Each match is replaced by
|
||||
<code>{{site_options.ANONYMIZATION_MASK || 'XXX'}}-[N]</code>, or use <code>term=>replacement</code> to pick your own.
|
||||
<code>{{ site_options?.ANONYMIZATION_MASK || 'XXX' }}-[N]</code>, or use <code>term=>replacement</code> to pick your own.
|
||||
</small>
|
||||
<div class="invalid-feedback" ng-show="defaultsForm.terms.$error.format">
|
||||
<div class="invalid-feedback" v-show="defaultsForm?.terms?.errors?.format">
|
||||
Terms are in an invalid format
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,22 +75,22 @@
|
||||
<p class="paper-section-copy">What reviewers can see inside an anonymized repository.</p>
|
||||
<div class="form-group paper-check-list">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="link" name="link" ng-model="options.link" />
|
||||
<input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value => { options.link = value }, value: options?.link, form: "defaultsForm", options: {} }">
|
||||
<label class="form-check-label" for="link">Keep links</label>
|
||||
<small class="form-text text-muted">Unchecked, every link in text files is removed.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="image" name="image" ng-model="options.image" />
|
||||
<input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value => { options.image = value }, value: options?.image, form: "defaultsForm", options: {} }">
|
||||
<label class="form-check-label" for="image">Display images</label>
|
||||
<small class="form-text text-muted">Images are shown as is. They are not anonymized.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" ng-model="options.pdf" />
|
||||
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value => { options.pdf = value }, value: options?.pdf, form: "defaultsForm", options: {} }">
|
||||
<label class="form-check-label" for="pdf">Display PDFs</label>
|
||||
<small class="form-text text-muted">PDFs are shown as is. They are not anonymized.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" ng-model="options.notebook" />
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.notebook = value }, value: options?.notebook, form: "defaultsForm", options: {} }">
|
||||
<label class="form-check-label" for="notebook">Display notebooks</label>
|
||||
<small class="form-text text-muted">Render Jupyter notebooks instead of raw JSON.</small>
|
||||
</div>
|
||||
@@ -116,30 +101,30 @@
|
||||
<div class="paper-section-eyebrow">Features</div>
|
||||
<div class="form-group paper-check-list">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="page" name="page" ng-model="options.page" />
|
||||
<input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value => { options.page = value }, value: options?.page, form: "defaultsForm", options: {} }">
|
||||
<label class="form-check-label" for="page">GitHub Pages</label>
|
||||
<small class="form-text text-muted">Serve an anonymized copy of the repository's GitHub Pages site. Only pages built from the anonymized branch are supported.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="loc" name="loc" ng-model="options.loc" />
|
||||
<input class="form-check-input" type="checkbox" id="loc" name="loc" v-field="{ state: viewState, set: value => { options.loc = value }, value: options?.loc, form: "defaultsForm", options: {} }">
|
||||
<label class="form-check-label" for="loc">Lines of code</label>
|
||||
<small class="form-text text-muted">Show the line count of the repository in the explorer.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="update" name="update" ng-model="options.update" />
|
||||
<input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value => { options.update = value }, value: options?.update, form: "defaultsForm", options: {} }">
|
||||
<label class="form-check-label" for="update">Auto-update from GitHub</label>
|
||||
<small class="form-text text-muted">Follow the branch and pull the latest commit automatically, at most once an hour.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="mode">Fetch mode</label>
|
||||
<select class="form-control" id="mode" name="mode" ng-model="options.mode">
|
||||
<select class="form-control" id="mode" name="mode" v-field="{ state: viewState, set: value => { options.mode = value }, value: options?.mode, form: "defaultsForm", options: {} }">
|
||||
<option value="GitHubStream" selected>Stream from GitHub on demand</option>
|
||||
<option value="GitHubDownload">Download to Anonymous GitHub</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Download is faster for reviewers and enables every feature. Streaming is the only option above
|
||||
{{site_options.MAX_REPO_SIZE * 1024 | humanFileSize}}.
|
||||
{{ fmt?.humanFileSize(site_options?.MAX_REPO_SIZE * 1024) }}.
|
||||
</small>
|
||||
</div>
|
||||
</section>
|
||||
@@ -148,7 +133,7 @@
|
||||
<div class="paper-section-eyebrow">Expiration</div>
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="expiration">When an anonymization expires</label>
|
||||
<select class="form-control" id="expiration" name="expiration" ng-model="options.expirationMode">
|
||||
<select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value => { options.expirationMode = value }, value: options?.expirationMode, form: "defaultsForm", options: {} }">
|
||||
<option value="redirect">Redirect visitors to the GitHub repository</option>
|
||||
<option value="remove" selected>Remove the anonymized repository</option>
|
||||
</select>
|
||||
@@ -159,10 +144,10 @@
|
||||
</section>
|
||||
|
||||
<div class="paper-settings-footer">
|
||||
<div class="paper-inline-alert paper-inline-alert-error" role="alert" ng-if="error"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <span ng-bind="error"></span></div>
|
||||
<div class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="error"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <span v-text="error"></span></div>
|
||||
<div class="paper-settings-actions">
|
||||
<button id="save" type="submit" class="btn btn-ink" ng-disabled="saving">
|
||||
<i class="fas fa-check mr-1" aria-hidden="true" ng-if="message"></i>{{ saving ? 'Saving…' : (message ? 'Saved' : 'Save defaults') }}
|
||||
<button id="save" type="submit" class="btn btn-ink" :disabled="saving">
|
||||
<i class="fas fa-check mr-1" aria-hidden="true" v-if="message"></i>{{ saving ? 'Saving…' : (message ? 'Saved' : 'Save defaults') }}
|
||||
</button>
|
||||
<span class="paper-settings-hint">Applies to new anonymizations only. Existing ones keep their settings.</span>
|
||||
</div>
|
||||
@@ -172,9 +157,9 @@
|
||||
<section id="settings-account" class="paper-settings-section">
|
||||
<div class="paper-section-eyebrow">Account</div>
|
||||
<div class="paper-account-card">
|
||||
<img ng-src="{{user.photo}}" ng-if="user.photo" width="40" height="40" class="rounded-circle" alt="" />
|
||||
<img width="40" height="40" class="rounded-circle" alt="" v-if="user?.photo" :src="safeUrl((user?.photo))">
|
||||
<div class="paper-account-identity">
|
||||
<div class="paper-account-name">@{{user.username}}</div>
|
||||
<div class="paper-account-name">@{{ user?.username }}</div>
|
||||
<div class="paper-account-meta">Signed in with GitHub. Anonymous GitHub only reads your repositories.</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-ink" href="/api/user/logout" target="_self">Sign out</a>
|
||||
@@ -187,8 +172,8 @@
|
||||
revokes the application's access to your GitHub account, and erases
|
||||
your personal data (username, email, access tokens). This cannot be undone.
|
||||
</p>
|
||||
<div class="paper-inline-alert paper-inline-alert-error" role="alert" ng-if="deleteError"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <span ng-bind="deleteError"></span></div>
|
||||
<button type="button" class="btn btn-outline-danger" ng-click="deleteAccount()" ng-disabled="deletingAccount">
|
||||
<div class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="deleteError"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <span v-text="deleteError"></span></div>
|
||||
<button type="button" class="btn btn-outline-danger" @click="deleteAccount()" :disabled="deletingAccount">
|
||||
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>{{ deletingAccount ? 'Deleting…' : 'Delete my account' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -7,84 +7,61 @@
|
||||
|
||||
<header class="pr-header">
|
||||
<h1 class="paper-page-title pr-title">
|
||||
<span ng-if="details.title" ng-bind="details.title"></span>
|
||||
<span ng-if="!details.title" class="text-muted">Untitled pull request</span>
|
||||
<a
|
||||
ng-if="options.isAdmin || options.isOwner"
|
||||
ng-href="/pull-request-anonymize/{{pullRequestId}}"
|
||||
class="btn btn-sm"
|
||||
aria-label="Edit"
|
||||
><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a
|
||||
>
|
||||
<span v-if="details?.title" v-text="details?.title"></span>
|
||||
<span class="text-muted" v-if="!details?.title">Untitled pull request</span>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/pull-request-anonymize/" + (pullRequestId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</h1>
|
||||
<div class="pr-header-meta">
|
||||
<span class="paper-pill" ng-class="{'good': details.merged, 'warn': details.state == 'open', 'bad': details.state == 'closed' && !details.merged}">
|
||||
<span class="status-dot" ng-class="{'status-ready': details.merged, 'status-error': details.state == 'closed' && !details.merged}"></span>
|
||||
{{ details.merged ? 'Merged' : (details.state | title) }}
|
||||
<span class="paper-pill" :class="{'good': details?.merged, 'warn': details?.state == 'open', 'bad': details?.state == 'closed' && !details?.merged}">
|
||||
<span class="status-dot" :class="{'status-ready': details?.merged, 'status-error': details?.state == 'closed' && !details?.merged}"></span>
|
||||
{{ details?.merged ? 'Merged' : fmt.title(details?.state) }}
|
||||
</span>
|
||||
<span class="pr-meta-item" ng-if="details.baseRepositoryFullName">
|
||||
<i class="fab fa-github"></i> <span ng-bind="details.baseRepositoryFullName"></span>
|
||||
<span class="pr-meta-item" v-if="details?.baseRepositoryFullName">
|
||||
<i class="fab fa-github"></i> <span v-text="details?.baseRepositoryFullName"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" ng-if="details.updatedDate">
|
||||
<i class="far fa-clock"></i> <span ng-bind="details.updatedDate | date"></span>
|
||||
<span class="pr-meta-item" v-if="details?.updatedDate">
|
||||
<i class="far fa-clock"></i> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" ng-if="details.anonymizeDate">
|
||||
<i class="fas fa-user-secret"></i> Anonymized <span ng-bind="details.anonymizeDate | date"></span>
|
||||
<span class="pr-meta-item" v-if="details?.anonymizeDate">
|
||||
<i class="fas fa-user-secret"></i> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="pr-body-card" ng-if="details.body">
|
||||
<section class="pr-body-card" v-if="details?.body">
|
||||
<div class="paper-section-eyebrow">Description</div>
|
||||
<markdown content="details.body"></markdown>
|
||||
<markdown :content="details?.body"></markdown>
|
||||
</section>
|
||||
|
||||
<nav class="paper-tabs" ng-if="details.diff || details.comments" role="tablist">
|
||||
<button
|
||||
class="paper-tab"
|
||||
ng-if="details.diff"
|
||||
ng-class="{'active': tabState.active == 'diff'}"
|
||||
ng-click="tabState.active = 'diff'"
|
||||
type="button"
|
||||
role="tab"
|
||||
>
|
||||
<nav class="paper-tabs" role="tablist" v-if="details?.diff || details?.comments">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.diff" :class="{'active': tabState?.active == 'diff'}" @click="tabState.active = 'diff'">
|
||||
<i class="fas fa-code"></i> Diff
|
||||
</button>
|
||||
<button
|
||||
class="paper-tab"
|
||||
ng-if="details.comments"
|
||||
ng-class="{'active': tabState.active == 'comments'}"
|
||||
ng-click="tabState.active = 'comments'"
|
||||
type="button"
|
||||
role="tab"
|
||||
>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" @click="tabState.active = 'comments'">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<ng-pluralize
|
||||
count="details.comments.length"
|
||||
when="{'0': 'No comments', 'one': '1 comment', 'other': '{} comments'}"
|
||||
></ng-pluralize>
|
||||
<span v-text="fmt.plural(details?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="paper-tab-content">
|
||||
<div ng-if="details.diff && tabState.active =='diff'">
|
||||
<div class="pr-diff" ng-bind-html="details.diff | diff"></div>
|
||||
<div v-if="details?.diff && tabState?.active =='diff'">
|
||||
<div class="pr-diff" v-html="sanitize(fmt?.diff(details?.diff))"></div>
|
||||
</div>
|
||||
|
||||
<div ng-if="details.comments && tabState.active =='comments'">
|
||||
<div v-if="details?.comments && tabState?.active =='comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" ng-repeat="comment in details.comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" ng-if="comment.author">
|
||||
<i class="far fa-user"></i> @<span ng-bind="comment.author"></span>
|
||||
<span class="pr-comment-author" v-if="comment?.author">
|
||||
<i class="far fa-user"></i> @<span v-text="comment?.author"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" ng-if="comment.updatedDate" ng-bind="comment.updatedDate | date"></span>
|
||||
<span class="pr-comment-date" v-if="comment?.updatedDate" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
<div class="pr-comment-body" ng-if="comment.body">
|
||||
<markdown content="comment.body"></markdown>
|
||||
<div class="pr-comment-body" v-if="comment?.body">
|
||||
<markdown :content="comment?.body"></markdown>
|
||||
</div>
|
||||
</li>
|
||||
<li class="paper-table-empty" ng-if="!details.comments.length">
|
||||
<li class="paper-table-empty" v-if="!details?.comments?.length">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<span>No comments on this pull request.</span>
|
||||
</li>
|
||||
|
||||
+23
-28
@@ -2,13 +2,13 @@
|
||||
<div class="paper-crumbs">Anonymization / <span class="here">Status</span></div>
|
||||
<div class="d-flex align-items-end justify-content-between flex-wrap" style="gap: 12px;">
|
||||
<div>
|
||||
<h1 class="paper-page-title">Status of <em>{{repoId}}</em></h1>
|
||||
<h1 class="paper-page-title">Status of <em>{{ repoId }}</em></h1>
|
||||
<p class="paper-page-lede">Track progress as your anonymization is prepared.</p>
|
||||
</div>
|
||||
<span class="status-pill" ng-class="{'status-pill-ready': repo.status == 'ready', 'status-pill-error': repo.status == 'error', 'status-pill-removed': repo.status == 'removed' || repo.status == 'expired', 'status-pill-ratelimit': rateLimitResetAt}">
|
||||
<span class="status-dot" ng-class="{'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ratelimit': rateLimitResetAt}"></span>
|
||||
<span ng-if="!rateLimitResetAt" ng-bind="repo.status | title"></span>
|
||||
<span ng-if="rateLimitResetAt">Rate limited</span>
|
||||
<span class="status-pill" :class="{'status-pill-ready': repo?.status == 'ready', 'status-pill-error': repo?.status == 'error', 'status-pill-removed': repo?.status == 'removed' || repo?.status == 'expired', 'status-pill-ratelimit': rateLimitResetAt}">
|
||||
<span class="status-dot" :class="{'status-ready': repo?.status == 'ready', 'status-error': repo?.status == 'error', 'status-removed': repo?.status == 'removed' || repo?.status == 'expired', 'status-ratelimit': rateLimitResetAt}"></span>
|
||||
<span v-if="!rateLimitResetAt" v-text="fmt?.title(repo?.status)"></span>
|
||||
<span v-if="rateLimitResetAt">Rate limited</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
size. Visit the <a href="/faq">FAQ</a> for more information.
|
||||
</p>
|
||||
|
||||
<div class="paper-ratelimit-card" ng-if="rateLimitResetAt" role="status">
|
||||
<div class="paper-ratelimit-card" role="status" v-if="rateLimitResetAt">
|
||||
<div class="paper-ratelimit-head">
|
||||
<i class="fas fa-hourglass-half"></i>
|
||||
<div>
|
||||
@@ -28,18 +28,18 @@
|
||||
<div class="paper-error-title">GitHub API rate limit reached</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="paper-error-msg">Anonymization will resume automatically in <strong>{{rateLimitCountdown}}</strong>. No action needed — the job is queued and will continue where it left off.</p>
|
||||
<p class="paper-error-msg">Anonymization will resume automatically in <strong>{{ rateLimitCountdown }}</strong>. No action needed — the job is queued and will continue where it left off.</p>
|
||||
</div>
|
||||
|
||||
<div class="paper-progress" ng-if="repo.status != 'error' && !rateLimitResetAt" role="progressbar" aria-valuenow="{{progress}}" aria-valuemin="0" aria-valuemax="100" ng-class="{'paper-progress-ready': repo.status == 'ready'}">
|
||||
<div class="paper-progress-bar" style="width: {{progress}}%;"></div>
|
||||
<div class="paper-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" v-if="repo?.status != 'error' && !rateLimitResetAt" :class="{'paper-progress-ready': repo?.status == 'ready'}" :aria-valuenow="(progress)">
|
||||
<div class="paper-progress-bar" :style=""width: " + (progress) + "%;""></div>
|
||||
<div class="paper-progress-label">
|
||||
<span ng-bind="repo.status | title"></span><span ng-if="repo.statusMessage"> · <span ng-bind="repo.statusMessage"></span></span>
|
||||
<span class="paper-progress-pct">{{progress || 0}}%</span>
|
||||
<span v-text="fmt?.title(repo?.status)"></span><span v-if="repo?.statusMessage"> · <span v-text="repo?.statusMessage"></span></span>
|
||||
<span class="paper-progress-pct">{{ progress || 0 }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paper-error-card" ng-if="repo.status == 'error' && !rateLimitResetAt" role="alert">
|
||||
<div class="paper-error-card" role="alert" v-if="repo?.status == 'error' && !rateLimitResetAt">
|
||||
<div class="paper-error-head">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<div>
|
||||
@@ -47,15 +47,15 @@
|
||||
<div class="paper-error-title">Something went wrong while preparing this repository.</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="paper-error-msg" ng-if="repo.statusMessage">{{ 'ERRORS.' + repo.statusMessage | translate }}</p>
|
||||
<p class="paper-error-msg" ng-if="!repo.statusMessage">No additional details were reported. The most common causes are private repositories, missing branches, and rate limits.</p>
|
||||
<p class="paper-error-msg" v-if="repo?.statusMessage">{{ fmt?.translate('ERRORS.' + repo?.statusMessage) }}</p>
|
||||
<p class="paper-error-msg" v-if="!repo?.statusMessage">No additional details were reported. The most common causes are private repositories, missing branches, and rate limits.</p>
|
||||
<ul class="paper-error-hints">
|
||||
<li>Make sure the source URL points to a repository or pull request you can access.</li>
|
||||
<li>Check that the chosen branch and commit still exist on GitHub.</li>
|
||||
<li>If you just signed in, the access token may need a moment to propagate — try again.</li>
|
||||
</ul>
|
||||
<div class="paper-error-actions">
|
||||
<a class="btn btn-ink" ng-href="/anonymize/{{repoId}}"><i class="far fa-edit mr-1"></i> Edit anonymization</a>
|
||||
<a class="btn btn-ink" :href="safeUrl("/anonymize/" + (repoId))"><i class="far fa-edit mr-1"></i> Edit anonymization</a>
|
||||
<a class="btn" href="/faq"><i class="far fa-question-circle mr-1"></i> Read the FAQ</a>
|
||||
<a class="btn" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><i class="fas fa-bug mr-1" aria-hidden="true"></i> Report a bug in Anonymous GitHub</a>
|
||||
</div>
|
||||
@@ -64,19 +64,19 @@
|
||||
<div class="paper-detail-grid">
|
||||
<div class="detail-label">Repository</div>
|
||||
<div class="detail-value">
|
||||
<a href="/r/{{repoId}}/" target="_self">/r/{{repoId}}/</a>
|
||||
<a target="_self" :href="safeUrl("/r/" + (repoId) + "/")">/r/{{ repoId }}/</a>
|
||||
</div>
|
||||
<div class="detail-label" ng-if="repo.options.page">GitHub Page</div>
|
||||
<div class="detail-value" ng-if="repo.options.page">
|
||||
<a href="/w/{{repoId}}/" target="_self">/w/{{repoId}}/</a>
|
||||
<div class="detail-label" v-if="repo?.options?.page">GitHub Page</div>
|
||||
<div class="detail-value" v-if="repo?.options?.page">
|
||||
<a target="_self" :href="safeUrl("/w/" + (repoId) + "/")">/w/{{ repoId }}/</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="anonymize-submit-bar" ng-if="repo.status == 'ready'">
|
||||
<a class="btn btn-ink" href="/r/{{repoId}}/" target="_self">
|
||||
<div class="anonymize-submit-bar" v-if="repo?.status == 'ready'">
|
||||
<a class="btn btn-ink" target="_self" :href="safeUrl("/r/" + (repoId) + "/")">
|
||||
<i class="far fa-eye mr-1"></i> Go to anonymized repository
|
||||
</a>
|
||||
<a class="btn" href="/w/{{repoId}}/" target="_self" ng-if="repo.options.page">
|
||||
<a class="btn" target="_self" v-if="repo?.options?.page" :href="safeUrl("/w/" + (repoId) + "/")">
|
||||
<i class="fas fa-globe mr-1"></i> Go to anonymized GitHub page
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,12 +108,7 @@
|
||||
</div>
|
||||
|
||||
<div class="paper-kofi-wrap">
|
||||
<iframe
|
||||
id="kofiframe"
|
||||
src="https://ko-fi.com/tdurieux/?hidefeed=true&widget=true&embed=true&preview=true"
|
||||
title="tdurieux"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
<iframe id="kofiframe" src="https://ko-fi.com/tdurieux/?hidefeed=true&widget=true&embed=true&preview=true" title="tdurieux" loading="lazy"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user