refactor: migrate the frontend from AngularJS to Vue 3

This commit is contained in:
tdurieux
2026-09-09 13:19:53 +02:00
parent dc0ef022fb
commit 4a18f94631
65 changed files with 6164 additions and 8425 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="paper-empty-inner">
<div class="paper-eyebrow">Error &middot; 404</div>
<h1 class="paper-empty-title">
This link<br /><em>isn&rsquo;t here.</em>
This link<br><em>isn&rsquo;t here.</em>
</h1>
<p class="paper-empty-lede">
The anonymous mirror you&rsquo;re looking for has expired, been removed
+42 -46
View File
@@ -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 =&gt; { 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 &lt;= 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 &gt;= 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 == &#x27;name&#x27;}" @click="sortBy(&#x27;name&#x27;)">Conference <i class="fas" :class="sortIcon(&#x27;name&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;status&#x27;}" @click="sortBy(&#x27;status&#x27;)">Status <i class="fas" :class="sortIcon(&#x27;status&#x27;)"></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 == &#x27;startDate&#x27;}" @click="sortBy(&#x27;startDate&#x27;)">Window <i class="fas" :class="sortIcon(&#x27;startDate&#x27;)"></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(&quot;/conference/&quot; + (conference?.conferenceID))"></a>
<div class="anon-sub">
<span>{{conference.conferenceID}}</span>&nbsp;&middot;&nbsp;<span>{{conference.price || 0 | number}} &euro;</span>
<span>{{ conference?.conferenceID }}</span>&nbsp;&middot;&nbsp;<span>{{ fmt?.number(conference?.price || 0) }} &euro;</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="{&#x27;status-removed&#x27;: conference?.status == &#x27;removed&#x27; || conference?.status == &#x27;expired&#x27;, &#x27;status-ready&#x27;: conference?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: conference?.status == &#x27;error&#x27;, &#x27;status-preparing&#x27;: conference?.status == &#x27;preparing&#x27;}"></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(&quot;/admin/?conference=&quot; + (conference?.conferenceID))"></a>
</div>
<div class="cell-expires" role="cell">{{conference.startDate | date}} &ndash; {{conference.endDate | date}}</div>
<div class="cell-expires" role="cell">{{ fmt?.date(conference?.startDate) }} &ndash; {{ 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(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/edit&quot;)"><i class="far fa-edit"></i> Edit</a>
<a class="dropdown-item" :href="safeUrl(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/&quot;)"><i class="fa fa-eye"></i> View</a>
<a class="dropdown-item" :href="safeUrl(&quot;/admin/?conference=&quot; + (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 != &#x27;removed&#x27;" @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 &gt; 1">
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page &lt;= 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 =&gt; { 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 &gt;= 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 =&gt; { 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>
+71 -71
View File
@@ -4,8 +4,8 @@
<header class="errors-header">
<h1 class="paper-page-title">Errors</h1>
<div class="errors-actions">
<button class="btn btn-sm" type="button" ng-click="exportCsv()"><i class="fas fa-file-export"></i> Export CSV</button>
<button class="btn btn-sm btn-danger" type="button" ng-click="clearAll()"><i class="fas fa-trash"></i> Clear all</button>
<button class="btn btn-sm" type="button" @click="exportCsv()"><i class="fas fa-file-export"></i> Export CSV</button>
<button class="btn btn-sm btn-danger" type="button" @click="clearAll()"><i class="fas fa-trash"></i> Clear all</button>
</div>
</header>
@@ -21,33 +21,33 @@
<section class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">Last 24h</div>
<div class="kpi-value">{{stats.last24h}}</div>
<div class="kpi-sub" ng-class="{up: stats.delta > 0, down: stats.delta < 0}">
<span ng-if="stats.prev24h">{{stats.delta > 0 ? '+' : ''}}{{stats.delta}}% vs yesterday</span>
<span ng-if="!stats.prev24h">no prior baseline</span>
<div class="kpi-value">{{ stats?.last24h }}</div>
<div class="kpi-sub" :class="{up: stats?.delta &gt; 0, down: stats?.delta &lt; 0}">
<span v-if="stats?.prev24h">{{ stats?.delta > 0 ? '+' : '' }}{{ stats?.delta }}% vs yesterday</span>
<span v-if="!stats?.prev24h">no prior baseline</span>
</div>
</div>
<div class="kpi-card kpi-error">
<div class="kpi-label">Errors (5xx)</div>
<div class="kpi-value">{{stats.severity.error}}</div>
<div class="kpi-sub">{{stats.unique.error}} unique</div>
<div class="kpi-value">{{ stats?.severity?.error }}</div>
<div class="kpi-sub">{{ stats?.unique?.error }} unique</div>
</div>
<div class="kpi-card kpi-warn">
<div class="kpi-label">Warnings (4xx)</div>
<div class="kpi-value">{{stats.severity.warn}}</div>
<div class="kpi-sub">{{stats.unique.warn}} unique</div>
<div class="kpi-value">{{ stats?.severity?.warn }}</div>
<div class="kpi-sub">{{ stats?.unique?.warn }} unique</div>
</div>
<div class="kpi-card kpi-info">
<div class="kpi-label">Info (auth, 404)</div>
<div class="kpi-value">{{stats.severity.info}}</div>
<div class="kpi-sub">{{stats.unique.info}} unique</div>
<div class="kpi-value">{{ stats?.severity?.info }}</div>
<div class="kpi-sub">{{ stats?.unique?.info }} unique</div>
</div>
<div class="kpi-card" ng-class="{'kpi-error': stats.dropped > 0}">
<div class="kpi-card" :class="{&#x27;kpi-error&#x27;: stats?.dropped &gt; 0}">
<div class="kpi-label">Captured</div>
<div class="kpi-value">{{total}}</div>
<div class="kpi-value">{{ total }}</div>
<div class="kpi-sub">
cap {{cap}} · {{available ? 'live' : 'redis off'}}
<span ng-if="stats.dropped > 0" class="dropped-warn"> · {{stats.dropped}} dropped</span>
cap {{ cap }} · {{ available ? 'live' : 'redis off' }}
<span class="dropped-warn" v-if="stats?.dropped &gt; 0"> · {{ stats?.dropped }} dropped</span>
</div>
</div>
</section>
@@ -62,55 +62,55 @@
</span>
</div>
<div class="volume-bars">
<div class="volume-bar" ng-repeat="b in stats.buckets track by $index" title="{{bucketTitle(b)}}">
<span class="seg seg-error" ng-style="{height: barPx(b, 'error') + 'px'}"></span>
<span class="seg seg-warn" ng-style="{height: barPx(b, 'warn') + 'px'}"></span>
<span class="seg seg-info" ng-style="{height: barPx(b, 'info') + 'px'}"></span>
<div class="volume-bar" v-for="(b, index) in stats?.buckets" :key="index" :title="(bucketTitle(b))">
<span class="seg seg-error" :style="{height: barPx(b, &#x27;error&#x27;) + &#x27;px&#x27;}"></span>
<span class="seg seg-warn" :style="{height: barPx(b, &#x27;warn&#x27;) + &#x27;px&#x27;}"></span>
<span class="seg seg-info" :style="{height: barPx(b, &#x27;info&#x27;) + &#x27;px&#x27;}"></span>
</div>
</div>
</section>
<form class="errors-toolbar" aria-label="Error filters">
<form class="errors-toolbar" aria-label="Error filters" v-form="viewState">
<div class="seg-tabs">
<button type="button" ng-class="{active: query.bucket === ''}" ng-click="setBucket('')">All</button>
<button type="button" ng-class="{active: query.bucket === 'error'}" ng-click="setBucket('error')">5xx</button>
<button type="button" ng-class="{active: query.bucket === 'warn'}" ng-click="setBucket('warn')">4xx</button>
<button type="button" ng-class="{active: query.bucket === 'info'}" ng-click="setBucket('info')">Info</button>
<button type="button" :class="{active: query?.bucket === &#x27;&#x27;}" @click="setBucket(&#x27;&#x27;)">All</button>
<button type="button" :class="{active: query?.bucket === &#x27;error&#x27;}" @click="setBucket(&#x27;error&#x27;)">5xx</button>
<button type="button" :class="{active: query?.bucket === &#x27;warn&#x27;}" @click="setBucket(&#x27;warn&#x27;)">4xx</button>
<button type="button" :class="{active: query?.bucket === &#x27;info&#x27;}" @click="setBucket(&#x27;info&#x27;)">Info</button>
</div>
<div class="search-wrap">
<i class="fas fa-search search-icon"></i>
<input type="search" class="form-control" placeholder="code:repo_not_found module:route status:>=400" ng-model="query.search" autocomplete="off" />
<span class="filter-count" ng-if="parsedFilterCount">{{parsedFilterCount}} filter{{parsedFilterCount > 1 ? 's' : ''}}</span>
<input type="search" class="form-control" placeholder="code:repo_not_found module:route status:&gt;=400" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { query.search = value }, value: query?.search, form: null, options: {} }">
<span class="filter-count" v-if="parsedFilterCount">{{ parsedFilterCount }} filter{{ parsedFilterCount > 1 ? 's' : '' }}</span>
</div>
<div class="select-wrap">
<label>Sort</label>
<select class="form-control form-control-sm" ng-model="query.sort">
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.sort = value }, value: query?.sort, form: null, options: {} }">
<option value="recent">Most recent</option>
<option value="count">Most frequent</option>
</select>
</div>
<div class="select-wrap">
<label>Group</label>
<select class="form-control form-control-sm" ng-model="query.group">
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.group = value }, value: query?.group, form: null, options: {} }">
<option value="">Off</option>
<option value="code">By code</option>
<option value="module">By module</option>
</select>
</div>
<label class="autoref">
<input type="checkbox" ng-model="query.autoRefresh" />
<input type="checkbox" v-field="{ state: viewState, set: value =&gt; { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
Auto-refresh
</label>
<button class="btn btn-sm btn-icon" type="button" ng-click="refreshNow()" title="Refresh now"><i class="fas fa-sync"></i></button>
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()"><i class="fas fa-sync"></i></button>
</form>
<div ng-if="!visible.length" class="admin-empty">No errors captured.</div>
<div ng-if="canLoadMore() && visible.length" class="errors-pager">
<span>Showing {{entries.length}} of {{total}} captured</span>
<button class="btn btn-sm" type="button" ng-click="loadMore()">Load older</button>
<div class="admin-empty" v-if="!visible?.length">No errors captured.</div>
<div class="errors-pager" v-if="canLoadMore() &amp;&amp; visible?.length">
<span>Showing {{ entries?.length }} of {{ total }} captured</span>
<button class="btn btn-sm" type="button" @click="loadMore()">Load older</button>
</div>
<div class="errors-list" ng-if="visible.length">
<div class="errors-list" v-if="visible?.length">
<div class="errors-list-head">
<span class="col-when">When</span>
<span class="col-sev">Severity</span>
@@ -120,75 +120,75 @@
<span class="col-status">Status</span>
</div>
<div class="errors-row" ng-repeat="row in visible track by row._key" ng-class="{open: expanded[row._key]}">
<div class="errors-row-main" ng-click="toggle(row)">
<div class="errors-row" v-for="(row, index) in visible" :key="row?._key" :class="{open: expanded[row?._key]}">
<div class="errors-row-main" @click="toggle(row)">
<div class="col-when">
<div class="when-rel">{{relTime(row.ts)}}</div>
<div class="when-abs">{{absTimeShort(row.ts)}}</div>
<div class="when-rel">{{ relTime(row?.ts) }}</div>
<div class="when-abs">{{ absTimeShort(row?.ts) }}</div>
</div>
<div class="col-sev">
<span class="sev-dot" ng-class="'sev-' + row._bucket"></span>
<span class="sev-label">{{row._bucket | uppercase}}</span>
<span class="sev-dot" :class="&#x27;sev-&#x27; + row?._bucket"></span>
<span class="sev-label">{{ fmt?.uppercase(row?._bucket) }}</span>
</div>
<div class="col-mod"><span class="pill pill-module">{{row.module}}</span></div>
<div class="col-mod"><span class="pill pill-module">{{ row?.module }}</span></div>
<div class="col-msg">
<strong class="msg-code">{{row.displayMessage}}</strong>
<span class="msg-context" ng-if="row.displayContext && row.displayContext !== row.displayMessage">{{row.displayContext}}</span>
<span class="msg-detail" ng-if="row._detail">{{row._detail}}</span>
<div class="msg-url" ng-if="row._url">{{row._url}}</div>
<strong class="msg-code">{{ row?.displayMessage }}</strong>
<span class="msg-context" v-if="row?.displayContext &amp;&amp; row?.displayContext !== row?.displayMessage">{{ row?.displayContext }}</span>
<span class="msg-detail" v-if="row?._detail">{{ row?._detail }}</span>
<div class="msg-url" v-if="row?._url">{{ row?._url }}</div>
</div>
<div class="col-count">
<span class="count-pill" ng-if="row.count > 1">×{{row.count}}</span>
<span class="count-pill count-pill-muted" ng-if="row.count === 1">×1</span>
<span class="count-pill" v-if="row?.count &gt; 1">×{{ row?.count }}</span>
<span class="count-pill count-pill-muted" v-if="row?.count === 1">×1</span>
</div>
<div class="col-status">
<span class="status-pill" ng-if="row._status" ng-class="'status-' + row._bucket">{{row._status}}</span>
<span class="status-pill" v-if="row?._status" :class="&#x27;status-&#x27; + row?._bucket">{{ row?._status }}</span>
</div>
</div>
<div class="errors-row-detail" ng-if="expanded[row._key]">
<div class="errors-row-detail" v-if="expanded[row?._key]">
<div class="detail-tabs">
<button type="button" ng-class="{active: detailTab[row._key] === 'raw' || !detailTab[row._key]}" ng-click="detailTab[row._key] = 'raw'">Raw</button>
<button type="button" ng-class="{active: detailTab[row._key] === 'stack'}" ng-click="detailTab[row._key] = 'stack'" ng-if="row._stack">Stack</button>
<button type="button" ng-class="{active: detailTab[row._key] === 'related'}" ng-click="detailTab[row._key] = 'related'" ng-if="row.count > 1">Related ({{row.count}})</button>
<button type="button" :class="{active: detailTab[row?._key] === &#x27;raw&#x27; || !detailTab[row?._key]}" @click="detailTab[row._key] = &#x27;raw&#x27;">Raw</button>
<button type="button" v-if="row?._stack" :class="{active: detailTab[row?._key] === &#x27;stack&#x27;}" @click="detailTab[row._key] = &#x27;stack&#x27;">Stack</button>
<button type="button" v-if="row?.count &gt; 1" :class="{active: detailTab[row?._key] === &#x27;related&#x27;}" @click="detailTab[row._key] = &#x27;related&#x27;">Related ({{ row?.count }})</button>
</div>
<div class="detail-body">
<div class="detail-main">
<pre ng-if="(detailTab[row._key] || 'raw') === 'raw'">{{row._detailJson}}</pre>
<pre ng-if="detailTab[row._key] === 'stack'" class="stack-pre">{{row._stack}}</pre>
<div ng-if="detailTab[row._key] === 'related'" class="related-list">
<div class="related-row" ng-repeat="r in row._related track by $index">
<span class="when-abs">{{absTimeShort(r.ts)}}</span>
<span class="msg-url">{{r._url}}</span>
<span class="status-pill" ng-if="r._status" ng-class="'status-' + r._bucket">{{r._status}}</span>
<pre v-if="(detailTab[row?._key] || &#x27;raw&#x27;) === &#x27;raw&#x27;">{{ row?._detailJson }}</pre>
<pre class="stack-pre" v-if="detailTab[row?._key] === &#x27;stack&#x27;">{{ row?._stack }}</pre>
<div class="related-list" v-if="detailTab[row?._key] === &#x27;related&#x27;">
<div class="related-row" v-for="(r, index) in row?._related" :key="index">
<span class="when-abs">{{ absTimeShort(r?.ts) }}</span>
<span class="msg-url">{{ r?._url }}</span>
<span class="status-pill" v-if="r?._status" :class="&#x27;status-&#x27; + r?._bucket">{{ r?._status }}</span>
</div>
</div>
<div class="detail-actions">
<button class="btn btn-sm" type="button" ng-click="copyCurl(row)" title="Copy a curl that reproduces the request"><i class="fas fa-terminal"></i> Copy curl</button>
<button class="btn btn-sm" type="button" ng-click="copyJson(row)"><i class="fas fa-clipboard"></i> Copy JSON</button>
<span class="copy-hint" ng-if="copyHint">{{copyHint}}</span>
<button class="btn btn-sm" type="button" title="Copy a curl that reproduces the request" @click="copyCurl(row)"><i class="fas fa-terminal"></i> Copy curl</button>
<button class="btn btn-sm" type="button" @click="copyJson(row)"><i class="fas fa-clipboard"></i> Copy JSON</button>
<span class="copy-hint" v-if="copyHint">{{ copyHint }}</span>
</div>
</div>
<aside class="detail-aside">
<div class="aside-block">
<div class="aside-label">First seen</div>
<div class="aside-value" title="{{absTime(row._firstSeen)}}">{{relTime(row._firstSeen)}}</div>
<div class="aside-value" :title="(absTime(row?._firstSeen))">{{ relTime(row?._firstSeen) }}</div>
</div>
<div class="aside-block">
<div class="aside-label">Last seen</div>
<div class="aside-value" title="{{absTime(row.ts)}}">{{relTime(row.ts)}}</div>
<div class="aside-value" :title="(absTime(row?.ts))">{{ relTime(row?.ts) }}</div>
</div>
<div class="aside-block">
<div class="aside-label">Occurrences</div>
<div class="aside-value">{{row.count}}<span class="aside-sub" ng-if="row._lastHourCount"> · {{row._lastHourCount}} this hour</span></div>
<div class="aside-value">{{ row?.count }}<span class="aside-sub" v-if="row?._lastHourCount"> · {{ row?._lastHourCount }} this hour</span></div>
</div>
<div class="aside-block" ng-if="row._repoId">
<div class="aside-block" v-if="row?._repoId">
<div class="aside-label">Repository</div>
<a class="aside-value" ng-href="/r/{{row._repoId}}">{{row._repoId}}</a>
<a class="aside-value" :href="safeUrl(&quot;/r/&quot; + (row?._repoId))">{{ row?._repoId }}</a>
</div>
<div class="aside-block" ng-if="row._url">
<div class="aside-block" v-if="row?._url">
<div class="aside-label">URL</div>
<div class="aside-value mono">{{row._url}}</div>
<div class="aside-value mono">{{ row?._url }}</div>
</div>
</aside>
</div>
+88 -96
View File
@@ -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&hellip;</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&hellip;</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}} &middot; last 24h</div>
<div class="ov-kpi-value">{{ humanNum(data?.repos?.total) }}</div>
<div class="ov-kpi-sub">+{{ data?.repos?.newRepos24h }} &middot; 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 &middot; load {{data.system.loadAvg[0].toFixed(1)}}</div>
<div class="ov-kpi-value" :class="{&#x27;ov-val-warn&#x27;: data?.system?.cpuPercent &gt; 80}">{{ data?.system?.cpuPercent }}%</div>
<div class="ov-kpi-sub">{{ data?.system?.cpuCount }} cores &middot; 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="{&#x27;ov-val-warn&#x27;: data?.system?.memPercent &gt; 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)}} &middot; {{data.system.diskMount}}</div>
<div class="ov-kpi-value" :class="{&#x27;ov-val-warn&#x27;: data?.system?.diskPercent &gt; 85}">{{ data?.system?.diskPercent }}%</div>
<div class="ov-kpi-sub">{{ humanBytes(data?.system?.diskUsed) }} / {{ humanBytes(data?.system?.diskTotal) }} &middot; {{ 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}} &middot; {{data.system.platform}}</div>
<div class="ov-kpi-value">{{ humanDuration(data?.system?.uptime) }}</div>
<div class="ov-kpi-sub">{{ data?.system?.nodeVersion }} &middot; {{ 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 &middot; 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)) + &quot;: +&quot; + (fmt?.number(d?.dailyPageViews)) + &quot; views&quot;" :aria-label="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyPageViews)) + &quot; views&quot;">
<span class="ov-spark-fill" :style="{height: historyBarH(d, &#x27;dailyPageViews&#x27;) + &#x27;px&#x27;}"></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 &middot; 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)) + &quot;: +&quot; + (fmt?.number(d?.dailyRepositories)) + &quot; repos&quot;" :aria-label="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyRepositories)) + &quot; repos&quot;">
<span class="ov-spark-fill ov-spark-fill-alt" :style="{height: historyBarH(d, &#x27;dailyRepositories&#x27;) + &#x27;px&#x27;}"></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 &middot; 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)) + &quot;: &quot; + (fmt?.number(d?.nbUsers)) + &quot; users&quot;" :aria-label="(historyLabel(d)) + &quot;: &quot; + (fmt?.number(d?.nbUsers)) + &quot; users&quot;">
<span class="ov-spark-fill ov-spark-fill-user" :style="{height: historyBarH(d, &#x27;nbUsers&#x27;) + &#x27;px&#x27;}"></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 &middot; 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)) + &quot;: +&quot; + (fmt?.number(d?.dailyUsers)) + &quot; users&quot;" :aria-label="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyUsers)) + &quot; users&quot;">
<span class="ov-spark-fill ov-spark-fill-new-user" :style="{height: historyBarH(d, &#x27;dailyUsers&#x27;) + &#x27;px&#x27;}"></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(&#x27;ready&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-preparing" title="Preparing" :style="{width: barPct(&#x27;preparing&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-error" title="Error" :style="{width: barPct(&#x27;error&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-expired" title="Expired" :style="{width: barPct(&#x27;expired&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-removed" title="Removed" :style="{width: barPct(&#x27;removed&#x27;) + &#x27;%&#x27;}"></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 &middot; 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(&#x27;error&#x27;) + &#x27;%&#x27;}"></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(&#x27;warn&#x27;) + &#x27;%&#x27;}"></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(&#x27;info&#x27;) + &#x27;%&#x27;}"></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 &rarr;</a>
<div class="ov-panel-foot" v-if="data?.repos?.recentErrors24h">
<a href="/admin/errors">{{ data?.repos?.recentErrors24h }} repos in error state &rarr;</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="{&#x27;ov-n-bad&#x27;: data?.queues?.download?.failed &gt; 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="{&#x27;ov-n-bad&#x27;: data?.queues?.remove?.failed &gt; 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="{&#x27;ov-n-bad&#x27;: data?.queues?.cache?.failed &gt; 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 &middot; {{data.conferences.total}} conferences</span>
<span class="ov-panel-meta">{{ fmt?.number(data?.users?.total) }} users &middot; {{ 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) &gt; 0 ? &#x27;ov-dot-ok&#x27; : &#x27;ov-dot-idle&#x27;"></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) &gt; 0 ? &#x27;ov-dot-ok&#x27; : &#x27;ov-dot-idle&#x27;"></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) &gt; 0 ? &#x27;ov-dot-ok&#x27; : &#x27;ov-dot-idle&#x27;"></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 &gt; 10 ? &#x27;ov-dot-warn&#x27; : &#x27;ov-dot-ok&#x27;"></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>
+79 -79
View File
@@ -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 == &#x27;1h&#x27;}" @click="setRange(&#x27;1h&#x27;)">1h</button>
<button class="btn btn-sm" :class="{active: range == &#x27;6h&#x27;}" @click="setRange(&#x27;6h&#x27;)">6h</button>
<button class="btn btn-sm" :class="{active: range == &#x27;24h&#x27;}" @click="setRange(&#x27;24h&#x27;)">24h</button>
<button class="btn btn-sm" :class="{active: range == &#x27;7d&#x27;}" @click="setRange(&#x27;7d&#x27;)">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="{&#x27;q-dot-red&#x27;: q?.paused || q?.counts?.failed &gt; 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 &middot; {{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 &middot; {{ 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) + &#x27;%&#x27;}"></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}}&middot;throughput <span class="q-section-right"><span class="q-legend-completed">&#9679;</span> completed <span class="q-legend-failed">&#9679;</span> failed <span class="q-legend-exec">- -</span> avg time &middot; {{range | uppercase}}</span></div>
<div class="q-section-label">{{ selectedQueue }}&middot;throughput <span class="q-section-right"><span class="q-legend-completed">&#9679;</span> completed <span class="q-legend-failed">&#9679;</span> failed <span class="q-legend-exec">- -</span> avg time &middot; {{ 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}}&middot;stats</div>
<div class="q-section-label">{{ selectedQueue }}&middot;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 &middot; {{selectedQueue | uppercase}}</div>
<div class="q-section-label">ALL JOBS &middot; {{ 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 =&gt; { stateFilter[s] = value }, value: stateFilter[s], form: null, options: {} }">
<span :class="&quot;q-state-chip q-state-&quot; + (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 =&gt; { 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 =&gt; { 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 &gt; 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="{&#x27;q-row-failed&#x27;: job?._state == &#x27;failed&#x27;, &#x27;q-row-expanded&#x27;: 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="&quot;q-state-badge q-state-&quot; + (job?._state)"></span>
<span class="q-delay-hint" v-if="job?._state == &#x27;delayed&#x27; &amp;&amp; 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="{&#x27;q-chevron-open&#x27;: expanded[job?.id]}"></i>
<a target="_blank" v-text="&#x27;job:&#x27; + fmt.limitTo(job?.id, 6)" @click="$event.stopPropagation()" :href="safeUrl(&quot;/r/&quot; + (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"> &middot; {{job.data.repoId}}</span>
<span v-text="job?.name || &#x27;anonymize&#x27;"></span>
<span class="q-payload-detail" v-if="job?.data?.repoId &amp;&amp; job?.data?.repoId !== job?.name"> &middot; {{ 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="{&#x27;--pct&#x27;: jobProgressPct(job) + &#x27;%&#x27;}"></div>
<span class="q-progress-label" v-text="jobProgressPct(job) + &#x27;%&#x27;"></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 == &#x27;failed&#x27;" @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(&quot;/r/&quot; + (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="&quot;q-state-badge q-state-&quot; + (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 == &#x27;delayed&#x27; &amp;&amp; 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 &amp;&amp; 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) + &#x27;%&#x27;"></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 == &#x27;failed&#x27;" @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(&quot;/r/&quot; + (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>
+59 -71
View File
@@ -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 =&gt; { 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 =&gt; { 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 =&gt; { 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 &lt;= 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 &gt;= 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() &gt; 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 == &#x27;source.repositoryName&#x27;}" @click="sortBy(&#x27;source.repositoryName&#x27;)">Repository <i class="fas" :class="sortIcon(&#x27;source.repositoryName&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;status&#x27;}" @click="sortBy(&#x27;status&#x27;)">Status <i class="fas" :class="sortIcon(&#x27;status&#x27;)"></i></span></div>
<div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == &#x27;pageView&#x27;}" @click="sortBy(&#x27;pageView&#x27;)">Views <i class="fas" :class="sortIcon(&#x27;pageView&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;anonymizeDate&#x27;}" @click="sortBy(&#x27;anonymizeDate&#x27;)">Anonymized <i class="fas" :class="sortIcon(&#x27;anonymizeDate&#x27;)"></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="{&#x27;repo-inactive&#x27;: repo?.status == &#x27;expired&#x27; || repo?.status == &#x27;removed&#x27;, &#x27;repo-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;row-selected&#x27;: 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 =&gt; { 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(&quot;/r/&quot; + (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">&nbsp;&middot;&nbsp;<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">&nbsp;&middot;&nbsp;@<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">&nbsp;&middot;&nbsp;<i class="fas fa-chalkboard-teacher"></i> {{repo.conference}}</span><span>&nbsp;&middot;&nbsp;{{::repo.size.storage | humanFileSize}}</span><span>&nbsp;&middot;&nbsp;{{::repo.options.terms.length | number}} terms</span>
<a v-text="repo?.source?.repositoryName" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.repositoryName) + &quot;/&quot;)"></a><span v-if="repo?.options?.update">&nbsp;&middot;&nbsp;<a v-text="repo?.source?.branch" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update">&nbsp;&middot;&nbsp;@<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.commit))"></a></span><span v-if="repo?.conference">&nbsp;&middot;&nbsp;<i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span>&nbsp;&middot;&nbsp;{{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span>&nbsp;&middot;&nbsp;{{ 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="{&#x27;status-removed&#x27;: repo?.status == &#x27;removed&#x27; || repo?.status == &#x27;expired&#x27;, &#x27;status-ready&#x27;: repo?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;status-preparing&#x27;: repo?.status == &#x27;preparing&#x27;}"></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(&quot;/anonymize/&quot; + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
<a class="dropdown-item" :href="safeUrl(&quot;/r/&quot; + (repo?.repoId) + &quot;/&quot;)"><i class="fa fa-eye"></i> View repo</a>
<a class="dropdown-item" target="_self" v-if="repo?.options?.page &amp;&amp; repo?.status == &#x27;ready&#x27;" :href="safeUrl(&quot;/w/&quot; + (repo?.repoId) + &quot;/&quot;)"><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 == &#x27;ready&#x27; || repo?.status == &#x27;error&#x27;" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
<a class="dropdown-item" href="#" v-show="repo?.status == &#x27;removed&#x27;" @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 == &#x27;ready&#x27;" @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 &gt; 1">
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page &lt;= 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 =&gt; { 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 &gt;= 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 =&gt; { query.limit = value }, value: query?.limit, form: null, options: {} }">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
+82 -94
View File
@@ -1,6 +1,6 @@
<div class="container paper-page admin-page">
<div class="paper-crumbs"><a href="/admin/users">Users</a> &nbsp;/&nbsp; <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> &nbsp;/&nbsp; <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="{&#x27;status-ready&#x27;: userInfo?.status == &#x27;active&#x27;, &#x27;status-removed&#x27;: userInfo?.status != &#x27;active&#x27;}"></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 !== &#x27;banned&#x27;" @click="banUser()"><i class="fas fa-ban"></i> Ban</button>
<button class="btn btn-sm" v-if="userInfo?.status === &#x27;banned&#x27; || userInfo?.status === &#x27;removed&#x27;" @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(&quot;https://github.com/&quot; + (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 &amp;&amp; userInfo?.isAdmin &amp;&amp; user &amp;&amp; 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 &amp;&amp; userInfo?.isAdmin &amp;&amp; user &amp;&amp; user?.username == userInfo?.username">
<p class="paper-page-lede">Personal API tokens for this admin account. Send as <code>Authorization: Bearer &lt;token&gt;</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, () =&gt; { createToken() })">
<input type="text" class="form-control" placeholder="Token name (e.g. dev-laptop)" required v-field="{ state: viewState, set: value =&gt; { 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 =&gt; { 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() &gt; 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 == &#x27;source.repositoryName&#x27;}" @click="sortBy(&#x27;source.repositoryName&#x27;)">Repository <i class="fas" :class="sortIcon(&#x27;source.repositoryName&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;status&#x27;}" @click="sortBy(&#x27;status&#x27;)">Status <i class="fas" :class="sortIcon(&#x27;status&#x27;)"></i></span></div>
<div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == &#x27;pageView&#x27;}" @click="sortBy(&#x27;pageView&#x27;)">Views <i class="fas" :class="sortIcon(&#x27;pageView&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;anonymizeDate&#x27;}" @click="sortBy(&#x27;anonymizeDate&#x27;)">Anonymized <i class="fas" :class="sortIcon(&#x27;anonymizeDate&#x27;)"></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="{&#x27;repo-inactive&#x27;: repo?.status == &#x27;expired&#x27; || repo?.status == &#x27;removed&#x27;, &#x27;repo-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;row-selected&#x27;: 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 =&gt; { 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(&quot;/r/&quot; + (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">&nbsp;&middot;&nbsp;<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">&nbsp;&middot;&nbsp;@<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">&nbsp;&middot;&nbsp;<i class="fas fa-chalkboard-teacher"></i> {{repo.conference}}</span><span>&nbsp;&middot;&nbsp;{{::repo.size.storage | humanFileSize}}</span><span>&nbsp;&middot;&nbsp;{{::repo.options.terms.length | number}} terms</span>
<a v-text="repo?.source?.fullName" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/&quot;)"></a><span v-if="repo?.options?.update">&nbsp;&middot;&nbsp;<a v-text="repo?.source?.branch" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update">&nbsp;&middot;&nbsp;@<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.commit))"></a></span><span v-if="repo?.conference">&nbsp;&middot;&nbsp;<i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span>&nbsp;&middot;&nbsp;{{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span>&nbsp;&middot;&nbsp;{{ 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="{&#x27;status-removed&#x27;: repo?.status == &#x27;removed&#x27; || repo?.status == &#x27;expired&#x27;, &#x27;status-ready&#x27;: repo?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;status-preparing&#x27;: repo?.status == &#x27;preparing&#x27;}"></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(&quot;/anonymize/&quot; + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
<a class="dropdown-item" :href="safeUrl(&quot;/r/&quot; + (repo?.repoId) + &quot;/&quot;)"><i class="fa fa-eye"></i> View repo</a>
<a class="dropdown-item" target="_self" v-if="repo?.options?.page &amp;&amp; repo?.status == &#x27;ready&#x27;" :href="safeUrl(&quot;/w/&quot; + (repo?.repoId) + &quot;/&quot;)"><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 == &#x27;ready&#x27; || repo?.status == &#x27;error&#x27;" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
<a class="dropdown-item" href="#" v-show="repo?.status == &#x27;removed&#x27;" @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 == &#x27;ready&#x27;" @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>
+48 -53
View File
@@ -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 == &#x27;active&#x27;}" @click="query.status = query.status == &#x27;active&#x27; ? &#x27;&#x27; : &#x27;active&#x27;; query.page = 1">Active <span class="count">{{ fmt?.number(statusCountFor('active')) }}</span></span>
<span class="summary-pill error" :class="{active: query?.status == &#x27;banned&#x27;}" @click="query.status = query.status == &#x27;banned&#x27; ? &#x27;&#x27; : &#x27;banned&#x27;; query.page = 1">Banned <span class="count">{{ fmt?.number(statusCountFor('banned')) }}</span></span>
<span class="summary-pill" :class="{active: query?.status == &#x27;removed&#x27;}" @click="query.status = query.status == &#x27;removed&#x27; ? &#x27;&#x27; : &#x27;removed&#x27;; 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 =&gt; { 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 =&gt; { 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 &lt;= 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 &gt;= 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() &gt; 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 == &#x27;username&#x27;}" @click="sortBy(&#x27;username&#x27;)">User <i class="fas" :class="sortIcon(&#x27;username&#x27;)"></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 == &#x27;status&#x27;}" @click="sortBy(&#x27;status&#x27;)">Status <i class="fas" :class="sortIcon(&#x27;status&#x27;)"></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="{&#x27;row-selected&#x27;: 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 =&gt; { 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(&quot;/admin/users/&quot; + (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">&nbsp;&middot;&nbsp;</span><a href="https://github.com/{{u.username}}" target="_blank"><i class="fab fa-github"></i> {{u.username}}</a><span ng-if="u.dateOfEntry">&nbsp;&middot;&nbsp;Joined {{u.dateOfEntry | humanTime}}</span>
<span v-if="u?.emails[0].email">{{ u?.emails[0].email }}</span><span v-if="u?.emails[0].email">&nbsp;&middot;&nbsp;</span><a target="_blank" :href="safeUrl(&quot;https://github.com/&quot; + (u?.username))"><i class="fab fa-github"></i> {{ u?.username }}</a><span v-if="u?.dateOfEntry">&nbsp;&middot;&nbsp;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&#x27;s repositories" v-text="fmt?.number((u?.repoCount || 0))" :href="safeUrl(&quot;/admin/?owner=&quot; + (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="{&#x27;status-ready&#x27;: u?.status == &#x27;active&#x27;, &#x27;status-removed&#x27;: u?.status == &#x27;removed&#x27; || u?.status == &#x27;banned&#x27;}"></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">&mdash;</span>
<span class="type-badge type-repo" v-if="u?.isAdmin">Admin</span>
<span class="empty-dash" v-if="!u?.isAdmin">&mdash;</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(&quot;/admin/users/&quot; + (u?.username))"><i class="far fa-eye"></i> View details</a>
<a class="dropdown-item" :href="safeUrl(&quot;/admin/?owner=&quot; + (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 == &#x27;active&#x27;" @click.prevent="banUser(u)"><i class="fas fa-ban"></i> Ban</a>
<a class="dropdown-item" href="#" v-show="u?.status == &#x27;removed&#x27; || u?.status == &#x27;banned&#x27;" @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 &gt; 1">
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page &lt;= 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 =&gt; { 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 &gt;= 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 =&gt; { 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
View File
@@ -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 &nbsp;/&nbsp; <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 =&gt; { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { 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="{&#x27;type-repo&#x27;: detectedType === &#x27;repo&#x27;, &#x27;type-pr&#x27;: detectedType === &#x27;pr&#x27;, &#x27;type-gist&#x27;: detectedType === &#x27;gist&#x27;}">
{{ 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 &amp;&amp; detectedType !== &#x27;repo&#x27;" placeholder="Paste a GitHub repo or pull request URL" v-field="{ state: viewState, set: value =&gt; { sourceUrl = value }, value: sourceUrl, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { urlSelected() } }" :class="{&#x27;is-invalid&#x27;: 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 === &#x27;repo&#x27;">
<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 =&gt; { source.branch = value }, value: source?.branch, form: &quot;anonymize&quot;, 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 =&gt; { source.commit = value }, value: source?.commit, form: &quot;anonymize&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: 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 =&gt; { options.update = value }, value: options?.update, form: &quot;anonymize&quot;, 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 === &#x27;repo&#x27;">
<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 =&gt; { repoId = value }, value: repoId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: 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 === &#x27;pr&#x27;">
<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 =&gt; { pullRequestId = value }, value: pullRequestId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: 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 === &#x27;gist&#x27;">
<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 =&gt; { gistId = value }, value: gistId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: 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 =&gt; { conference = value }, value: conference, form: &quot;anonymize&quot;, options: { debounce: { default: 800, blur: 0 }, updateOn: &#x27;default blur&#x27; } }" :class="{&#x27;is-invalid&#x27;: 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=&gt;replacement</code> to pick your own (e.g. <code>Anonymous=&gt;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 =&gt; { terms = value }, value: terms, form: &quot;anonymize&quot;, options: { debounce: 250 } }" :class="{&#x27;is-invalid&#x27;: 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=&gt;replacement</code> to pick your own (e.g. <code>Anonymous=&gt;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 === &#x27;repo&#x27;">
<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 =&gt; { options.link = value }, value: options?.link, form: &quot;anonymize&quot;, 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 =&gt; { options.image = value }, value: options?.image, form: &quot;anonymize&quot;, 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 =&gt; { options.pdf = value }, value: options?.pdf, form: &quot;anonymize&quot;, 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 =&gt; { options.notebook = value }, value: options?.notebook, form: &quot;anonymize&quot;, 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 =&gt; { options.page = value }, value: options?.page, form: &quot;anonymize&quot;, options: {} }" :disabled="!details?.hasPage || (details?.pageSource &amp;&amp; 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 &amp;&amp; details?.pageSource &amp;&amp; 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 === &#x27;gist&#x27;">
<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 =&gt; { options.title = value }, value: options?.title, form: &quot;anonymize&quot;, 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 =&gt; { options.content = value }, value: options?.content, form: &quot;anonymize&quot;, 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 =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymize&quot;, 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 =&gt; { options.username = value }, value: options?.username, form: &quot;anonymize&quot;, 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 =&gt; { options.date = value }, value: options?.date, form: &quot;anonymize&quot;, 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 =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="origin-gist">Source gist ID</label>
</div>
</div>
<div ng-show="detectedType === 'pr'">
<div v-show="detectedType === &#x27;pr&#x27;">
<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 =&gt; { options.title = value }, value: options?.title, form: &quot;anonymize&quot;, 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 =&gt; { options.body = value }, value: options?.body, form: &quot;anonymize&quot;, 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 =&gt; { options.diff = value }, value: options?.diff, form: &quot;anonymize&quot;, 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 =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymize&quot;, 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 =&gt; { options.username = value }, value: options?.username, form: &quot;anonymize&quot;, 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 =&gt; { options.date = value }, value: options?.date, form: &quot;anonymize&quot;, 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 =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymize&quot;, 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 =&gt; { options.expirationMode = value }, value: options?.expirationMode, form: &quot;anonymize&quot;, 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 =&gt; { options.expirationDate = value }, value: options?.expirationDate, form: &quot;anonymize&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: 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==&#x27;remove&#x27; &amp;&amp; options?.expirationDate">After {{ fmt?.date(options?.expirationDate) }}, the content will be removed.</small>
<small class="form-text text-muted" v-show="options?.expirationMode==&#x27;redirect&#x27; &amp;&amp; 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 &amp;&amp; detectedType === &#x27;repo&#x27;">
<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 === &#x27;owner&#x27; || role === &#x27;admin&#x27;">
<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 =&gt; { coauthorSearch = value }, value: coauthorSearch, form: &quot;anonymize&quot;, options: { debounce: 300 }, change: () =&gt; { searchCoauthors() } }">
<div class="dropdown-menu show" style="display: block; max-height: 220px; overflow-y: auto; width: 100%;" v-show="coauthorResults?.length &gt; 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(&quot;https://github.com/&quot; + (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 === &#x27;owner&#x27; || role === &#x27;admin&#x27;" @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 === &#x27;repo&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { 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 === &#x27;repo&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { 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 === &#x27;pr&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { 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 === &#x27;pr&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { 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 === &#x27;gist&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { 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 === &#x27;gist&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { 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 === &#x27;repo&#x27; &amp;&amp; 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 === &#x27;gist&#x27; &amp;&amp; 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="{&#x27;badge-success&#x27;: details?.gist?.isPublic, &#x27;badge-secondary&#x27;: !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 &amp;&amp; details?.gist?.ownerLogin">By @{{ anonymizeGistContent(details?.gist?.ownerLogin) }}</small>
<div v-if="options?.content &amp;&amp; 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 &amp;&amp; details?.gist?.comments &amp;&amp; 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 === &#x27;pr&#x27; &amp;&amp; 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="{&#x27;badge-success&#x27;:details?.pullRequest?.merged, &#x27;badge-warning&#x27;:details?.pullRequest?.state==&#x27;open&#x27;, &#x27;badge-danger&#x27;:details?.pullRequest?.state==&#x27;closed&#x27; &amp;&amp; !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="{&#x27;active&#x27;: prTabState?.active == &#x27;diff&#x27;}" @click="prTabState.active = &#x27;diff&#x27;">
<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="{&#x27;active&#x27;: prTabState?.active == &#x27;comments&#x27;}" @click="prTabState.active = &#x27;comments&#x27;">
<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, {&#x27;0&#x27;: &#x27;No comments&#x27;, &#x27;one&#x27;: &#x27;1 comment&#x27;, &#x27;other&#x27;: &#x27;{} comments&#x27;})"></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 &amp;&amp; prTabState?.active == &#x27;diff&#x27;">
<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 &amp;&amp; prTabState?.active == &#x27;comments&#x27;">
<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>
+94 -380
View File
@@ -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="{&#x27;card&#x27;: !pullRequestUrl,&#x27;container&#x27;: pullRequestUrl}">
<form class="form needs-validation" name="anonymizeForm" novalidate v-form="viewState" :class="{&#x27;card-body&#x27;: !pullRequestUrl}">
<div class="paper-crumbs">Anonymize &nbsp;/&nbsp; <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 =&gt; { pullRequestUrl = value }, value: pullRequestUrl, form: &quot;anonymizeForm&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { pullRequestSelected() } }" :class="{&#x27;is-invalid&#x27;: 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 =&gt; { options.update = value }, value: options?.update, form: &quot;anonymizeForm&quot;, 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 =&gt; { conference = value }, value: conference, form: &quot;anonymizeForm&quot;, options: { debounce: { default: 800, blur: 0 }, updateOn: &#x27;default blur&#x27; } }" :class="{&#x27;is-invalid&#x27;: 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 =&gt; { pullRequestId = value }, value: pullRequestId, form: &quot;anonymizeForm&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: 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 =&gt; { terms = value }, value: terms, form: &quot;anonymizeForm&quot;, options: { debounce: 250 } }" :class="{&#x27;is-invalid&#x27;: 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 =&gt; { options.expirationMode = value }, value: options?.expirationMode, form: &quot;anonymizeForm&quot;, 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==&#x27;never&#x27;)">
<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 =&gt; { options.expirationDate = value }, value: options?.expirationDate, form: &quot;anonymizeForm&quot;, options: {} }">
<small class="form-text text-muted" v-show="options?.expirationMode==&#x27;remove&#x27;">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==&#x27;redirect&#x27;">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 =&gt; { options.link = value }, value: options?.link, form: &quot;anonymizeForm&quot;, 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 =&gt; { options.image = value }, value: options?.image, form: &quot;anonymizeForm&quot;, 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 =&gt; { options.date = value }, value: options?.date, form: &quot;anonymizeForm&quot;, 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 =&gt; { options.username = value }, value: options?.username, form: &quot;anonymizeForm&quot;, 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 =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymizeForm&quot;, 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 =&gt; { options.diff = value }, value: options?.diff, form: &quot;anonymizeForm&quot;, 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 =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymizeForm&quot;, 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 =&gt; { options.title = value }, value: options?.title, form: &quot;anonymizeForm&quot;, 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 =&gt; { options.body = value }, value: options?.body, form: &quot;anonymizeForm&quot;, 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, () =&gt; { 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, () =&gt; { 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="{&#x27;badge-success&#x27;:details?.pullRequest?.merged, &#x27;badge-warning&#x27;:details?.pullRequest?.state==&#x27;open&#x27;, &#x27;badge-danger&#x27;:details?.pullRequest?.state==&#x27;closed&#x27; &amp;&amp;!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="{&#x27;active&#x27;:!options?.diff}">
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {&#x27;0&#x27;: &#x27;No comment&#x27;,
&#x27;one&#x27;: &#x27;One Comment&#x27;,
&#x27;other&#x27;: &#x27;{} Comments&#x27;})">
</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="{&#x27;show active&#x27;:!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>
+6 -28
View File
@@ -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, () =&gt; { 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 =&gt; { repoUrl = value }, value: repoUrl, form: &quot;claimForm&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: claimForm?.repoUrl?.invalid &amp;&amp; (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/&lt;ID&gt;</code>.</small
>
<input type="text" class="form-control" name="repoId" id="repoId" required v-field="{ state: viewState, set: value =&gt; { repoId = value }, value: repoId, form: &quot;claimForm&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: claimForm?.repoId?.invalid &amp;&amp; (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/&lt;ID&gt;</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>
+53 -87
View File
@@ -1,99 +1,77 @@
<div class="container page paper-page">
<div>
<div class="paper-crumbs">
<a href="/dashboard">My work</a> &nbsp;/&nbsp; <a href="/conferences">Conferences</a> &nbsp;/&nbsp; <span class="here">{{conference.conferenceID}}</span>
<a href="/dashboard">My work</a> &nbsp;/&nbsp; <a href="/conferences">Conferences</a> &nbsp;/&nbsp; <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="{&#x27;status-pill-ready&#x27;: conference?.status == &#x27;ready&#x27;, &#x27;status-pill-removed&#x27;: conference?.status == &#x27;removed&#x27; || conference?.status == &#x27;expired&#x27;}">
<span class="status-dot" aria-hidden="true" :class="&#x27;status-&#x27; + 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(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/edit&quot;)"><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'}} &ndash; {{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}} &euro;</b></span>
<span>ID <b class="commit-hash" v-text="conference?.conferenceID"></b></span>
<span>Review window <b>{{ fmt?.date(conference?.startDate, 'mediumDate') }} &ndash; {{ 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 || &#x27;Free&#x27;"></b></span>
<span v-if="conference?.price">Cost so far <b>{{ fmt?.number(conference?.price, 2) }} &euro;</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, () =&gt; { $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 =&gt; { 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 =&gt; { 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 =&gt; { 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 =&gt; { 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 =&gt; { 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 &amp;&amp; filters?.status?.expired &amp;&amp; filters?.status?.removed">All</span>
<span class="filter-btn-value" v-if="!(filters?.status?.ready &amp;&amp; filters?.status?.expired &amp;&amp; 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 =&gt; { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id="&quot;status-&quot; + (key)">
<label class="form-check-label" :for="&quot;status-&quot; + (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;">&times;</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="&quot;Show &quot; + (statusLabels[f]) + &quot; again&quot;">&times;</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="{&#x27;repo-inactive&#x27;: repo?.status == &#x27;expired&#x27; || repo?.status == &#x27;removed&#x27;, &#x27;repo-error&#x27;: repo?.status == &#x27;error&#x27;}">
<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(&quot;/r/&quot; + (repo?.repoId) + &quot;/&quot;)"></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"> &middot; <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"> &middot; <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(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/&quot;)"></a><span v-if="repo?.options?.update &amp;&amp; repo?.source?.branch"> &middot; <a target="_blank" rel="noopener" v-text="repo?.source?.branch" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.branch))"></a></span><span v-if="repo?.source?.commit"> &middot; <a class="commit-hash" target="_blank" rel="noopener" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (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="&#x27;status-&#x27; + (repo?.status == &#x27;error&#x27; ? &#x27;error&#x27; : repo?.status == &#x27;ready&#x27; ? &#x27;ready&#x27; : (repo?.status == &#x27;expired&#x27; || repo?.status == &#x27;removed&#x27;) ? repo?.status : &#x27;progress&#x27;)"></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">&mdash;</span>
<span class="expires-never" v-if="repo?.status == &#x27;ready&#x27; &amp;&amp; (repo?.options?.expirationMode === &#x27;never&#x27; || !repo?.options?.expirationDate)">Never</span>
<span v-if="repo?.status == &#x27;ready&#x27; &amp;&amp; repo?.options?.expirationMode !== &#x27;never&#x27; &amp;&amp; repo?.options?.expirationDate" v-text="fmt?.humanTime(repo?.options?.expirationDate)"></span>
<span class="expires-past" v-if="repo?.status == &#x27;expired&#x27;">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 != &#x27;ready&#x27; &amp;&amp; repo?.status != &#x27;expired&#x27;">&mdash;</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="&quot;Actions for &quot; + (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(&quot;/r/&quot; + (repo?.repoId) + &quot;/&quot;)">
<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(&quot;/anonymize/&quot; + (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 = &#x27;&#x27;; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
</div>
</div>
</div>
+38 -72
View File
@@ -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, () =&gt; { $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 =&gt; { 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 =&gt; { 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 =&gt; { 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 =&gt; { 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 &amp;&amp; filters?.status?.expired &amp;&amp; filters?.status?.removed">All</span>
<span class="filter-btn-value" v-if="!(filters?.status?.ready &amp;&amp; filters?.status?.expired &amp;&amp; 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 =&gt; { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id="&quot;status-&quot; + (key)">
<label class="form-check-label" :for="&quot;status-&quot; + (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;">&times;</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="&quot;Show &quot; + (statusLabels[f]) + &quot; again&quot;">&times;</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="{&#x27;repo-inactive&#x27;: conference?.status == &#x27;expired&#x27; || conference?.status == &#x27;removed&#x27;}">
<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(&quot;/conference/&quot; + (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"> &middot; <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"> &middot; <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="&#x27;status-&#x27; + 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'}} &ndash; {{conference.endDate | date:'mediumDate'}}</span>
<span class="empty-dash" ng-if="!conference.startDate" aria-label="No review window">&mdash;</span>
<span v-if="conference?.startDate">{{ fmt?.date(conference?.startDate, 'mediumDate') }} &ndash; {{ fmt?.date(conference?.endDate, 'mediumDate') }}</span>
<span class="empty-dash" aria-label="No review window" v-if="!conference?.startDate">&mdash;</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="&quot;Actions for &quot; + (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(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/&quot;)">
<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(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/edit&quot;)">
<i class="far fa-edit" aria-hidden="true"></i> Edit
</a>
<div ng-if="conference.status != 'removed'">
<div v-if="conference?.status != &#x27;removed&#x27;">
<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 &gt; 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 &gt; 0" @click="search = &#x27;&#x27;; 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
View File
@@ -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: &#x27;repository&#x27;, label: &#x27;Repositories&#x27;, kind: &#x27;count&#x27;},
{key: &#x27;storage&#x27;, label: &#x27;Storage&#x27;, kind: &#x27;bytes&#x27;},
{key: &#x27;file&#x27;, label: &#x27;Files&#x27;, kind: &#x27;count&#x27;}
]">
<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 === &#x27;count&#x27;">
{{ 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 === &#x27;bytes&#x27;">
{{ 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="&#x27;quota-&#x27; + quota[q?.key].level" :aria-label="(q?.label) + &quot; quota&quot;" :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 ? &#x27;unlimited&#x27; : fmt.number(quota[q?.key].percent, 0) + &#x27;% used&#x27;)">
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + &#x27;%&#x27;}"></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, () =&gt; { $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 =&gt; { 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="{&#x27;btn-primary&#x27;: typeFilter === &#x27;all&#x27;}" @click="typeFilter = &#x27;all&#x27;" :aria-pressed="(typeFilter === &#x27;all&#x27;)">All</button>
<button type="button" class="btn" :class="{&#x27;btn-primary&#x27;: typeFilter === &#x27;repo&#x27;}" @click="typeFilter = &#x27;repo&#x27;" :aria-pressed="(typeFilter === &#x27;repo&#x27;)">Repos</button>
<button type="button" class="btn" :class="{&#x27;btn-primary&#x27;: typeFilter === &#x27;pr&#x27;}" @click="typeFilter = &#x27;pr&#x27;" :aria-pressed="(typeFilter === &#x27;pr&#x27;)">PRs</button>
<button type="button" class="btn" :class="{&#x27;btn-primary&#x27;: typeFilter === &#x27;gist&#x27;}" @click="typeFilter = &#x27;gist&#x27;" :aria-pressed="(typeFilter === &#x27;gist&#x27;)">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() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></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="&quot;sort-&quot; + (index)">
<label class="form-check-label" :for="&quot;sort-&quot; + (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 =&gt; { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id="&quot;status-&quot; + (key)">
<label class="form-check-label" :for="&quot;status-&quot; + (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;">&times;</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="&quot;Show &quot; + (statusKeyLabels[f]) + &quot; again&quot;">&times;</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(&#x27;_name&#x27;) ? (sortDesc() ? &#x27;descending&#x27; : &#x27;ascending&#x27;) : &#x27;none&#x27;)">
<button type="button" class="sortable" :class="{active: isSortedBy(&#x27;_name&#x27;)}" @click="setSort(&#x27;_name&#x27;)">
Anonymization <i class="fas" aria-hidden="true" v-if="isSortedBy(&#x27;_name&#x27;)" :class="sortDesc() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></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(&#x27;status&#x27;) ? (sortDesc() ? &#x27;descending&#x27; : &#x27;ascending&#x27;) : &#x27;none&#x27;)">
<button type="button" class="sortable" :class="{active: isSortedBy(&#x27;status&#x27;)}" @click="setSort(&#x27;status&#x27;)">
Status <i class="fas" aria-hidden="true" v-if="isSortedBy(&#x27;status&#x27;)" :class="sortDesc() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></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(&#x27;pageView&#x27;) ? (sortDesc() ? &#x27;descending&#x27; : &#x27;ascending&#x27;) : &#x27;none&#x27;)">
<button type="button" class="sortable" :class="{active: isSortedBy(&#x27;pageView&#x27;)}" @click="setSort(&#x27;pageView&#x27;)">
Views <i class="fas" aria-hidden="true" v-if="isSortedBy(&#x27;pageView&#x27;)" :class="sortDesc() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></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(&#x27;options.expirationDate&#x27;) ? (sortDesc() ? &#x27;descending&#x27; : &#x27;ascending&#x27;) : &#x27;none&#x27;)">
<button type="button" class="sortable" :class="{active: isSortedBy(&#x27;options.expirationDate&#x27;)}" @click="setSort(&#x27;options.expirationDate&#x27;)">
Expires <i class="fas" aria-hidden="true" v-if="isSortedBy(&#x27;options.expirationDate&#x27;)" :class="sortDesc() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></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 + &#x27;:&#x27; + (item?._id || index))"><div class="paper-table-row" role="row" v-if="!loading" :class="{&#x27;repo-inactive&#x27;: item?._statusKey == &#x27;expired&#x27; || item?._statusKey == &#x27;removed&#x27;, &#x27;repo-error&#x27;: item?.status == &#x27;error&#x27;, &#x27;row-clickable&#x27;: !!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="{&#x27;type-repo&#x27;: item?._type === &#x27;repo&#x27;, &#x27;type-pr&#x27;: item?._type === &#x27;pr&#x27;, &#x27;type-gist&#x27;: item?._type === &#x27;gist&#x27;}">{{ 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 === &#x27;coauthor&#x27;">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"> &middot; <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"> &middot; <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 === &#x27;repo&#x27; &amp;&amp; item?.source?.fullName &amp;&amp; !item?._broken">
<a target="_blank" rel="noopener" v-text="item?.source?.fullName" :href="safeUrl(&quot;https://github.com/&quot; + (item?.source?.fullName) + &quot;/&quot;)"></a><span v-if="item?.options?.update &amp;&amp; item?.source?.branch"> &middot; <a target="_blank" rel="noopener" v-text="item?.source?.branch" :href="safeUrl(&quot;https://github.com/&quot; + (item?.source?.fullName) + &quot;/tree/&quot; + (item?.source?.branch))"></a></span><span v-if="item?.source?.commit"> &middot; <a class="commit-hash" target="_blank" rel="noopener" :href="safeUrl(&quot;https://github.com/&quot; + (item?.source?.fullName) + &quot;/tree/&quot; + (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 === &#x27;pr&#x27; &amp;&amp; item?.source?.repositoryFullName &amp;&amp; !item?._broken">
<a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl(&quot;https://github.com/&quot; + (item?.source?.repositoryFullName) + &quot;/pull/&quot; + (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 === &#x27;gist&#x27; &amp;&amp; item?.source?.gistId &amp;&amp; !item?._broken">
<a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl(&quot;https://gist.github.com/&quot; + (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="&#x27;status-&#x27; + 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}} &middot; may be stuck
<div class="status-sub status-sub-error" v-if="item?.status == &#x27;error&#x27; &amp;&amp; item?.statusMessage" v-text="fmt?.statusMsg(item?.statusMessage)" :title="(item?.statusMessage)"></div>
<div class="status-sub status-sub-warn" v-if="item?._stale" :title="&quot;Last activity &quot; + (fmt?.humanTime(item?.anonymizeDate || item?.lastView))">
Last activity {{ fmt?.humanTime(item?.anonymizeDate || item?.lastView) }} &middot; 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 != &#x27;error&#x27; &amp;&amp; !item?._stale &amp;&amp; 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">&mdash;</span>
<span class="expires-never" v-if="item?._expiry?.kind === &#x27;never&#x27;">Never</span>
<span v-if="item?._expiry?.kind === &#x27;date&#x27;" v-text="fmt?.humanTime(item?._expiry?.date)"></span>
<span class="expires-past" v-if="item?._expiry?.kind === &#x27;expired&#x27; &amp;&amp; item?._expiry?.date">Expired {{ fmt?.humanTime(item?._expiry?.date) }}</span>
<span class="expires-past" v-if="item?._expiry?.kind === &#x27;expired&#x27; &amp;&amp; !item?._expiry?.date">Expired</span>
<span class="empty-dash" aria-label="Not applicable" v-if="item?._expiry?.kind === &#x27;none&#x27;">&mdash;</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="&quot;Actions for &quot; + (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 === &#x27;repo&#x27; &amp;&amp; item?.options?.page &amp;&amp; item?.status == &#x27;ready&#x27;" :href="safeUrl(&quot;/w/&quot; + (item?.repoId) + &quot;/&quot;)">
<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 == &#x27;ready&#x27; || item?.status == &#x27;error&#x27;" @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 == &#x27;removed&#x27;" @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 == &#x27;expired&#x27;" @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 == &#x27;ready&#x27; || item?.status == &#x27;expired&#x27; || item?.status == &#x27;error&#x27;) &amp;&amp; item?.role !== &#x27;coauthor&#x27;">
<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 &amp;&amp; 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 &gt; 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
View File
@@ -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 ? &#x27;Show files&#x27; : &#x27;Hide files&#x27;)">
<i class="fas" :class="sidebarCollapsed ? &#x27;fa-folder-open&#x27; : &#x27;fa-times&#x27;"></i>
<span v-text="sidebarCollapsed ? &#x27;Files&#x27; : &#x27;Close&#x27;"></span>
</button>
<div class="leftCol" ng-show="files.length" ng-class="{'collapsed': sidebarCollapsed}">
<div class="leftCol" v-show="files?.length" :class="{&#x27;collapsed&#x27;: 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"
>&times;</button>
<i class="fas tree-search-icon" :class="fileSearchLoading ? &#x27;fa-spinner fa-spin&#x27; : &#x27;fa-search&#x27;"></i>
<input type="text" class="tree-search-input" placeholder="Search files" aria-label="Search files" v-field="{ state: viewState, set: value =&gt; { fileSearchQuery = value }, value: fileSearchQuery, form: null, options: { debounce: 300 }, change: () =&gt; { 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 = &#x27;&#x27;; onFileSearchChange()">&times;</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 &gt; 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 &amp;&amp; !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(&quot;/anonymize/&quot; + (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 == &#x27;html-doc&#x27; &amp;&amp; !showSource" :class="{&#x27;btn-active&#x27;: 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 == &#x27;html-doc&#x27;" @click="toggleSource()">
<i class="fas" :class="showSource ? &#x27;fa-eye&#x27; : &#x27;fa-code&#x27;"></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) + &quot;&amp;download=true&quot;)"><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(&quot;/api/repo/&quot; + (repoId) + &quot;/zip&quot;)"><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(&quot;/w/&quot; + (repoId) + &quot;/&quot;)"><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
View File
@@ -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 &amp; 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 &mdash; 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
View File
@@ -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> &nbsp;/&nbsp;
@@ -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(&quot;/gist-anonymize/&quot; + (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="{&#x27;good&#x27;: details?.isPublic, &#x27;warn&#x27;: !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 &amp;&amp; details?.files?.length) || (details?.comments &amp;&amp; details?.comments?.length)">
<button class="paper-tab" type="button" role="tab" v-if="details?.files" :class="{&#x27;active&#x27;: tabState?.active == &#x27;files&#x27;}" @click="tabState.active = &#x27;files&#x27;">
<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, {&#x27;0&#x27;: &#x27;No files&#x27;, &#x27;one&#x27;: &#x27;1 file&#x27;, &#x27;other&#x27;: &#x27;{} files&#x27;})"></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="{&#x27;active&#x27;: tabState?.active == &#x27;comments&#x27;}" @click="tabState.active = &#x27;comments&#x27;">
<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, {&#x27;0&#x27;: &#x27;No comments&#x27;, &#x27;one&#x27;: &#x27;1 comment&#x27;, &#x27;other&#x27;: &#x27;{} comments&#x27;})"></span>
</button>
</nav>
<div class="paper-tab-content">
<div ng-if="details.files && tabState.active =='files'">
<div v-if="details?.files &amp;&amp; tabState?.active ==&#x27;files&#x27;">
<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 &amp;&amp; tabState?.active ==&#x27;comments&#x27;">
<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
View File
@@ -1,68 +1,39 @@
<nav
class="navbar navbar-expand-lg"
ng-class="{'navbar-dark': isDarkMode}"
>
<nav class="navbar navbar-expand-lg" :class="{&#x27;navbar-dark&#x27;: 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="{&#x27;active&#x27;: path == &#x27;/&#x27;}">
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="{&#x27;active&#x27;: path == &#x27;/dashboard&#x27;}">
<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="{&#x27;active&#x27;:path == &#x27;/anonymize&#x27; || path == &#x27;/pull-request-anonymize&#x27;}">
<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="{&#x27;active&#x27;:path == &#x27;/conferences&#x27;}">
<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="{&#x27;active&#x27;:path == &#x27;/faq&#x27;}"><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 &amp;&amp; user?.isAdmin">
<a class="nav-link" href="/admin/" :class="{&#x27;active&#x27;:path?.indexOf(&#x27;/admin&#x27;) === 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
View File
@@ -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 &rarr;
</a>
<a
class="paper-link-arrow"
target="_self"
href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/"
>See an anonymized repository &rarr;</a
>
<a class="paper-link-arrow" target="_self" href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/">See an anonymized repository &rarr;</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 &gt; 1" :viewBox="&quot;0 0 &quot; + (history[card?.key].viewW) + &quot; 36&quot;">
<rect v-for="(b, index) in history[card?.key].bars" :key="index" :class="{&#x27;is-latest&#x27;: 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 &gt; 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}} &middot; {{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="{&#x27;is-on&#x27;: feature === f?.key}">
<button type="button" class="paper-feature-tab" role="tab" @click="selectFeature(f.key)" @keydown="featureKeydown($event, index)" :id="&quot;feature-tab-&quot; + (f?.key)" :aria-controls="&quot;feature-panel-&quot; + (f?.key)" :aria-selected="(feature === f?.key)" :tabindex="(feature === f?.key ? 0 : -1)">
<span class="paper-eyebrow">{{ f?.num }} &middot; {{ 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}} &rarr;</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 }} &rarr;</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="&quot;feature-panel-&quot; + (f?.key)" :aria-labelledby="&quot;feature-tab-&quot; + (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="&quot;paper-crop paper-crop-&quot; + (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>&copy; {{year || 2026}} Anonymous GitHub &middot; MIT licensed</span>
<span>&copy; {{ year || 2026 }} Anonymous GitHub &middot; MIT licensed</span>
<span class="paper-footer-meta">Built for reviewers, by researchers.</span>
</div>
</footer>
+4 -4
View File
@@ -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">&hellip;</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>
+45 -115
View File
@@ -1,13 +1,13 @@
<div class="container paper-page paper-settings">
<div class="paper-crumbs"><a href="/dashboard">My work</a> &nbsp;/&nbsp; <a href="/conferences">Conferences</a> &nbsp;/&nbsp; <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> &nbsp;/&nbsp; <a href="/conferences">Conferences</a> &nbsp;/&nbsp; <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 &gt; 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 =&gt; { options.name = value }, value: options?.name, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.name?.invalid &amp;&amp; (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 =&gt; { options.url = value }, value: options?.url, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.url?.invalid &amp;&amp; (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 =&gt; { options.conferenceID = value }, value: options?.conferenceID, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.conferenceID?.invalid &amp;&amp; (conference?.conferenceID?.touched || conference?.submitted)}">
<small class="form-text text-muted">3&ndash;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 =&gt; { options.startDate = value }, value: options?.startDate, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.startDate?.invalid &amp;&amp; (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 =&gt; { options.endDate = value }, value: options?.endDate, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.endDate?.invalid &amp;&amp; (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 =&gt; { options.options.link = value }, value: options?.options?.link, form: &quot;conference&quot;, 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 =&gt; { options.options.image = value }, value: options?.options?.image, form: &quot;conference&quot;, 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 =&gt; { options.options.pdf = value }, value: options?.options?.pdf, form: &quot;conference&quot;, 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 =&gt; { options.options.notebook = value }, value: options?.options?.notebook, form: &quot;conference&quot;, 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 =&gt; { options.options.update = value }, value: options?.options?.update, form: &quot;conference&quot;, 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 =&gt; { options.options.page = value }, value: options?.options?.page, form: &quot;conference&quot;, 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="{&#x27;selected&#x27;: options?.plan?.planID == plan?.id}">
<input type="radio" name="planPicker" v-field="{ state: viewState, set: value =&gt; { options.plan.planID = value }, value: options?.plan?.planID, form: &quot;conference&quot;, options: {} }" :value="plan?.id">
<div class="paper-plan-head" v-text="plan?.name"></div>
<div class="paper-plan-price">
{{plan.pricePerRepo | number}}&euro;
{{ fmt?.number(plan?.pricePerRepo) }}&euro;
<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 &gt; 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 &amp; last name" />
<input type="text" class="form-control" name="billing_name" id="billing_name" required placeholder="First &amp; last name" v-field="{ state: viewState, set: value =&gt; { options.billing.name = value }, value: options?.billing?.name, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.billing_name?.invalid &amp;&amp; (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 =&gt; { options.billing.email = value }, value: options?.billing?.email, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.email?.invalid &amp;&amp; (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 =&gt; { options.billing.address = value }, value: options?.billing?.address, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.inputAddress?.invalid &amp;&amp; (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 =&gt; { options.billing.address2 = value }, value: options?.billing?.address2, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.inputAddress2?.invalid &amp;&amp; (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 =&gt; { options.billing.city = value }, value: options?.billing?.city, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.city?.invalid &amp;&amp; (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 =&gt; { options.billing.country = value }, value: options?.billing?.country, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.country?.invalid &amp;&amp; (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 =&gt; { options.billing.zip = value }, value: options?.billing?.zip, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.zip?.invalid &amp;&amp; (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 =&gt; { options.billing.vat = value }, value: options?.billing?.vat, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.vat?.invalid &amp;&amp; (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, () =&gt; { submit($event) })">
{{ editionMode ? "Update" : "Create" }} conference
</button>
</div>
</form>
+16 -25
View File
@@ -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 == &#x27;text&#x27;"></div>
<div class="file-content markdown-body" v-if="type == &#x27;html&#x27;" v-html="sanitize(content)"></div>
<html-doc v-if="type == &#x27;html-doc&#x27; &amp;&amp; !showSource &amp;&amp; content != null" :content="content" :base-url="(fileBaseUrl)" :allow-scripts="allowScripts"></html-doc>
<div v-code-editor="{ content: content, options: aceOption }" v-if="(type == &#x27;code&#x27; || (type == &#x27;html-doc&#x27; &amp;&amp; showSource)) &amp;&amp; content != null"></div>
<img class="image-content" v-if="type == &#x27;image&#x27;" :src="safeUrl((url))">
<iframe class="h-100 overflow-auto w-100 b-0" v-if="type == &#x27;media&#x27;" :src="safeUrl((url))"></iframe>
<pdfviewer v-if="type == &#x27;pdf&#x27;" :src="safeUrl((url))"></pdfviewer>
<div v-if="type == &#x27;audio&#x27;"><audio controls="controls"><source :src="safeUrl((url))"></audio></div>
<div v-if="type == &#x27;IPython&#x27;"><notebook :file="url" :content="content"></notebook></div>
<div class="file-error container d-flex h-100" v-if="type == &#x27;rate_limited&#x27;">
<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&hellip;</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 == &#x27;error&#x27;"><h1 class="paper-empty-title m-auto" v-text="fmt.translate(&quot;ERRORS.&quot; + (content))">Error</h1></div>
<div class="file-error container d-flex h-100" v-if="type == &#x27;loading&#x27; &amp;&amp; !error"><h1 class="paper-empty-title m-auto">Loading&hellip;</h1></div>
<div class="file-error container d-flex h-100" v-if="type == &#x27;empty&#x27;"><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 &amp;&amp; type != &#x27;empty&#x27;"><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 == &#x27;binary&#x27;"><h1 class="paper-empty-title m-auto">Unsupported <em>binary file</em>. You can <a target="_blank" :href="safeUrl((url) + &quot;&amp;download=true&quot;)">download it</a>.</h1></div>
+41 -89
View File
@@ -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 =&gt; { 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 =&gt; { 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 =&gt; { 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 =&gt; { 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 =&gt; { 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 =&gt; { 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 =&gt; { 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 =&gt; { 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 =&gt; { 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;">
&times;
</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="{&#x27;repo-inactive&#x27;: pr?.status == &#x27;expired&#x27; || pr?.status == &#x27;removed&#x27; || pr?.status == &#x27;error&#x27;}">
<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(&quot;/pr/&quot; + (pr?.pullRequestId))"></a>
<span class="status-badge" :class="{&#x27;status-removed&#x27;: pr?.status == &#x27;removed&#x27; || pr?.status == &#x27;expired&#x27; || pr?.status == &#x27;removing&#x27; || pr?.status == &#x27;expiring&#x27;, &#x27;status-preparing&#x27;: pr?.status == &#x27;preparing&#x27; || pr?.status == &#x27;download&#x27;, &#x27;status-ready&#x27;: pr?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: pr?.status == &#x27;error&#x27;}"><span v-text="fmt?.title(pr?.status)"></span><span v-if="pr?.status == &#x27;error&#x27;" v-text="&#x27;: &#x27; + 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(&quot;https://github.com/&quot; + (pr?.source?.repositoryFullName) + &quot;/pull/&quot; + (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="{&#x27;status-ready&#x27;: pr?.merged, &#x27;status-removed&#x27;: pr?.state==&#x27;open&#x27;, &#x27;status-error&#x27;: pr?.state==&#x27;closed&#x27; &amp;&amp; !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="&quot;Terms: &quot; + (pr?.options?.terms?.join(&#x27;, &#x27;))">
<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="&quot;Views: &quot; + (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="&quot;Last view: &quot; + (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!=&#x27;never&#x27; &amp;&amp; pr?.status == &#x27;ready&#x27;">
<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(&quot;/pull-request-anonymize/&quot; + (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 == &#x27;ready&#x27; || pr?.status == &#x27;error&#x27;" @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 == &#x27;removed&#x27;" @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 == &#x27;ready&#x27;" @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(&quot;/pr/&quot; + (pr?.pullRequestId) + &quot;/&quot;)">
<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
View File
@@ -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: &#x27;repository&#x27;, label: &#x27;Repositories&#x27;, kind: &#x27;count&#x27;},
{key: &#x27;storage&#x27;, label: &#x27;Storage&#x27;, kind: &#x27;bytes&#x27;},
{key: &#x27;file&#x27;, label: &#x27;Files&#x27;, kind: &#x27;count&#x27;}
]">
<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 === &#x27;count&#x27;">
{{ 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 === &#x27;bytes&#x27;">
{{ 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="&#x27;quota-&#x27; + quota[q?.key].level" :aria-label="(q?.label) + &quot; quota&quot;" :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 ? &#x27;unlimited&#x27; : fmt.number(quota[q?.key].percent, 0) + &#x27;% used&#x27;)">
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + &#x27;%&#x27;}"></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, () =&gt; { 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&#10;MIT CSAIL&#10;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 =&gt; { terms = value }, value: terms, form: &quot;defaultsForm&quot;, 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=&gt;replacement</code> to pick your own.
<code>{{ site_options?.ANONYMIZATION_MASK || 'XXX' }}-[N]</code>, or use <code>term=&gt;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 =&gt; { options.link = value }, value: options?.link, form: &quot;defaultsForm&quot;, 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 =&gt; { options.image = value }, value: options?.image, form: &quot;defaultsForm&quot;, 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 =&gt; { options.pdf = value }, value: options?.pdf, form: &quot;defaultsForm&quot;, 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 =&gt; { options.notebook = value }, value: options?.notebook, form: &quot;defaultsForm&quot;, 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 =&gt; { options.page = value }, value: options?.page, form: &quot;defaultsForm&quot;, 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 =&gt; { options.loc = value }, value: options?.loc, form: &quot;defaultsForm&quot;, 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 =&gt; { options.update = value }, value: options?.update, form: &quot;defaultsForm&quot;, 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 =&gt; { options.mode = value }, value: options?.mode, form: &quot;defaultsForm&quot;, 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 =&gt; { options.expirationMode = value }, value: options?.expirationMode, form: &quot;defaultsForm&quot;, 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>
+28 -51
View File
@@ -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(&quot;/pull-request-anonymize/&quot; + (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="{&#x27;good&#x27;: details?.merged, &#x27;warn&#x27;: details?.state == &#x27;open&#x27;, &#x27;bad&#x27;: details?.state == &#x27;closed&#x27; &amp;&amp; !details?.merged}">
<span class="status-dot" :class="{&#x27;status-ready&#x27;: details?.merged, &#x27;status-error&#x27;: details?.state == &#x27;closed&#x27; &amp;&amp; !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="{&#x27;active&#x27;: tabState?.active == &#x27;diff&#x27;}" @click="tabState.active = &#x27;diff&#x27;">
<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="{&#x27;active&#x27;: tabState?.active == &#x27;comments&#x27;}" @click="tabState.active = &#x27;comments&#x27;">
<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, {&#x27;0&#x27;: &#x27;No comments&#x27;, &#x27;one&#x27;: &#x27;1 comment&#x27;, &#x27;other&#x27;: &#x27;{} comments&#x27;})"></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 &amp;&amp; tabState?.active ==&#x27;diff&#x27;">
<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 &amp;&amp; tabState?.active ==&#x27;comments&#x27;">
<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
View File
@@ -2,13 +2,13 @@
<div class="paper-crumbs">Anonymization &nbsp;/&nbsp; <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="{&#x27;status-pill-ready&#x27;: repo?.status == &#x27;ready&#x27;, &#x27;status-pill-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;status-pill-removed&#x27;: repo?.status == &#x27;removed&#x27; || repo?.status == &#x27;expired&#x27;, &#x27;status-pill-ratelimit&#x27;: rateLimitResetAt}">
<span class="status-dot" :class="{&#x27;status-ready&#x27;: repo?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;status-removed&#x27;: repo?.status == &#x27;removed&#x27; || repo?.status == &#x27;expired&#x27;, &#x27;status-ratelimit&#x27;: 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 &mdash; 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 &mdash; 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 != &#x27;error&#x27; &amp;&amp; !rateLimitResetAt" :class="{&#x27;paper-progress-ready&#x27;: repo?.status == &#x27;ready&#x27;}" :aria-valuenow="(progress)">
<div class="paper-progress-bar" :style="&quot;width: &quot; + (progress) + &quot;%;&quot;"></div>
<div class="paper-progress-label">
<span ng-bind="repo.status | title"></span><span ng-if="repo.statusMessage">&nbsp;&middot;&nbsp;<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">&nbsp;&middot;&nbsp;<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 == &#x27;error&#x27; &amp;&amp; !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 &mdash; 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(&quot;/anonymize/&quot; + (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(&quot;/r/&quot; + (repoId) + &quot;/&quot;)">/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(&quot;/w/&quot; + (repoId) + &quot;/&quot;)">/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 == &#x27;ready&#x27;">
<a class="btn btn-ink" target="_self" :href="safeUrl(&quot;/r/&quot; + (repoId) + &quot;/&quot;)">
<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(&quot;/w/&quot; + (repoId) + &quot;/&quot;)">
<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&amp;widget=true&amp;embed=true&amp;preview=true" title="tdurieux" loading="lazy"></iframe>
</div>
</section>
</div>