Merge commit 'refs/security-review/pr846' into HEAD
# Conflicts: # public/script/main.js
No files matched your search
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"core.min.js": "core.3b07189932.min.js",
|
||||
"vendor.min.js": "vendor.3036803e29.min.js",
|
||||
"vendor.min.js": "vendor.126b1a8e99.min.js",
|
||||
"mermaid.min.js": "mermaid.f848a72d16.min.js",
|
||||
"all.min.css": "all.5fbafcda4e.min.css",
|
||||
"all.min.css": "all.4c22aa9cd3.min.css",
|
||||
"markdown.min.js": "markdown.44f2e93820.min.js",
|
||||
"pdf.min.js": "pdf.0175d53d2f.min.js",
|
||||
"pdf.min.js": "pdf.4d9cc6ff49.min.js",
|
||||
"notebook.min.js": "notebook.c3144c18ae.min.js",
|
||||
"org.min.js": "org.f61f262915.min.js",
|
||||
"editor.min.js": "editor.4f142d3adc.min.js"
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
*/:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unsLine truncated
|
||||
* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
|
||||
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"ERRORS": {
|
||||
"cache_busy": "Downloads are busy. Please try again shortly.",
|
||||
"invalid_sort": "The selected sort order is invalid.",
|
||||
"invalid_cursor": "This project page is no longer valid. Reload the list.",
|
||||
"repository_changed": "The repository changed while processing. Please retry.",
|
||||
"repository_job_cancelled": "The repository changed or was removed while processing.",
|
||||
"storage_delete_failed": "Unable to remove cached files. Please try again later.",
|
||||
"unknown_error": "Unknown error, contact the admin.",
|
||||
@@ -58,6 +62,7 @@
|
||||
"path_not_defined": "The file path has not been resolved yet.",
|
||||
"invalid_file_path": "The requested file path is not valid.",
|
||||
"invalid_request": "The request is missing required fields or is malformed.",
|
||||
"invalid_project_name": "Use a project name of up to 100 characters without line breaks or control characters.",
|
||||
"no_file_selected": "Please select a file.",
|
||||
"file_not_found": "The requested file is not found.",
|
||||
"file_not_accessible": "The requested file is not accessible.",
|
||||
|
||||
|
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 295 KiB After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 62 KiB |
@@ -76,6 +76,8 @@
|
||||
<script src="/script/__VENDOR_JS__" defer></script>
|
||||
<script>
|
||||
window.addEventListener("load", function () {
|
||||
// Phones use the always-visible header link instead of an overlay.
|
||||
if (window.matchMedia("(max-width: 991px)").matches) return;
|
||||
var s = document.createElement("script");
|
||||
s.src = "https://storage.ko-fi.com/cdn/scripts/overlay-widget.js";
|
||||
s.onload = function () {
|
||||
@@ -84,11 +86,9 @@
|
||||
type: "floating-chat",
|
||||
"floating-chat.donateButton.text": "Support me",
|
||||
"floating-chat.donateButton.background-color": isDark
|
||||
? "#FAF9F6"
|
||||
? "#34312C"
|
||||
: "#1A1815",
|
||||
"floating-chat.donateButton.text-color": isDark
|
||||
? "#1A1815"
|
||||
: "#FAF9F6",
|
||||
"floating-chat.donateButton.text-color": "#FAF9F6",
|
||||
});
|
||||
};
|
||||
document.body.appendChild(s);
|
||||
|
||||
@@ -3,25 +3,25 @@
|
||||
<h1 class="paper-page-title">Conferences</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences" class="active"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary">
|
||||
<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>
|
||||
<button type="button" class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1" :aria-pressed="!!(query?.ready)">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></button>
|
||||
<button type="button" class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1" :aria-pressed="!!(query?.preparing)">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1" :aria-pressed="!!(query?.error)">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1" :aria-pressed="!!(query?.expired)">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle removed filter" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1" :aria-pressed="!!(query?.removed)">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ fetchError }}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Conferences" accept-charset="UTF-8" v-form="viewState">
|
||||
@@ -31,11 +31,11 @@
|
||||
<span class="admin-search-hint" v-if="!query?.search">/</span>
|
||||
</div>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export</button>
|
||||
<span class="admin-filter-inline" aria-label="Pagination">
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1" aria-label="Previous page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg></button>
|
||||
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{ query?.page }}/{{ totalPage || 1 }}</span>
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage" aria-label="Next page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<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>
|
||||
<button type="button" @click="clearFilter(chip.key)" aria-label="Clear filter"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,15 +52,15 @@
|
||||
|
||||
<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" :class="{active: query?.sort == 'name'}" @click="sortBy('name')">Conference <i class="fas" :class="sortIcon('name')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'name'}" @click="sortBy('name')">Conference <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('name')" :class="{ascending: sortIcon('name') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('status')" :class="{ascending: sortIcon('status') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" class="num">Repos</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'startDate'}" @click="sortBy('startDate')">Window <i class="fas" :class="sortIcon('startDate')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'startDate'}" @click="sortBy('startDate')">Window <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('startDate')" :class="{ascending: sortIcon('startDate') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<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>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Conference"><use href="#project-icon-conference"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="conference?.name" :href="safeUrl("/conference/" + (conference?.conferenceID))"></a>
|
||||
<div class="anon-sub">
|
||||
@@ -72,27 +72,27 @@
|
||||
<span class="status-dot" :class="{'status-removed': conference?.status == 'removed' || conference?.status == 'expired', 'status-ready': conference?.status == 'ready', 'status-error': conference?.status == 'error', 'status-preparing': conference?.status == 'preparing'}"></span>
|
||||
<span v-text="fmt?.title(conference?.status)"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell">
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg>
|
||||
<a title="Show repositories in this conference" v-text="fmt?.number(conference?.repositories?.length || 0)" :href="safeUrl("/admin/?conference=" + (conference?.conferenceID))"></a>
|
||||
</div>
|
||||
<div class="cell-expires" role="cell">{{ fmt?.date(conference?.startDate) }} – {{ fmt?.date(conference?.endDate) }}</div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> {{ fmt?.date(conference?.startDate) }} – {{ fmt?.date(conference?.endDate) }}</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/")"><i class="fa fa-eye"></i> View</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/?conference=" + (conference?.conferenceID))"><i class="fas fa-code-branch"></i> View repositories</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/?conference=" + (conference?.conferenceID))"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> View repositories</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="conference?.status != 'removed'" @click.prevent="removeConference(conference)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="conference?.status != 'removed'" @click.prevent="removeConference(conference)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredConferences?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No conferences match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,10 +100,10 @@
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i> Previous</button>
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
<span>of {{ totalPage }}</span>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage">Next <i class="fas fa-chevron-right"></i></button>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage">Next <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
<label>Per page</label>
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
<header class="errors-header">
|
||||
<h1 class="paper-page-title">Errors</h1>
|
||||
<div class="errors-actions">
|
||||
<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>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export CSV</button>
|
||||
<button class="btn btn-sm btn-danger-soft" type="button" @click="clearAll()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Clear all</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors" class="active"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<section class="kpi-grid">
|
||||
@@ -78,7 +78,7 @@
|
||||
<button type="button" :class="{active: query?.bucket === 'info'}" @click="setBucket('info')">Info</button>
|
||||
</div>
|
||||
<div class="search-wrap">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<svg class="ui-icon search-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg>
|
||||
<input type="search" class="form-control" placeholder="code:repo_not_found module:route status:>=400" autocomplete="off" v-field="{ state: viewState, set: value => { query.search = value }, value: query?.search, form: null, options: {} }">
|
||||
<span class="filter-count" v-if="parsedFilterCount">{{ parsedFilterCount }} filter{{ parsedFilterCount > 1 ? 's' : '' }}</span>
|
||||
</div>
|
||||
@@ -101,7 +101,7 @@
|
||||
<input type="checkbox" v-field="{ state: viewState, set: value => { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()" aria-label="Refresh"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
|
||||
</form>
|
||||
|
||||
<div class="admin-empty" v-if="!visible?.length">No errors captured.</div>
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
<div class="col-sev">
|
||||
<span class="sev-dot" :class="'sev-' + row?._bucket"></span>
|
||||
<span class="sev-label">{{ fmt?.uppercase(row?._bucket) }}</span>
|
||||
<span class="sev-label">{{ fmt?.title(row?._bucket) }}</span>
|
||||
</div>
|
||||
<div class="col-mod"><span class="pill pill-module">{{ row?.module }}</span></div>
|
||||
<div class="col-msg">
|
||||
@@ -164,8 +164,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-actions">
|
||||
<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>
|
||||
<button class="btn btn-sm" type="button" title="Copy a curl that reproduces the request" @click="copyCurl(row)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Copy curl</button>
|
||||
<button class="btn btn-sm" type="button" @click="copyJson(row)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-clipboard"></use></svg> Copy JSON</button>
|
||||
<span class="copy-hint" v-if="copyHint">{{ copyHint }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<div class="container paper-page admin-page overview-page">
|
||||
<div class="paper-crumbs">Admin · System Health</div>
|
||||
<div class="paper-crumbs">Admin / <span class="here">Overview</span></div>
|
||||
|
||||
<div class="ov-header">
|
||||
<h1 class="paper-page-title">Overview</h1>
|
||||
</div>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/" class="active"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-empty" v-if="loading">Loading overview…</div>
|
||||
<div class="alert alert-danger" style="margin:12px 0" v-if="error"><i class="fas fa-exclamation-triangle"></i> {{ error }}</div>
|
||||
<div class="alert alert-danger" style="margin:12px 0" v-if="error"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ error }}</div>
|
||||
|
||||
|
||||
<div v-if="data">
|
||||
|
||||
@@ -104,11 +105,11 @@
|
||||
<div class="ov-chart-card">
|
||||
<div class="ov-chart-head">
|
||||
<span class="ov-chart-title">Users · 30d</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>total users</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>total users · {{ fmt?.number(historyRange('nbUsers').min) }}–{{ fmt?.number(historyRange('nbUsers').max) }}</span>
|
||||
</div>
|
||||
<div class="ov-spark-bars" v-if="data?.history?.length">
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": " + (fmt?.number(d?.nbUsers)) + " users"" :aria-label="(historyLabel(d)) + ": " + (fmt?.number(d?.nbUsers)) + " users"">
|
||||
<span class="ov-spark-fill ov-spark-fill-user" :style="{height: historyBarH(d, 'nbUsers') + 'px'}"></span>
|
||||
<span class="ov-spark-fill ov-spark-fill-user" :style="{height: historyRangeH(d, 'nbUsers') + 'px'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-spark-x" v-if="data?.history?.length">
|
||||
@@ -274,4 +275,81 @@
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<section class="ov-panel-card ov-performance" aria-labelledby="performance-heading">
|
||||
<div class="ov-panel-head ov-performance-head">
|
||||
<div>
|
||||
<h2 class="ov-panel-title" id="performance-heading">Request performance</h2>
|
||||
<p class="ov-performance-note">Request latency and process health · refreshes every 30 seconds</p>
|
||||
</div>
|
||||
<div class="ov-performance-window">
|
||||
<label for="performance-window">Time range</label>
|
||||
<select id="performance-window" class="form-control form-control-sm" v-model.number="performanceMinutes" @change="loadPerformance()" aria-label="Performance time range">
|
||||
<option :value="15">Last 15 minutes</option><option :value="60">Last hour</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="performanceError" class="ov-performance-message ov-performance-warning" role="alert">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
||||
<span>{{ performanceError }}</span>
|
||||
<button type="button" class="btn btn-sm" @click="loadPerformance()">Retry</button>
|
||||
</div>
|
||||
<p v-if="!performance && !performanceError" class="ov-performance-empty" role="status">Loading performance monitoring…</p>
|
||||
<div v-if="performance?.available">
|
||||
<div class="paper-tabs ov-performance-tabs" role="tablist" aria-label="Performance metrics">
|
||||
<button id="performance-routes-tab" type="button" class="paper-tab" role="tab" :class="{active: performanceTab === 'routes'}" :aria-selected="performanceTab === 'routes'" :tabindex="performanceTab === 'routes' ? 0 : -1" aria-controls="performance-routes" @click="performanceTab = 'routes'" @keydown="previewTabKeydown($event)">Routes</button>
|
||||
<button id="performance-processes-tab" type="button" class="paper-tab" role="tab" :class="{active: performanceTab === 'processes'}" :aria-selected="performanceTab === 'processes'" :tabindex="performanceTab === 'processes' ? 0 : -1" aria-controls="performance-processes" @click="performanceTab = 'processes'" @keydown="previewTabKeydown($event)">Processes</button>
|
||||
<button id="performance-stages-tab" type="button" class="paper-tab" role="tab" :class="{active: performanceTab === 'stages'}" :aria-selected="performanceTab === 'stages'" :tabindex="performanceTab === 'stages' ? 0 : -1" aria-controls="performance-stages" @click="performanceTab = 'stages'" @keydown="previewTabKeydown($event)">Work stages</button>
|
||||
</div>
|
||||
<div id="performance-routes" role="tabpanel" aria-labelledby="performance-routes-tab" v-show="performanceTab === 'routes'">
|
||||
<div class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Route latency metrics, scroll for more columns" v-if="performance.routes?.length">
|
||||
<table class="ov-metrics-table">
|
||||
<caption class="sr-only">Route latency for the selected time range</caption>
|
||||
<thead><tr><th scope="col">Route</th><th scope="col" class="num">Requests</th><th scope="col" class="num">Average</th><th scope="col" class="num">First byte p95</th><th scope="col" class="num">p95</th><th scope="col" class="num">p99</th><th scope="col" class="num">Interrupted</th><th scope="col" class="num">5xx</th><th scope="col" class="num">Slow</th></tr></thead>
|
||||
<tbody><tr v-for="row in performance.routes" :key="row.service + row.method + row.route">
|
||||
<th scope="row" class="ov-metric-name"><span>{{ row.route }}</span><small>{{ row.service }} · {{ row.method }}</small></th>
|
||||
<td class="num">{{ fmt.number(row.count) }}</td><td class="num">{{ formatMs(row.avgMs) }}</td>
|
||||
<td class="num">{{ firstByteBound(row) }}</td><td class="num">{{ latencyBound(row, 'p95UpperMs') }}</td><td class="num">{{ latencyBound(row, 'p99UpperMs') }}</td>
|
||||
<td class="num" :class="{'ov-metric-warn': row.aborted > 0}">{{ fmt.number(row.aborted) }}</td><td class="num" :class="{'ov-metric-warn': row.errors > 0}">{{ fmt.number(row.errors) }}</td><td class="num" :class="{'ov-metric-warn': row.slow > 0}">{{ fmt.number(row.slow) }}</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="ov-performance-empty">No recorded requests in this time range yet.</p>
|
||||
</div>
|
||||
<div id="performance-processes" role="tabpanel" aria-labelledby="performance-processes-tab" v-show="performanceTab === 'processes'">
|
||||
<p class="ov-performance-note">API and streamer processes · latest sample per instance</p>
|
||||
<div v-if="!performance.instances?.length" class="ov-performance-message ov-performance-warning" role="status">No recent process samples. Monitoring coverage is incomplete.</div>
|
||||
<div v-else class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Process resource metrics, scroll for more columns">
|
||||
<table class="ov-metrics-table">
|
||||
<caption class="sr-only">API and streamer process resource usage</caption>
|
||||
<thead><tr><th scope="col">Process</th><th scope="col" class="num">RSS / limit</th><th scope="col" class="num">CPU</th><th scope="col" class="num">Event loop p95</th><th scope="col" class="num">Active requests</th><th scope="col" class="num">Sockets / CLOSE_WAIT</th><th scope="col" class="num">Workers / waiting</th><th scope="col" class="num">Sample age</th><th scope="col" class="num">Dropped batches / metrics</th></tr></thead>
|
||||
<tbody><tr v-for="sample in performance.instances" :key="sample.instance" :class="{'ov-metric-warning-row': performanceWarning(sample)}">
|
||||
<th scope="row" class="ov-metric-name"><span>{{ sample.instance }}</span><small class="ov-metric-warn" v-if="performanceWarning(sample)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> Needs attention</small></th>
|
||||
<td class="num">{{ humanBytes(sample.memory.rss) }} / {{ humanBytes(sample.memoryLimitBytes) }}<small>Peak {{ humanBytes(peakRss(sample)) }}</small></td>
|
||||
<td class="num">{{ sample.cpuPercent.toFixed(1) }}%</td><td class="num">{{ sample.eventLoop.p95Ms.toFixed(1) }} ms</td>
|
||||
<td class="num">{{ sample.activeRequests }}</td><td class="num">{{ sample.sockets?.descriptors ?? 'Unavailable' }} / {{ sample.sockets?.closeWait ?? 'Unavailable' }}</td>
|
||||
<td class="num">{{ sample.workers?.running ?? 0 }} / {{ sample.workers?.waiting ?? 0 }}</td><td class="num">{{ sampleAge(sample) }} s</td>
|
||||
<td class="num">{{ sample.droppedBatches }} / {{ sample.droppedMetrics }}</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="performance-stages" role="tabpanel" aria-labelledby="performance-stages-tab" v-show="performanceTab === 'stages'">
|
||||
<div v-if="performance.stages?.some(row => row.metric !== 'first_byte')" class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Work stage metrics, scroll for more columns">
|
||||
<table class="ov-metrics-table">
|
||||
<caption class="sr-only">Work stages and cache results</caption>
|
||||
<thead><tr><th scope="col">Work stage</th><th scope="col" class="num">Samples</th><th scope="col" class="num">Average</th><th scope="col" class="num">p95</th></tr></thead>
|
||||
<tbody><tr v-for="row in performance.stages.filter(row => row.metric !== 'first_byte')" :key="row.service + row.metric">
|
||||
<th scope="row" class="ov-metric-name"><span>{{ row.metric }}</span><small>{{ row.service }}</small></th><td class="num">{{ fmt.number(row.count) }}</td><td class="num">{{ formatMs(row.avgMs) }}</td><td class="num">{{ latencyBound(row, 'p95UpperMs') }}</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="ov-performance-empty">No recorded work stages or cache results in this time range yet.</p>
|
||||
</div>
|
||||
<details class="ov-performance-help">
|
||||
<summary>How to read these metrics</summary>
|
||||
<p>Latency includes upstream waits and response transfer. Percentiles show histogram bucket upper bounds, not exact timings. Process rows marked “Needs attention” have stale samples, dropped monitoring data, CLOSE_WAIT sockets, high event loop delay, or high memory usage.</p>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -11,22 +11,22 @@
|
||||
<button class="btn btn-sm" :class="{active: range == '7d'}" @click="setRange('7d')">7d</button>
|
||||
</div>
|
||||
<button class="btn btn-sm" @click="pauseAll()">Pause all</button>
|
||||
<button class="btn btn-sm btn-dark" @click="drainSelected()">Drain {{ selectedQueue }}</button>
|
||||
<button class="btn btn-sm btn-danger-soft" @click="drainSelected()">Drain {{ selectedQueue }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues" class="active"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<!-- Queue overview cards -->
|
||||
<div class="q-cards">
|
||||
<div class="q-card" v-for="(q, index) in queueList" :class="{selected: selectedQueue == q?.key, paused: q?.paused}" @click="selectQueue(q.key)">
|
||||
<div class="q-card" role="button" tabindex="0" :aria-pressed="selectedQueue === q.key" @keydown.enter.prevent="selectQueue(q.key)" @keydown.space.prevent="selectQueue(q.key)" v-for="(q, index) in queueList" :class="{selected: selectedQueue == q?.key, paused: q?.paused}" @click="selectQueue(q.key)">
|
||||
<div class="q-card-head">
|
||||
<span class="q-dot" :class="{'q-dot-red': q?.paused || q?.counts?.failed > 0}"></span>
|
||||
<span class="q-card-name" v-text="q?.label"></span>
|
||||
@@ -44,7 +44,7 @@
|
||||
<!-- Detail: throughput chart + stats panel -->
|
||||
<div class="q-detail" v-if="selectedStats">
|
||||
<div class="q-throughput">
|
||||
<div class="q-section-label">{{ selectedQueue }}·throughput <span class="q-section-right"><span class="q-legend-completed">●</span> completed <span class="q-legend-failed">●</span> failed <span class="q-legend-exec">- -</span> avg time · {{ fmt?.uppercase(range) }}</span></div>
|
||||
<div class="q-section-label">{{ selectedQueue }} · throughput <span class="q-section-right"><span class="q-legend-completed">●</span> completed <span class="q-legend-failed">●</span> failed <span class="q-legend-exec">- -</span> avg time · {{ 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,30 +52,30 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-stats-panel">
|
||||
<div class="q-section-label">{{ selectedQueue }}·stats</div>
|
||||
<div class="q-section-label">{{ selectedQueue }} · stats</div>
|
||||
<div class="q-stats-grid">
|
||||
<div class="q-stat">
|
||||
<div class="q-stat-label">WAITING</div>
|
||||
<div class="q-stat-label">Waiting</div>
|
||||
<div class="q-stat-value" v-text="selectedStats?.counts?.waiting || 0"></div>
|
||||
</div>
|
||||
<div class="q-stat">
|
||||
<div class="q-stat-label">ACTIVE</div>
|
||||
<div class="q-stat-label">Active</div>
|
||||
<div class="q-stat-value" v-text="selectedStats?.counts?.active || 0"></div>
|
||||
</div>
|
||||
<div class="q-stat">
|
||||
<div class="q-stat-label">COMPLETED (24H)</div>
|
||||
<div class="q-stat-label">Completed (24h)</div>
|
||||
<div class="q-stat-value" v-text="fmt?.number(selectedStats?.completed24h)"></div>
|
||||
</div>
|
||||
<div class="q-stat">
|
||||
<div class="q-stat-label">FAILED (24H)</div>
|
||||
<div class="q-stat-label">Failed (24h)</div>
|
||||
<div class="q-stat-value" v-text="selectedStats?.failed24h || 0"></div>
|
||||
</div>
|
||||
<div class="q-stat">
|
||||
<div class="q-stat-label">DELAYED</div>
|
||||
<div class="q-stat-label">Delayed</div>
|
||||
<div class="q-stat-value" v-text="selectedStats?.counts?.delayed || 0"></div>
|
||||
</div>
|
||||
<div class="q-stat">
|
||||
<div class="q-stat-label">WORKERS</div>
|
||||
<div class="q-stat-label">Workers</div>
|
||||
<div class="q-stat-value" v-text="selectedStats?.workers || 0"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,13 +88,13 @@
|
||||
</div>
|
||||
|
||||
<div class="q-toast-error" v-if="actionError" @click="actionError = null">
|
||||
<i class="fas fa-exclamation-circle"></i> {{ actionError }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> {{ actionError }}
|
||||
</div>
|
||||
|
||||
<!-- Jobs table -->
|
||||
<div class="q-jobs">
|
||||
<div class="q-jobs-header">
|
||||
<div class="q-section-label">ALL JOBS · {{ fmt?.uppercase(selectedQueue) }}</div>
|
||||
<div class="q-section-label">All jobs · {{ selectedQueue }}</div>
|
||||
<div class="q-state-filters">
|
||||
<label class="q-state-toggle" v-for="(s, index) in allStates">
|
||||
<input type="checkbox" v-field="{ state: viewState, set: value => { stateFilter[s] = value }, value: stateFilter[s], form: null, options: {} }">
|
||||
@@ -109,18 +109,18 @@
|
||||
<input type="checkbox" v-field="{ state: viewState, set: value => { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button class="btn btn-sm" type="button" title="Refresh now" @click="refreshNow()"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()" aria-label="Refresh"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
|
||||
</div>
|
||||
|
||||
<table class="q-table" v-if="filteredJobs().length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>STATE</th>
|
||||
<th>JOB ID</th>
|
||||
<th>PAYLOAD</th>
|
||||
<th>ATTEMPTS</th>
|
||||
<th>DURATION</th>
|
||||
<th>PROGRESS</th>
|
||||
<th>State</th>
|
||||
<th>Job id</th>
|
||||
<th>Payload</th>
|
||||
<th>Attempts</th>
|
||||
<th>Duration</th>
|
||||
<th>Progress</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -131,7 +131,7 @@
|
||||
<span class="q-delay-hint" v-if="job?._state == 'delayed' && job?.delayUntil" v-text="delayCountdown(job?.delayUntil)"></span>
|
||||
</td>
|
||||
<td class="q-cell-id">
|
||||
<i class="fas fa-chevron-right q-chevron" :class="{'q-chevron-open': expanded[job?.id]}"></i>
|
||||
<svg class="ui-icon q-chevron" :class="{'q-chevron-open': expanded[job?.id]}" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg>
|
||||
<a target="_blank" v-text="'job:' + fmt.limitTo(job?.id, 6)" @click="$event.stopPropagation()" :href="safeUrl("/r/" + (job?.id))"></a>
|
||||
</td>
|
||||
<td class="q-cell-payload">
|
||||
@@ -147,8 +147,8 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="q-cell-actions">
|
||||
<button class="btn btn-sm" title="Retry" v-if="job?._state == 'failed'" @click="retryJob(job); $event.stopPropagation()"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm" title="Remove" @click="removeJob(job); $event.stopPropagation()"><i class="fas fa-trash-alt"></i></button>
|
||||
<button class="btn btn-sm btn-icon" title="Retry" v-if="job?._state == 'failed'" @click="retryJob(job); $event.stopPropagation()" aria-label="Retry job"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
|
||||
<button class="btn btn-sm btn-icon" title="Remove" aria-label="Remove job" @click="removeJob(job); $event.stopPropagation()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="q-detail-row" v-if="expanded[job?.id]">
|
||||
@@ -156,58 +156,58 @@
|
||||
<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-label">Job id</span>
|
||||
<span class="q-job-detail-value"><a target="_blank" v-text="job?.id" :href="safeUrl("/r/" + (job?.id))"></a></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item">
|
||||
<span class="q-job-detail-label">STATE</span>
|
||||
<span class="q-job-detail-label">State</span>
|
||||
<span class="q-job-detail-value"><span v-text="job?._state" :class=""q-state-badge q-state-" + (job?._state)"></span></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" v-if="job?.data?.repoId">
|
||||
<span class="q-job-detail-label">REPO ID</span>
|
||||
<span class="q-job-detail-label">Repo ID</span>
|
||||
<span class="q-job-detail-value" v-text="job?.data?.repoId"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" v-if="job?.timestamp">
|
||||
<span class="q-job-detail-label">CREATED</span>
|
||||
<span class="q-job-detail-label">Created</span>
|
||||
<span class="q-job-detail-value" v-text="humanTime(job?.timestamp)"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" v-if="job?._state == 'delayed' && job?.delayUntil">
|
||||
<span class="q-job-detail-label">RETRY AT</span>
|
||||
<span class="q-job-detail-label">Retry at</span>
|
||||
<span class="q-job-detail-value">{{ humanTime(job?.delayUntil) }} ({{ delayCountdown(job?.delayUntil) }})</span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" v-if="job?.processedOn">
|
||||
<span class="q-job-detail-label">PROCESSED</span>
|
||||
<span class="q-job-detail-label">Processed</span>
|
||||
<span class="q-job-detail-value" v-text="humanTime(job?.processedOn)"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" v-if="job?.finishedOn">
|
||||
<span class="q-job-detail-label">FINISHED</span>
|
||||
<span class="q-job-detail-label">Finished</span>
|
||||
<span class="q-job-detail-value" v-text="humanTime(job?.finishedOn)"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" v-if="job?.attemptsMade">
|
||||
<span class="q-job-detail-label">ATTEMPTS</span>
|
||||
<span class="q-job-detail-label">Attempts</span>
|
||||
<span class="q-job-detail-value" v-text="job?.attemptsMade"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" v-if="job?.progress && job?.progress?.status">
|
||||
<span class="q-job-detail-label">STATUS</span>
|
||||
<span class="q-job-detail-label">Status</span>
|
||||
<span class="q-job-detail-value" v-text="job?.progress?.status"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" v-if="jobProgressPct(job) !== null">
|
||||
<span class="q-job-detail-label">PROGRESS</span>
|
||||
<span class="q-job-detail-label">Progress</span>
|
||||
<span class="q-job-detail-value" v-text="jobProgressPct(job) + '%'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-job-detail-error" v-if="job?.failedReason">
|
||||
<span class="q-job-detail-label">ERROR</span>
|
||||
<span class="q-job-detail-label">Error</span>
|
||||
<div class="q-error-reason" v-text="job?.failedReason"></div>
|
||||
</div>
|
||||
<div v-if="job?.stacktrace?.length">
|
||||
<span class="q-job-detail-label">STACKTRACE</span>
|
||||
<span class="q-job-detail-label">Stacktrace</span>
|
||||
<pre class="q-error-stack" v-for="(stack, index) in job?.stacktrace" :key="index"><code v-text="stack"></code></pre>
|
||||
</div>
|
||||
<div class="q-job-detail-actions">
|
||||
<button class="btn btn-sm" v-if="job?._state == 'failed'" @click="retryJob(job)"><i class="fas fa-sync"></i> Retry</button>
|
||||
<button class="btn btn-sm" @click="removeJob(job)"><i class="fas fa-trash-alt"></i> Remove</button>
|
||||
<a class="btn btn-sm" target="_blank" :href="safeUrl("/r/" + (job?.id))"><i class="fas fa-external-link-alt"></i> View repo</a>
|
||||
<button class="btn btn-sm" v-if="job?._state == 'failed'" @click="retryJob(job)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Retry</button>
|
||||
<button class="btn btn-sm" @click="removeJob(job)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</button>
|
||||
<a class="btn btn-sm" target="_blank" :href="safeUrl("/r/" + (job?.id))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg> View repo</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -216,7 +216,7 @@
|
||||
</table>
|
||||
|
||||
<div class="paper-table-empty" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);" v-if="filteredJobs().length == 0">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg>
|
||||
<span v-if="!query?.search">No jobs in the {{ selectedQueue }} queue.</span>
|
||||
<span v-if="query?.search">No jobs match the current filters.</span>
|
||||
</div>
|
||||
|
||||
@@ -3,26 +3,26 @@
|
||||
<h1 class="paper-page-title">Repositories</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories" class="active"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary">
|
||||
<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>
|
||||
<button type="button" class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1" :aria-pressed="!!(query?.ready)">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></button>
|
||||
<button type="button" class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1" :aria-pressed="!!(query?.preparing)">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing') + statusCountFor('download')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1" :aria-pressed="!!(query?.error)">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1" :aria-pressed="!!(query?.expired)">Expired <span class="count">{{ fmt?.number(statusCountFor('expired') + statusCountFor('expiring')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle removed filter" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1" :aria-pressed="!!(query?.removed)">Removed <span class="count">{{ fmt?.number(statusCountFor('removed') + statusCountFor('removing')) }}</span></button>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ fetchError }}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8" v-form="viewState">
|
||||
@@ -35,11 +35,11 @@
|
||||
<span class="admin-filter-inline"><label>Owner</label><input type="text" class="form-control form-control-sm" placeholder="username" v-field="{ state: viewState, set: value => { query.owner = value }, value: query?.owner, form: null, options: {} }"></span>
|
||||
<span class="admin-filter-inline"><label>Conference</label><input type="text" class="form-control form-control-sm" placeholder="ID" v-field="{ state: viewState, set: value => { query.conference = value }, value: query?.conference, form: null, options: {} }"></span>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export</button>
|
||||
<span class="admin-filter-inline" aria-label="Pagination">
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1" aria-label="Previous page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg></button>
|
||||
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{ query?.page }}/{{ totalPage || 1 }}</span>
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage" aria-label="Next page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<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>
|
||||
<button type="button" aria-label="Remove filter" @click="clearFilter(chip.key)"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,8 +57,8 @@
|
||||
|
||||
<div class="bulk-bar" v-if="selectedCount() > 0">
|
||||
<span><strong>{{ selectedCount() }}</strong> selected</span>
|
||||
<button class="btn btn-sm" type="button" @click="bulkRefresh()"><i class="fas fa-sync"></i> Force refresh</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @click="bulkRefresh()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force refresh</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-broom"></use></svg> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @click="clearSelection()">Clear</button>
|
||||
</div>
|
||||
|
||||
@@ -67,10 +67,10 @@
|
||||
<div role="columnheader" style="width: 28px;">
|
||||
<input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
|
||||
</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <i class="fas" :class="sortIcon('source.repositoryName')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <i class="fas" :class="sortIcon('pageView')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <i class="fas" :class="sortIcon('anonymizeDate')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('source.repositoryName')" :class="{ascending: sortIcon('source.repositoryName') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('status')" :class="{ascending: sortIcon('status') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" class="num"><button type="button" class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('pageView')" :class="{ascending: sortIcon('pageView') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('anonymizeDate')" :class="{ascending: sortIcon('anonymizeDate') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error', 'row-selected': selected[repo?.repoId]}">
|
||||
@@ -78,11 +78,11 @@
|
||||
<input type="checkbox" aria-label="Select repository" v-field="{ state: viewState, set: value => { selected[repo.repoId] = value }, value: selected[repo?.repoId], form: null, options: {} }">
|
||||
</div>
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId))"></a>
|
||||
<div class="anon-sub">
|
||||
<a v-text="repo?.source?.repositoryName" :href="safeUrl("https://github.com/" + (repo?.source?.repositoryName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
|
||||
<a v-text="repo?.source?.repositoryName" :href="safeUrl("https://github.com/" + (repo?.source?.repositoryName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} {{ repo?.options?.terms?.length === 1 ? 'term' : 'terms' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,31 +93,31 @@
|
||||
</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" v-text="fmt?.number(repo?.pageView || 0)"></div>
|
||||
<div class="cell-expires" role="cell" v-text="fmt?.humanTime(repo?.anonymizeDate)"></div>
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> <span v-text="fmt?.number(repo?.pageView || 0)"></span></div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> <span v-text="fmt?.humanTime(repo?.anonymizeDate)"></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">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><i class="fa fa-eye"></i> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><i class="fas fa-globe"></i> View page</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-globe"></use></svg> 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="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="showStatusMessage(repo)"><i class="fas fa-exclamation-triangle"></i> View status message</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Enable</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="showStatusMessage(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> View status message</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-broom"></use></svg> Remove cache</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No repositories match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,12 +126,12 @@
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1">
|
||||
<i class="fas fa-chevron-left"></i> Previous
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg> Previous
|
||||
</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" aria-label="Page" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
<span>of {{ totalPage }}</span>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage">
|
||||
Next <i class="fas fa-chevron-right"></i>
|
||||
Next <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
<div class="container paper-page admin-page">
|
||||
<div class="paper-crumbs"><a href="/admin/users">Users</a> / <span class="here">{{ userInfo?.username || 'Profile' }}</span></div>
|
||||
<div class="paper-crumbs"><a href="/admin/">Admin</a> / <a href="/admin/users">Users</a> / <span class="here">{{ userInfo?.username || 'Profile' }}</span></div>
|
||||
<h1 class="paper-page-title">{{ userInfo?.username || 'User' }}</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users" class="active"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="user-detail-card" v-if="userInfo">
|
||||
<div class="user-header">
|
||||
<img width="56" height="56" v-if="userInfo?.photo" :src="safeUrl((userInfo?.photo))">
|
||||
<div>
|
||||
<h1>
|
||||
<h2>
|
||||
{{ userInfo?.username }}
|
||||
<span class="status-dot-wrap">
|
||||
<span class="status-dot" :class="{'status-ready': userInfo?.status == 'active', 'status-removed': userInfo?.status != 'active'}"></span>
|
||||
<span v-text="fmt?.title(userInfo?.status)"></span>
|
||||
</span>
|
||||
<span class="type-badge type-repo" v-if="userInfo?.isAdmin">Admin</span>
|
||||
</h1>
|
||||
</h2>
|
||||
<div class="user-actions" style="margin-top: 4px;">
|
||||
<button class="btn btn-sm text-danger" v-if="userInfo?.status !== 'banned'" @click="banUser()"><i class="fas fa-ban"></i> Ban</button>
|
||||
<button class="btn btn-sm" v-if="userInfo?.status === 'banned' || userInfo?.status === 'removed'" @click="activateUser()"><i class="fas fa-check-circle"></i> Activate</button>
|
||||
<button class="btn btn-sm" v-if="!userInfo?.isAdmin" @click="promoteUser()"><i class="fas fa-user-shield"></i> Promote to admin</button>
|
||||
<button class="btn btn-sm text-danger" v-if="userInfo?.isAdmin" @click="demoteUser()"><i class="fas fa-user-minus"></i> Remove admin</button>
|
||||
<button class="btn btn-sm text-danger" v-if="userInfo?.status !== 'banned'" @click="banUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-ban"></use></svg> Ban</button>
|
||||
<button class="btn btn-sm" v-if="userInfo?.status === 'banned' || userInfo?.status === 'removed'" @click="activateUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Activate</button>
|
||||
<button class="btn btn-sm" v-if="!userInfo?.isAdmin" @click="promoteUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user-shield"></use></svg> Promote to admin</button>
|
||||
<button class="btn btn-sm text-danger" v-if="userInfo?.isAdmin" @click="demoteUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user-minus"></use></svg> Remove admin</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +40,8 @@
|
||||
<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;" v-if="userInfo?.accessTokens?.github">{{ userInfo?.accessTokens?.github }}</div>
|
||||
<div class="detail-value text-muted" v-if="!userInfo?.accessTokens?.github">None stored</div>
|
||||
|
||||
<div class="detail-label">GitHub</div>
|
||||
<div class="detail-value">
|
||||
@@ -65,7 +66,7 @@
|
||||
{{ showRepos ? 'Hide' : 'Show' }}
|
||||
</button>
|
||||
<button class="btn btn-sm ml-1" @click="getGitHubRepositories()">
|
||||
<i class="fas fa-sync"></i> Refresh
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,13 +76,13 @@
|
||||
<div class="paper-table w-100" style="margin-top: 10px;">
|
||||
<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>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<span class="repo-name" v-text="repo?.name"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<i class="fas fa-database"></i> {{ fmt?.humanFileSize(repo?.size) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-database"></use></svg> {{ fmt?.humanFileSize(repo?.size) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,7 +90,7 @@
|
||||
</div>
|
||||
|
||||
<div class="admin-section-header" v-if="userInfo && userInfo?.isAdmin && user && user?.username == userInfo?.username">
|
||||
<h2><i class="fas fa-key"></i> API tokens</h2>
|
||||
<h2><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-key"></use></svg> API tokens</h2>
|
||||
<span class="section-count">{{ tokens?.length }}</span>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +99,7 @@
|
||||
|
||||
<form class="d-flex" style="gap: 8px; margin-bottom: 12px;" v-form="viewState" @submit.prevent="submitForm($event, () => { createToken() })">
|
||||
<input type="text" class="form-control" placeholder="Token name (e.g. dev-laptop)" required v-field="{ state: viewState, set: value => { tokenForm.name = value }, value: tokenForm?.name, form: null, options: {} }">
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-plus"></i> Generate</button>
|
||||
<button type="submit" class="btn btn-ink"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-plus"></use></svg> Generate</button>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-warning" role="alert" v-if="tokenForm?.plaintext">
|
||||
@@ -119,28 +120,28 @@
|
||||
<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" title="Revoke" @click="revokeToken(t)"><i class="fas fa-trash-alt"></i></button>
|
||||
<button class="btn btn-sm text-danger btn-icon" title="Revoke" aria-label="Revoke token" @click="revokeToken(t)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="!tokens?.length">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No tokens yet.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section-header">
|
||||
<h2><i class="fas fa-code-branch"></i> Anonymized repositories</h2>
|
||||
<h2><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Anonymized repositories</h2>
|
||||
<span class="section-count">{{ repositories?.length }}</span>
|
||||
</div>
|
||||
|
||||
<div class="admin-summary">
|
||||
<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>
|
||||
<button type="button" class="summary-pill ok" title="Toggle ready filter" :class="{active: filters?.status?.ready === false}" @click="filters.status.ready = !filters.status.ready" :aria-pressed="!!(filters?.status?.ready === false)">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></button>
|
||||
<button type="button" class="summary-pill warn" title="Toggle preparing filter" :class="{active: filters?.status?.preparing === false}" @click="filters.status.preparing = !filters.status.preparing" :aria-pressed="!!(filters?.status?.preparing === false)">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" title="Toggle errored filter" :class="{active: filters?.status?.error === false}" @click="filters.status.error = !filters.status.error" :aria-pressed="!!(filters?.status?.error === false)">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle expired filter" :class="{active: filters?.status?.expired === false}" @click="filters.status.expired = !filters.status.expired" :aria-pressed="!!(filters?.status?.expired === false)">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle removed filter" :class="{active: filters?.status?.removed === false}" @click="filters.status.removed = !filters.status.removed" :aria-pressed="!!(filters?.status?.removed === false)">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8" v-form="viewState">
|
||||
@@ -149,14 +150,14 @@
|
||||
<input type="search" class="form-control" aria-label="Search repositories" placeholder="Search repoId, source repo, error message…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="bulk-bar" v-if="selectedCount() > 0">
|
||||
<span><strong>{{ selectedCount() }}</strong> selected</span>
|
||||
<button class="btn btn-sm" type="button" @click="bulkRefresh()"><i class="fas fa-sync"></i> Force refresh</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @click="bulkRefresh()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force refresh</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-broom"></use></svg> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @click="clearSelection()">Clear</button>
|
||||
</div>
|
||||
|
||||
@@ -165,10 +166,10 @@
|
||||
<div role="columnheader" style="width: 28px;">
|
||||
<input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
|
||||
</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <i class="fas" :class="sortIcon('source.repositoryName')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <i class="fas" :class="sortIcon('pageView')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <i class="fas" :class="sortIcon('anonymizeDate')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('source.repositoryName')" :class="{ascending: sortIcon('source.repositoryName') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('status')" :class="{ascending: sortIcon('status') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" class="num"><button type="button" class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('pageView')" :class="{ascending: sortIcon('pageView') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('anonymizeDate')" :class="{ascending: sortIcon('anonymizeDate') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error', 'row-selected': selected[repo?.repoId]}">
|
||||
@@ -176,11 +177,11 @@
|
||||
<input type="checkbox" aria-label="Select repository" v-field="{ state: viewState, set: value => { selected[repo.repoId] = value }, value: selected[repo?.repoId], form: null, options: {} }">
|
||||
</div>
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId))"></a>
|
||||
<div class="anon-sub">
|
||||
<a v-text="repo?.source?.fullName" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
|
||||
<a v-text="repo?.source?.fullName" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} {{ repo?.options?.terms?.length === 1 ? 'term' : 'terms' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,26 +197,26 @@
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><i class="fa fa-eye"></i> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><i class="fas fa-globe"></i> View page</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-globe"></use></svg> 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="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="showStatusMessage(repo)"><i class="fas fa-exclamation-triangle"></i> View status message</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Enable</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="showStatusMessage(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> View status message</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-broom"></use></svg> Remove cache</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No repositories match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
<h1 class="paper-page-title">Users</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users" class="active"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</span>
|
||||
<span class="summary-pill ok" :class="{active: query?.status == 'active'}" @click="query.status = query.status == 'active' ? '' : 'active'; query.page = 1">Active <span class="count">{{ fmt?.number(statusCountFor('active')) }}</span></span>
|
||||
<span class="summary-pill error" :class="{active: query?.status == 'banned'}" @click="query.status = query.status == 'banned' ? '' : 'banned'; query.page = 1">Banned <span class="count">{{ fmt?.number(statusCountFor('banned')) }}</span></span>
|
||||
<span class="summary-pill" :class="{active: query?.status == 'removed'}" @click="query.status = query.status == 'removed' ? '' : 'removed'; query.page = 1">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></span>
|
||||
<button type="button" class="summary-pill ok" :class="{active: query?.status == 'active'}" @click="query.status = query.status == 'active' ? '' : 'active'; query.page = 1" :aria-pressed="!!(query?.status == 'active')">Active <span class="count">{{ fmt?.number(statusCountFor('active')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" :class="{active: query?.status == 'banned'}" @click="query.status = query.status == 'banned' ? '' : 'banned'; query.page = 1" :aria-pressed="!!(query?.status == 'banned')">Banned <span class="count">{{ fmt?.number(statusCountFor('banned')) }}</span></button>
|
||||
<button type="button" class="summary-pill" :class="{active: query?.status == 'removed'}" @click="query.status = query.status == 'removed' ? '' : 'removed'; query.page = 1" :aria-pressed="!!(query?.status == 'removed')">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ fetchError }}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Users" accept-charset="UTF-8" v-form="viewState">
|
||||
@@ -32,11 +32,11 @@
|
||||
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value => { query.role = value }, value: query?.role, form: null, options: {} }"><option value="">Any</option><option value="admin">Admin</option></select>
|
||||
</span>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export</button>
|
||||
<span class="admin-filter-inline" aria-label="Pagination">
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1" aria-label="Previous page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg></button>
|
||||
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{ query?.page }}/{{ totalPage || 1 }}</span>
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage" aria-label="Next page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<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>
|
||||
<button type="button" @click="clearFilter(chip.key)" aria-label="Clear filter"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<div class="bulk-bar" v-if="selectedCount() > 0">
|
||||
<span><strong>{{ selectedCount() }}</strong> selected</span>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkBan()"><i class="fas fa-ban"></i> Ban</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkBan()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-ban"></use></svg> Ban</button>
|
||||
<button class="btn btn-sm" type="button" @click="selected = {}; allSelected = false">Clear</button>
|
||||
</div>
|
||||
|
||||
@@ -62,9 +62,9 @@
|
||||
<div role="columnheader" style="width: 28px;">
|
||||
<input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
|
||||
</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'username'}" @click="sortBy('username')">User <i class="fas" :class="sortIcon('username')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'username'}" @click="sortBy('username')">User <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('username')" :class="{ascending: sortIcon('username') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" class="num">Repos</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('status')" :class="{ascending: sortIcon('status') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader">Role</div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
@@ -77,12 +77,12 @@
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="u?.username" :href="safeUrl("/admin/users/" + (u?.username))"></a>
|
||||
<div class="anon-sub">
|
||||
<span v-if="u?.emails[0].email">{{ u?.emails[0].email }}</span><span v-if="u?.emails[0].email"> · </span><a target="_blank" :href="safeUrl("https://github.com/" + (u?.username))"><i class="fab fa-github"></i> {{ u?.username }}</a><span v-if="u?.dateOfEntry"> · Joined {{ fmt?.humanTime(u?.dateOfEntry) }}</span>
|
||||
<span v-if="u?.emails?.[0]?.email">{{ u?.emails?.[0]?.email }} · </span><a target="_blank" :href="safeUrl("https://github.com/" + (u?.username))"><i class="fab fa-github"></i> {{ u?.username }}</a><span v-if="u?.dateOfEntry"> · Joined {{ fmt?.humanTime(u?.dateOfEntry) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell">
|
||||
<a title="Show this user's repositories" v-text="fmt?.number((u?.repoCount || 0))" :href="safeUrl("/admin/?owner=" + (u?.username))"></a>
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg>
|
||||
<a title="Show this user's repositories" v-text="fmt?.number((u?.repoCount || 0))" :href="safeUrl("/admin/repositories?owner=" + (u?.username))"></a>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<span class="status-dot" :class="{'status-ready': u?.status == 'active', 'status-removed': u?.status == 'removed' || u?.status == 'banned'}"></span>
|
||||
@@ -95,20 +95,20 @@
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/users/" + (u?.username))"><i class="far fa-eye"></i> View details</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/?owner=" + (u?.username))"><i class="fas fa-code-branch"></i> View repositories</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/users/" + (u?.username))"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View details</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/repositories?owner=" + (u?.username))"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> View repositories</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="u?.status == 'active'" @click.prevent="banUser(u)"><i class="fas fa-ban"></i> Ban</a>
|
||||
<a class="dropdown-item" href="#" v-show="u?.status == 'removed' || u?.status == 'banned'" @click.prevent="activateUser(u)"><i class="fas fa-check-circle"></i> Activate</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="u?.status == 'active'" @click.prevent="banUser(u)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-ban"></use></svg> Ban</a>
|
||||
<a class="dropdown-item" href="#" v-show="u?.status == 'removed' || u?.status == 'banned'" @click.prevent="activateUser(u)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Activate</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredUsers?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No users match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,10 +116,10 @@
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i> Previous</button>
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
<span>of {{ totalPage }}</span>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage">Next <i class="fas fa-chevron-right"></i></button>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage">Next <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
<label>Per page</label>
|
||||
|
||||
@@ -1,49 +1,46 @@
|
||||
<div class="anonymize-page h-100">
|
||||
<div class="anonymize-page h-100" :class="{'mobile-preview-active': mobilePane === 'preview'}">
|
||||
<!-- ===== STATE 1: No URL — centered input ===== -->
|
||||
<div class="anonymize-landing" v-show="!(sourceUrl)">
|
||||
<div class="anonymize-landing" v-show="!workspaceOpen && !isUpdate">
|
||||
<div class="anonymize-landing-inner">
|
||||
<div class="paper-crumbs">My work / <span class="here">New anonymization</span></div>
|
||||
<h1 class="paper-page-title">New <em>anonymization</em></h1>
|
||||
<p class="paper-page-lede">
|
||||
Paste a GitHub repository, pull-request, or gist URL. We’ll fetch
|
||||
it, strip every trace of identity, and hand you back a stable link.
|
||||
it, redact the terms you choose in text files, and give you a share link.
|
||||
Images and PDFs are served unchanged when enabled. Review them before sharing.
|
||||
</p>
|
||||
<div class="form-group mt-4 mb-2">
|
||||
<div class="source-url-label-row">
|
||||
<label class="paper-field-label" for="sourceUrl-landing">Source URL</label>
|
||||
<aside class="repo-access" aria-label="GitHub access" v-if="!sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
||||
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate && detectedType !== 'gist'">
|
||||
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><i class="fas fa-shield-alt" aria-hidden="true"></i> <span class="sr-only">Read-only </span>GitHub App</button>
|
||||
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> <span class="sr-only">Read-only </span>GitHub App</button>
|
||||
<button type="button" :aria-pressed="githubConnection === 'oauth'" :disabled="githubConnection === 'oauth'" @click="chooseGitHubConnection('oauth')">Legacy OAuth</button>
|
||||
</div>
|
||||
<span class="connection-badge" v-if="isUpdate || detectedType === 'gist'">{{ detectedType !== 'gist' && githubConnection === 'github-app' ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
|
||||
<span class="connection-badge" v-if="isUpdate || detectedType === 'gist'">{{ (detectedType === 'gist' ? githubConnections?.gistConnection === 'github-app' : githubConnection === 'github-app') ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
|
||||
</aside>
|
||||
</div>
|
||||
<input id="sourceUrl-landing" type="text" class="form-control form-control-lg" placeholder="https://github.com/owner/repository" v-field="{ state: viewState, set: value => { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }">
|
||||
</div>
|
||||
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
|
||||
<p class="text-danger" role="alert" v-if="sourceUrlError">{{ sourceUrlError }}</p>
|
||||
<small class="form-text" style="color: var(--ink-muted);">
|
||||
Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted.
|
||||
</small>
|
||||
<aside class="alert alert-info" role="status" v-if="detectedType === 'gist' && gistOAuthRequired">
|
||||
<p>Connect GitHub OAuth to access gists. Your account and existing anonymizations stay the same, and your draft will be restored when you return.</p>
|
||||
<p>GitHub's legacy OAuth connection includes write access to private repositories.</p>
|
||||
<button type="button" class="btn btn-ink" @click="connectGistOAuth()" v-if="githubConnections?.oauthEnabled">Connect GitHub OAuth to access gists</button>
|
||||
<p v-else>GitHub OAuth is currently unavailable. Please try again later.</p>
|
||||
</aside>
|
||||
<aside class="repo-access-details" aria-label="GitHub access" v-if="!sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
||||
<div class="repo-access-main">
|
||||
<div class="repo-access-controls">
|
||||
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <i class="fas fa-external-link-alt" aria-hidden="true"></i></button>
|
||||
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">{{ isUpdate ? 'Change connection' : 'Manage connections' }} →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-access-note"><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Select repositories on GitHub, then return here.</span><span><i class="fas fa-info-circle" aria-hidden="true"></i> Selected content will be published, including content from private repositories.</span></div>
|
||||
<div class="repo-access-note"><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Select repositories on GitHub, then return here.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Selected content will be published, including content from private repositories.</span></div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== STATE 2: URL provided — form (left) + preview (right) ===== -->
|
||||
<div class="anonymize-workspace" v-show="sourceUrl">
|
||||
<div class="anonymize-workspace" v-show="workspaceOpen || isUpdate">
|
||||
<header class="anonymize-topbar">
|
||||
<div class="anonymize-topbar-inner">
|
||||
<div class="paper-crumbs">
|
||||
@@ -62,9 +59,25 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<aside class="alert alert-info" role="status" v-if="detectedType === 'gist' && gistAppRequired">
|
||||
<p>Reconnect your GitHub account to read this gist. Your draft will be restored when you return.</p>
|
||||
<button type="button" class="btn btn-ink" @click="connectGistApp()">Connect GitHub App to access gists</button>
|
||||
</aside>
|
||||
<aside class="alert alert-info" role="status" v-if="detectedType === 'gist' && gistOAuthRequired">
|
||||
<p>Connect GitHub OAuth to access gists. Your account and existing anonymizations stay the same, and your draft will be restored when you return.</p>
|
||||
<p>GitHub's legacy OAuth connection includes write access to private repositories.</p>
|
||||
<button type="button" class="btn btn-ink" @click="connectGistOAuth()" v-if="githubConnections?.oauthEnabled">Connect GitHub OAuth to access gists</button>
|
||||
<p v-else>GitHub OAuth is currently unavailable. Please try again later.</p>
|
||||
</aside>
|
||||
|
||||
<nav class="anonymize-mobile-tabs" role="tablist" aria-label="Anonymization workspace">
|
||||
<button id="workspace-settings-tab" role="tab" type="button" :aria-selected="mobilePane === 'settings'" aria-controls="workspace-settings" :tabindex="mobilePane === 'settings' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="selectMobilePane('settings')">Settings</button>
|
||||
<button id="workspace-preview-tab" role="tab" type="button" :aria-selected="mobilePane === 'preview'" aria-controls="workspace-preview" :tabindex="mobilePane === 'preview' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="selectMobilePane('preview')">Reviewer preview</button>
|
||||
</nav>
|
||||
<div class="anonymize-preview-empty" id="workspace-preview" v-if="detectedType !== 'repo' && !details" role="status">{{ sourceLoading ? 'Loading source…' : 'Choose an accessible source to see its preview.' }}</div>
|
||||
<div class="anonymize-split">
|
||||
<!-- Form column (left) -->
|
||||
<div class="anonymize-form-col overflow-auto">
|
||||
<div id="workspace-settings" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-settings-tab' : null" class="anonymize-form-col overflow-auto">
|
||||
<form class="form needs-validation paper-settings-main" name="anonymize" novalidate v-form="viewState">
|
||||
|
||||
<section class="paper-settings-section">
|
||||
@@ -75,10 +88,10 @@
|
||||
<label class="paper-field-label" for="sourceUrl">GitHub URL</label>
|
||||
<aside class="repo-access" aria-label="GitHub access" v-if="sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
||||
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate && detectedType !== 'gist'">
|
||||
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><i class="fas fa-shield-alt" aria-hidden="true"></i> <span class="sr-only">Read-only </span>GitHub App</button>
|
||||
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> <span class="sr-only">Read-only </span>GitHub App</button>
|
||||
<button type="button" :aria-pressed="githubConnection === 'oauth'" :disabled="githubConnection === 'oauth'" @click="chooseGitHubConnection('oauth')">Legacy OAuth</button>
|
||||
</div>
|
||||
<span class="connection-badge" v-if="isUpdate || detectedType === 'gist'">{{ detectedType !== 'gist' && githubConnection === 'github-app' ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
|
||||
<span class="connection-badge" v-if="isUpdate || detectedType === 'gist'">{{ (detectedType === 'gist' ? githubConnections?.gistConnection === 'github-app' : githubConnection === 'github-app') ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
|
||||
</aside>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="sourceUrl" id="sourceUrl" :disabled="isUpdate && detectedType !== 'repo'" placeholder="Paste a GitHub repo or pull request URL" v-field="{ state: viewState, set: value => { sourceUrl = value }, value: sourceUrl, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }" :class="{'is-invalid': anonymize?.sourceUrl?.invalid}">
|
||||
@@ -99,11 +112,17 @@
|
||||
<aside class="repo-access-details" aria-label="GitHub access" v-if="sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
||||
<div class="repo-access-main">
|
||||
<div class="repo-access-controls">
|
||||
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <i class="fas fa-external-link-alt" aria-hidden="true"></i></button>
|
||||
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">{{ isUpdate ? 'Change connection' : 'Manage connections' }} →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-access-note"><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Select repositories on GitHub, then return here.</span><span><i class="fas fa-info-circle" aria-hidden="true"></i> Selected content will be published, including content from private repositories.</span></div>
|
||||
<div class="repo-access-note"><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Select repositories on GitHub, then return here.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Selected content will be published, including content from private repositories.</span></div>
|
||||
</aside>
|
||||
|
||||
<aside class="repo-access-details" v-if="isUpdate && detectedType === 'repo' && role === 'owner' && sourceUrl === 'https://github.com/' + _originalFullName">
|
||||
<p>If you deleted and recreated this repository, reconnect it before choosing a branch.</p>
|
||||
<button type="button" class="btn btn-outline-ink" @click="reconnectSource()">Reconnect source repository</button>
|
||||
<p v-if="reconnectRepositoryId" role="status">Source reloaded. Review the branch, commit, and anonymization preview, then save to replace the published snapshot.</p>
|
||||
</aside>
|
||||
|
||||
<div class="form-grid-2" v-if="detectedType === 'repo'">
|
||||
@@ -114,8 +133,8 @@
|
||||
<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" title="Refresh" data-toggle="tooltip" data-placement="bottom" @click="getBranches(true)">
|
||||
<i class="fas fa-sync" aria-hidden="true"></i>
|
||||
<button class="btn btn-icon" type="button" title="Refresh" data-toggle="tooltip" data-placement="bottom" @click="getBranches(true)" aria-label="Refresh">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,10 +220,12 @@
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value => { options.image = value }, value: options?.image, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="image">Display images</label>
|
||||
<small class="form-text text-muted">Images are served unchanged and may contain names or affiliations.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value => { options.pdf = value }, value: options?.pdf, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="pdf">Display PDFs</label>
|
||||
<small class="form-text text-muted">PDF contents and metadata are served unchanged. <a href="/faq#formats">About supported formats</a></small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.notebook = value }, value: options?.notebook, form: "anonymize", options: {} }">
|
||||
@@ -213,10 +234,10 @@
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value => { options.page = value }, value: options?.page, form: "anonymize", options: {} }" :disabled="!details?.hasPage || (details?.pageSource && details?.pageSource?.branch !== source?.branch)">
|
||||
<label class="form-check-label" for="page">GitHub Pages</label>
|
||||
<small class="form-text text-muted d-block" v-show="!details?.hasPage">
|
||||
<small class="form-text text-muted" v-if="!details?.hasPage">
|
||||
{{ fmt?.translate('WARNINGS.page_not_enabled_on_repo') }}
|
||||
</small>
|
||||
<small class="form-text text-muted d-block" v-show="details?.hasPage && details?.pageSource && details?.pageSource?.branch !== source?.branch">
|
||||
<small class="form-text text-muted" v-if="details?.hasPage && details?.pageSource?.branch && source?.branch && details.pageSource.branch !== source.branch">
|
||||
{{ fmt?.translate('WARNINGS.page_branch_mismatch', { pageBranch: details?.pageSource?.branch, selectedBranch: source?.branch }) }}
|
||||
</small>
|
||||
</div>
|
||||
@@ -329,8 +350,8 @@
|
||||
<img alt="" style="width: 24px; height: 24px; border-radius: 50%;" v-if="c?.photo" :src="safeUrl((c?.photo))">
|
||||
<a target="_blank" v-text="c?.username" :href="safeUrl("https://github.com/" + (c?.username))"></a>
|
||||
<span class="type-badge type-coauthor">Co-author</span>
|
||||
<button type="button" class="btn btn-sm" style="margin-left: auto;" title="Remove co-author" v-show="role === 'owner' || role === 'admin'" @click="removeCoauthor(c)">
|
||||
<i class="fas fa-times"></i>
|
||||
<button type="button" class="btn btn-sm btn-icon" style="margin-left: auto;" title="Remove co-author" v-show="role === 'owner' || role === 'admin'" @click="removeCoauthor(c)" aria-label="Remove co-author">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text text-muted" v-show="!coauthors || coauthors?.length === 0">
|
||||
@@ -341,44 +362,61 @@
|
||||
|
||||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||||
|
||||
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
|
||||
<div class="draft-status" v-else-if="unsavedChanges">{{ draftSaved ? "Draft saved in this tab for 30 minutes." : "Unsaved changes. Keep this tab open until you save." }} <button type="button" class="draft-discard" @click="discardDraft()">{{ isUpdate ? "Restore saved settings" : "Discard changes" }}</button></div>
|
||||
<p class="anonymize-review-note" v-if="detectedType === 'repo' && (options.image || options.pdf)">Review enabled images and PDFs before sharing. Their contents are not anonymized.</p>
|
||||
<button type="button" class="btn btn-outline-ink mobile-preview-jump" @click="selectMobilePane('preview')">Review preview <span aria-hidden="true">→</span></button>
|
||||
<div class="anonymize-submit-bar" v-show="detectedType">
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'repo' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize Repository
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'repo' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Repository
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'repo' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update Repository
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'repo' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Repository
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'pr' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize Pull Request
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'pr' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Pull Request
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'pr' && isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update Pull Request
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'pr' && isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Pull Request
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'gist' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize Gist
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'gist' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Gist
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'gist' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update Gist
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'gist' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Gist
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Preview column (right) -->
|
||||
<div class="anonymize-preview-col" v-if="detectedType === 'repo' && html_readme">
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'repo'">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">README with redactions applied</span>
|
||||
<span class="anonymize-preview-sub">{{ previewStatus === 'ready' ? 'README with redactions applied' : 'README preview' }}</span>
|
||||
</div>
|
||||
<div class="anonymize-preview-body markdown-body body" v-html="sanitize(html_readme)"></div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
||||
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-status" v-if="!sourceLoading && !readme && previewStatus === 'idle'">No README is available to preview. Review the published files before sharing.</div>
|
||||
<div class="anonymize-preview-body markdown-body body" v-if="previewStatus === 'ready'" v-html="sanitize(html_readme)"></div>
|
||||
</div>
|
||||
|
||||
<div class="anonymize-preview-col" v-if="detectedType === 'gist' && details">
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'gist' && details">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">Gist with redactions applied</span>
|
||||
<span class="anonymize-preview-sub">{{ previewStatus === 'ready' ? 'Gist with redactions applied' : 'Gist preview' }}</span>
|
||||
</div>
|
||||
<div class="anonymize-preview-body">
|
||||
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
||||
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-body" v-if="previewStatus === 'ready'">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span v-if="options?.title">{{ anonymizeGistContent(details?.gist?.description) || 'Untitled gist' }}</span>
|
||||
@@ -407,7 +445,7 @@
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.gist?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<i class="far fa-user"></i> @<span v-text="anonymizeGistContent(comment?.author)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizeGistContent(comment?.author)"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
@@ -420,12 +458,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="anonymize-preview-col" v-if="detectedType === 'pr' && details">
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'pr' && details">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">Pull request with redactions applied</span>
|
||||
<span class="anonymize-preview-sub">{{ previewStatus === 'ready' ? 'Pull request with redactions applied' : 'Pull request preview' }}</span>
|
||||
</div>
|
||||
<div class="anonymize-preview-body">
|
||||
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
||||
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-body" v-if="previewStatus === 'ready'">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span v-if="options?.title">{{ anonymizePrContent(details?.pullRequest?.title) }}</span>
|
||||
@@ -440,24 +484,25 @@
|
||||
<markdown :content="anonymizePrContent(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
<nav class="paper-tabs" role="tablist" v-if="options?.diff || options?.comments">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.diff" :class="{'active': prTabState?.active == 'diff'}" @click="prTabState.active = 'diff'">
|
||||
<i class="fas fa-code"></i> Diff
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.diff" :class="{'active': prTabState?.active == 'diff'}" id="preview-pr-diff-tab" :aria-selected="prTabState?.active === 'diff'" aria-controls="preview-pr-diff-panel" :tabindex="prTabState?.active === 'diff' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = 'diff'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Diff
|
||||
</button>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.comments" :class="{'active': prTabState?.active == 'comments'}" @click="prTabState.active = 'comments'">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.comments" :class="{'active': prTabState?.active == 'comments'}" id="preview-pr-comments-tab" :aria-selected="prTabState?.active === 'comments'" aria-controls="preview-pr-comments-panel" :tabindex="prTabState?.active === 'comments' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = 'comments'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="anonymize-preview-status" v-if="!options?.diff && !options?.comments">Diff and comments are hidden by your display settings.</div>
|
||||
<div class="paper-tab-content">
|
||||
<div v-if="options?.diff && prTabState?.active == 'diff'">
|
||||
<div role="tabpanel" id="preview-pr-diff-panel" aria-labelledby="preview-pr-diff-tab" v-if="options?.diff && prTabState?.active == 'diff'">
|
||||
<div class="pr-diff" v-html="sanitize(fmt?.diff(anonymizePrContent(details?.pullRequest?.diff)))"></div>
|
||||
</div>
|
||||
<div v-if="options?.comments && prTabState?.active == 'comments'">
|
||||
<div role="tabpanel" id="preview-pr-comments-panel" aria-labelledby="preview-pr-comments-tab" v-if="options?.comments && prTabState?.active == 'comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.pullRequest?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<i class="far fa-user"></i> @<span v-text="anonymizePrContent(comment?.author)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizePrContent(comment?.author)"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
|
||||
<div class="paper-section-eyebrow anonymize-section-title">
|
||||
<i class="fas fa-chalkboard-teacher"></i> Conference ID
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conference ID
|
||||
</div>
|
||||
<!-- Conference -->
|
||||
<div class="form-group">
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
|
||||
<div class="paper-section-eyebrow anonymize-section-title">
|
||||
<i class="fas fa-shield-alt"></i> Anonymization Options
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymization Options
|
||||
</div>
|
||||
<!-- Pull Request ID -->
|
||||
<div class="form-group">
|
||||
@@ -112,7 +112,7 @@
|
||||
<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">
|
||||
<i class="fas fa-cog mr-1"></i> Advanced options
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-filter"></use></svg> Advanced options
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -169,10 +169,10 @@
|
||||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||||
<div class="anonymize-submit-bar" v-show="pullRequestUrl">
|
||||
<button id="submit" type="submit" class="btn btn-ink btn-block" v-if="!isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize
|
||||
</button>
|
||||
<button id="submit" type="submit" class="btn btn-ink btn-block" v-if="isUpdate" @click.prevent="submitForm($event, () => { updatePullRequest($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container paper-page">
|
||||
<div class="container paper-page claim-page">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">Claim</span></div>
|
||||
<h1 class="paper-page-title">Claim an <em>anonymization</em></h1>
|
||||
<p class="paper-page-lede">
|
||||
@@ -10,14 +10,14 @@
|
||||
<form class="form needs-validation" name="claimForm" novalidate v-form="viewState" @submit.prevent="submitForm($event, () => { claim() })">
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="repoUrl">GitHub repository URL</label>
|
||||
<input type="text" class="form-control" name="repoUrl" id="repoUrl" required v-field="{ state: viewState, set: value => { repoUrl = value }, value: repoUrl, form: "claimForm", options: {} }" :class="{'is-invalid': claimForm?.repoUrl?.invalid && (claimForm?.repoUrl?.touched || claimForm?.submitted)}">
|
||||
<input type="text" class="form-control" name="repoUrl" id="repoUrl" placeholder="https://github.com/owner/repository" required v-field="{ state: viewState, set: value => { repoUrl = value }, value: repoUrl, form: "claimForm", options: {} }" :class="{'is-invalid': claimForm?.repoUrl?.invalid && (claimForm?.repoUrl?.touched || claimForm?.submitted)}">
|
||||
<div class="invalid-feedback" v-show="claimForm?.repoUrl?.errors?.not_found">
|
||||
No anonymization matches this repository and ID. Check both values and that you can access the repository on GitHub.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="repoId">Anonymized repository ID</label>
|
||||
<input type="text" class="form-control" name="repoId" id="repoId" required v-field="{ state: viewState, set: value => { repoId = value }, value: repoId, form: "claimForm", options: {} }" :class="{'is-invalid': claimForm?.repoId?.invalid && (claimForm?.repoId?.touched || claimForm?.submitted)}">
|
||||
<input type="text" class="form-control" name="repoId" id="repoId" placeholder="e.g. my-paper-7F2A" aria-describedby="idHelp" required v-field="{ state: viewState, set: value => { repoId = value }, value: repoId, form: "claimForm", options: {} }" :class="{'is-invalid': claimForm?.repoId?.invalid && (claimForm?.repoId?.touched || claimForm?.submitted)}">
|
||||
<small id="idHelp" class="form-text text-muted">The ID is the last part of the anonymized URL: <code>anonymous.4open.science/r/<ID></code>.</small>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container page paper-page">
|
||||
<div class="container page paper-page conference-page">
|
||||
<div>
|
||||
<div class="paper-crumbs">
|
||||
<a href="/dashboard">My work</a> / <a href="/conferences">Conferences</a> / <span class="here">{{ conference?.conferenceID }}</span>
|
||||
@@ -15,7 +15,7 @@
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + conference?.status"></span>
|
||||
<span v-text="fmt?.statusLabel(conference?.status)"></span>
|
||||
</span>
|
||||
<a class="btn btn-outline-ink" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")"><i class="far fa-edit mr-1" aria-hidden="true"></i> Edit conference</a>
|
||||
<a class="btn btn-outline-ink" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit conference</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-meta-rule">
|
||||
@@ -99,7 +99,7 @@
|
||||
</div>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :key="repo?.repoId" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error'}">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId) + "/")"></a>
|
||||
<div class="anon-sub">
|
||||
@@ -116,8 +116,8 @@
|
||||
</div>
|
||||
<div class="status-sub" v-if="repo?.anonymizeDate">anonymized {{ fmt?.humanTime(repo?.anonymizeDate) }}</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView)"></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> <span v-text="fmt?.number(repo?.pageView)"></span></div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
<span class="expires-never" v-if="repo?.status == 'ready' && (repo?.options?.expirationMode === 'never' || !repo?.options?.expirationDate)">Never</span>
|
||||
<span v-if="repo?.status == 'ready' && repo?.options?.expirationMode !== 'never' && repo?.options?.expirationDate" v-text="fmt?.humanTime(repo?.options?.expirationDate)"></span>
|
||||
<span class="expires-past" v-if="repo?.status == 'expired'">Expired<span v-if="repo?.options?.expirationDate"> {{ fmt?.humanTime(repo?.options?.expirationDate) }}</span></span>
|
||||
@@ -126,21 +126,21 @@
|
||||
<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)">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View
|
||||
</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
|
||||
<i class="fas fa-inbox" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<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">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container page paper-page">
|
||||
<div class="container page paper-page conferences-page">
|
||||
<div class="row">
|
||||
<div class="w-100">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">Conferences</span></div>
|
||||
@@ -8,7 +8,7 @@
|
||||
<p class="paper-page-lede">Group anonymizations by venue, give chairs a shared dashboard, and set one expiry for every submission.</p>
|
||||
</div>
|
||||
<a href="/conference/new" class="btn btn-ink">
|
||||
<i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New conference
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-plus"></use></svg> New conference
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conference-feedback" role="alert" v-if="error"><span>{{ error }}</span><button type="button" class="btn btn-outline-ink" @click="loadConferences()">Retry</button></div>
|
||||
<div class="conference-feedback" role="status" v-if="loading">Loading conferences…</div>
|
||||
<div class="paper-table paper-table-conferences w-100" role="table" aria-label="Conferences">
|
||||
<div class="paper-table-head" role="row">
|
||||
<div role="columnheader">Conference</div>
|
||||
@@ -80,7 +82,7 @@
|
||||
</div>
|
||||
<div class="paper-table-row row-clickable" role="row" v-for="(conference, index) in filteredConferences" :key="conference?.conferenceID" :class="{'repo-inactive': conference?.status == 'expired' || conference?.status == 'removed'}">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Conf</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Conference"><use href="#project-icon-conference"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="conference?.name" :href="safeUrl("/conference/" + (conference?.conferenceID))"></a>
|
||||
<div class="anon-sub">
|
||||
@@ -94,40 +96,40 @@
|
||||
<span class="status-word" v-text="fmt?.statusLabel(conference?.status)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(conference?.nbRepositories || 0)"></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> <span v-text="fmt?.number(conference?.nbRepositories || 0)"></span></div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
<span v-if="conference?.startDate">{{ fmt?.date(conference?.startDate, 'mediumDate') }} – {{ fmt?.date(conference?.endDate, 'mediumDate') }}</span>
|
||||
<span class="empty-dash" aria-label="No review window" v-if="!conference?.startDate">—</span>
|
||||
</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :aria-label=""Actions for " + (conference?.name)">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :disabled="conference?.removing" :aria-label=""Actions for " + (conference?.name)">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/")">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View
|
||||
</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit
|
||||
</a>
|
||||
<div v-if="conference?.status != 'removed'">
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="button" class="dropdown-item dropdown-item-danger" @click="removeConference(conference)">
|
||||
<i class="fas fa-trash-alt" aria-hidden="true"></i> Remove
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredConferences?.length == 0">
|
||||
<i class="fas fa-inbox" aria-hidden="true"></i>
|
||||
<div class="paper-table-empty" v-if="!loading && !error && filteredConferences?.length == 0">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span v-if="conferences?.length == 0">You have not created a conference yet.</span>
|
||||
<span v-if="conferences?.length > 0">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions">
|
||||
<button type="button" class="btn btn-outline-ink" v-if="conferences?.length > 0" @click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
|
||||
<a href="/conference/new" class="btn btn-ink" v-if="conferences?.length == 0"><i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New conference</a>
|
||||
<a href="/conference/new" class="btn btn-ink" v-if="conferences?.length == 0"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-plus"></use></svg> New conference</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,46 +12,46 @@
|
||||
<section v-if="connections.appEnabled">
|
||||
<div class="connection-top"><i class="fab fa-github" aria-hidden="true"></i><span class="connection-badge">Recommended</span></div>
|
||||
<div class="connection-title"><h2>GitHub App</h2><span class="connection-status" :class="{'is-connected': connections.appConnected && !connections.appError}">{{ connections.appError ? 'Needs attention' : connections.appConnected ? 'Connected' : 'Not connected' }}</span></div>
|
||||
<p>Read-only access to the repositories you choose.</p>
|
||||
<p>Read-only access to gists and the repositories you choose.</p>
|
||||
<p class="paper-inline-alert paper-inline-alert-error" v-if="connections.appError" role="alert">{{ connections.appError }}. Reconnect to restore access.</p>
|
||||
<div class="connection-actions">
|
||||
<a class="btn btn-ink" v-if="connections.appConnected && !connections.appError" target="_self" href="/github/app/install">Manage repository access <i class="fas fa-external-link-alt" aria-hidden="true"></i></a>
|
||||
<a class="btn btn-ink" v-if="connections.appConnected && !connections.appError" target="_self" href="/github/app/install">Manage repository access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
|
||||
<a :class="connections.appConnected && !connections.appError ? 'connection-link' : 'btn btn-ink'" target="_self" href="/github/app/login">{{ connections.appConnected ? 'Reconnect account' : 'Connect GitHub account' }}</a>
|
||||
</div>
|
||||
<p class="connection-help">Choose repositories on GitHub. Organization access may need an administrator's approval.</p>
|
||||
<ul class="connection-installations" v-if="connections.installations?.length">
|
||||
<li v-for="installation in connections.installations" :key="installation.id"><div><strong>{{ installation.account }}</strong><span v-if="installation.suspended"> · Suspended</span></div><a target="_self" :href="'/github/app/install?installationId=' + installation.id">Manage access <span class="sr-only">for {{ installation.account }}</span> →</a></li>
|
||||
</ul>
|
||||
<button class="connection-link" :disabled="busy" @click="loadConnections()"><i class="fas fa-sync-alt" aria-hidden="true"></i> Refresh access</button>
|
||||
<button class="connection-link" :disabled="busy" @click="loadConnections()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Refresh access</button>
|
||||
</section>
|
||||
<section>
|
||||
<div class="connection-top"><i class="fas fa-key" aria-hidden="true"></i><span class="connection-badge connection-badge-muted">Legacy</span></div>
|
||||
<div class="connection-top"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-key"></use></svg><span class="connection-badge connection-badge-muted">Legacy</span></div>
|
||||
<div class="connection-title"><h2>GitHub OAuth</h2><span class="connection-status" :class="{'is-connected': connections.oauthConnected}">{{ connections.oauthConnected ? 'Connected' : 'Not connected' }}</span></div>
|
||||
<p>For existing OAuth anonymizations and gists.</p>
|
||||
<p>For existing OAuth anonymizations.</p>
|
||||
<p class="connection-help">This connection uses GitHub's broader repository permissions. Existing work keeps using it until you switch below.</p>
|
||||
<a v-if="connections.oauthEnabled" target="_self" href="/github/login" class="btn btn-outline-ink">{{ connections.oauthConnected ? 'Reconnect OAuth' : 'Connect legacy OAuth' }}</a>
|
||||
<div class="connection-revoke" v-if="connections.oauthConnected">
|
||||
<p v-if="connections.gistCount">{{ connections.gistCount }} gist(s) still require OAuth.</p>
|
||||
<p>Switch all dependent repositories before revoking access. Gists still require OAuth.</p>
|
||||
<button v-if="connections.appConnected" class="connection-link connection-danger" :disabled="busy || connections.gistCount || connections.resources.some(r => r.connection === 'oauth')" @click="disconnectOAuth()">Revoke OAuth access</button>
|
||||
<p v-if="connections.gistCount">{{ connections.gistCount }} gist(s) use your GitHub App connection when available.</p>
|
||||
<p>Switch all dependent repositories before revoking access. Gists can use your GitHub App connection.</p>
|
||||
<button v-if="connections.appConnected" class="connection-link connection-danger" :disabled="busy || connections.resources.some(r => r.connection === 'oauth')" @click="disconnectOAuth()">Revoke OAuth access</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<section class="connection-resources">
|
||||
<div class="connection-title"><h2>Repository connections</h2><span class="connection-count">{{ connections.resources?.length || 0 }} resources</span></div>
|
||||
<p>Check access, then switch each resource to the GitHub App.</p>
|
||||
<div class="connection-empty" v-if="!connections.resources?.length"><i class="fas fa-code-branch" aria-hidden="true"></i><div><h3>No repository connections yet</h3><p>Your repositories and pull requests will appear here once you anonymize them.</p></div><a class="connection-link" href="/anonymize">New anonymization →</a></div>
|
||||
<p>Check access, then switch each resource to the GitHub App. After reinstalling the App, reconnect existing resources here to keep their anonymous URLs and published snapshots.</p>
|
||||
<div class="connection-empty" v-if="!connections.resources?.length"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg><div><h3>No repository connections yet</h3><p>Your repositories and pull requests will appear here once you anonymize them.</p></div><a class="connection-link" href="/anonymize">New anonymization →</a></div>
|
||||
<div class="connection-resource" v-for="resource in connections.resources" :key="resource.type + resource.id">
|
||||
<div class="connection-resource-name"><strong>{{ resource.name || resource.id }}</strong><small>{{ resource.type }} · {{ resource.id }}</small></div>
|
||||
<span class="connection-badge" :class="{'connection-badge-muted': resource.connection === 'oauth'}">{{ resource.connection === 'github-app' ? 'Read-only App' : 'Legacy OAuth' }}</span>
|
||||
<div class="connection-resource-actions" v-if="resource.connection === 'oauth' && connections.appEnabled && connections.appConnected">
|
||||
<div class="connection-resource-actions" v-if="!['archived', 'removed', 'preparing', 'queue', 'removing', 'expiring'].includes(resource.status) && (resource.status !== 'download' || (resource.type === 'pull-request' && (!resource.statusDate || new Date(resource.statusDate).getTime() <= Date.now() - 300000))) && connections.appEnabled && connections.appConnected">
|
||||
<button class="btn btn-outline-ink" :disabled="busy" @click="changeConnection(resource, 'github-app', true)">Check read-only access</button>
|
||||
<button v-if="resource.eligible" class="btn btn-ink" :disabled="busy" @click="changeConnection(resource, 'github-app', false)">Switch to read-only access</button>
|
||||
<button v-if="resource.eligible" class="btn btn-ink" :disabled="busy" @click="changeConnection(resource, 'github-app', false)">{{ resource.connection === 'github-app' ? 'Reconnect read-only access' : 'Switch to read-only access' }}</button>
|
||||
<span class="connection-help" role="status" v-if="resource.eligible === false">Access unavailable. Allow this repository on GitHub, then check again.</span>
|
||||
</div>
|
||||
<button v-if="resource.connection === 'github-app' && connections.oauthConnected" class="connection-link" :disabled="busy" @click="changeConnection(resource, 'oauth', false)">Use OAuth</button>
|
||||
<button v-if="!['archived', 'removed', 'preparing', 'queue', 'removing', 'expiring'].includes(resource.status) && (resource.status !== 'download' || (resource.type === 'pull-request' && (!resource.statusDate || new Date(resource.statusDate).getTime() <= Date.now() - 300000))) && resource.connection === 'github-app' && connections.oauthConnected" class="connection-link" :disabled="busy" @click="changeConnection(resource, 'oauth', false)">Use OAuth</button>
|
||||
</div>
|
||||
<p class="connection-footnote"><i class="fas fa-info-circle" aria-hidden="true"></i> Removing access stops new fetches from GitHub. Published content keeps its current expiration and removal settings.</p>
|
||||
<p class="connection-footnote"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Removing access stops new fetches from GitHub. Published content keeps its current expiration and removal settings.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,64 +1,59 @@
|
||||
<div class="container page dashboard-page paper-page">
|
||||
<div class="row">
|
||||
<div class="w-100">
|
||||
<div class="paper-crumbs">My work / <span class="here">Dashboard</span></div>
|
||||
<div class="d-flex align-items-end justify-content-between flex-wrap" style="gap: 12px;">
|
||||
<div class="dashboard-heading">
|
||||
<div>
|
||||
<h1 class="paper-page-title">Your <em>anonymizations</em></h1>
|
||||
<p class="paper-page-lede">Every repository, pull request, and gist you’ve mirrored, with live status and stats.</p>
|
||||
<h1 class="paper-page-title">Projects</h1>
|
||||
<p class="paper-page-lede">Manage your anonymous links.</p>
|
||||
<details class="dashboard-quota" :open="quotaExpanded" @toggle="quotaExpanded = $event.target.open" @keydown.esc.prevent.stop="closeQuota()">
|
||||
<summary aria-controls="dashboard-usage" :aria-expanded="quotaExpanded">
|
||||
<svg class="quota-summary-ring" viewBox="0 0 24 24" aria-hidden="true" :class="quota ? 'quota-' + quota.repository.level : ''">
|
||||
<circle class="quota-ring-track" cx="12" cy="12" r="9"></circle>
|
||||
<circle class="quota-ring-fill" cx="12" cy="12" r="9" pathLength="100" v-if="quota && !quota.repository.unlimited" :stroke-dasharray="quota.repository.percent + ' 100'"></circle>
|
||||
<path v-if="quota?.repository.unlimited" d="M7 12c0-4 4-4 5 0s5 4 5 0-4-4-5 0-5 4-5 0" class="quota-ring-unlimited"></path>
|
||||
</svg>
|
||||
<span class="quota-summary-label">Usage</span>
|
||||
<span class="quota-summary-value" v-if="quota">{{ fmt.number(quota.repository.used) }}<span v-if="!quota.repository.unlimited"> / {{ fmt.number(quota.repository.total) }}</span> repos</span>
|
||||
<svg class="project-type-svg quota-summary-chevron" aria-hidden="true"><use href="#project-icon-chevron"></use></svg>
|
||||
</summary>
|
||||
<div class="quota-panel" id="dashboard-usage">
|
||||
<div class="quota-panel-heading"><h2>Account usage</h2><button type="button" class="project-close" aria-label="Close usage" @click="closeQuota()"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button></div>
|
||||
<quota-usage :quota="quota" :fmt="fmt" v-if="quota"></quota-usage>
|
||||
<p class="quota-loading" role="status" v-else>Loading usage…</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="dashboard-heading-actions">
|
||||
<a href="/anonymize" class="btn btn-ink">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> New anonymization
|
||||
<a href="/anonymize" class="btn btn-ink" aria-label="New anonymization">
|
||||
<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-plus"></use></svg><span class="dashboard-new-label">New link</span><span class="dashboard-new-mobile" aria-hidden="true">New</span>
|
||||
</a>
|
||||
<a href="/connections" class="dashboard-connections-link">
|
||||
<i class="fab fa-github" aria-hidden="true"></i> GitHub connections <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quota -->
|
||||
<div class="quota-row" v-if="quota">
|
||||
<div class="quota-item" v-for="(q, index) in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes'},
|
||||
{key: 'file', label: 'Files', kind: 'count'}
|
||||
]">
|
||||
<div class="quota-header">
|
||||
<span class="quota-label">{{ q?.label }}</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'count'">
|
||||
{{ fmt?.number(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.number(quota[q?.key].total) }}</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'bytes'">
|
||||
{{ fmt?.humanFileSize(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.humanFileSize(quota[q?.key].total) }}</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="quota-track" role="progressbar" aria-valuemin="0" :class="'quota-' + quota[q?.key].level" :aria-label="(q?.label) + " quota"" :aria-valuenow="(quota[q?.key].used)" :aria-valuemax="(quota[q?.key].unlimited ? quota[q?.key].used : quota[q?.key].total)" :aria-valuetext="(quota[q?.key].unlimited ? 'unlimited' : fmt.number(quota[q?.key].percent, 0) + '% used')">
|
||||
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + filters -->
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter anonymizations" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () => { $event.preventDefault() })">
|
||||
<div class="search-wrap">
|
||||
<input type="search" id="search" class="form-control" aria-label="Search anonymizations" placeholder="Search by name, source, or conference…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
<input type="search" id="search" class="form-control" aria-label="Search anonymizations" placeholder="Search projects, IDs, or conferences…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<div class="dashboard-filter-controls">
|
||||
<button class="btn dashboard-filters-toggle" type="button" :aria-expanded="filtersExpanded" aria-controls="dashboard-filter-controls" @click="filtersExpanded = !filtersExpanded">Filters<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-filter"></use></svg></button>
|
||||
<div id="dashboard-filter-controls" class="dashboard-filter-controls" :class="{'expanded': filtersExpanded}">
|
||||
<div class="btn-group" role="group" aria-label="Type">
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'all'}" @click="typeFilter = 'all'" :aria-pressed="(typeFilter === 'all')">All</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'repo'}" @click="typeFilter = 'repo'" :aria-pressed="(typeFilter === 'repo')">Repos</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'pr'}" @click="typeFilter = 'pr'" :aria-pressed="(typeFilter === 'pr')">PRs</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'gist'}" @click="typeFilter = 'gist'" :aria-pressed="(typeFilter === 'gist')">Gists</button>
|
||||
<button type="button" class="btn dashboard-type-filter" title="Repositories" :class="{'btn-primary': typeFilter === 'repo'}" @click="typeFilter = 'repo'" :aria-pressed="(typeFilter === 'repo')"><svg class="project-type-svg" aria-hidden="true"><use href="#project-type-repo"></use></svg><span class="type-filter-label">Repos</span></button>
|
||||
<button type="button" class="btn dashboard-type-filter" title="Pull requests" :class="{'btn-primary': typeFilter === 'pr'}" @click="typeFilter = 'pr'" :aria-pressed="(typeFilter === 'pr')"><svg class="project-type-svg" aria-hidden="true"><use href="#project-type-pr"></use></svg><span class="type-filter-label">PRs</span></button>
|
||||
<button type="button" class="btn dashboard-type-filter" title="Gists" :class="{'btn-primary': typeFilter === 'gist'}" @click="typeFilter = 'gist'" :aria-pressed="(typeFilter === 'gist')"><svg class="project-type-svg" aria-hidden="true"><use href="#project-type-gist"></use></svg><span class="type-filter-label">Gists</span></button>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dashboard-extra-filters" class="dashboard-extra-filters" :class="{'expanded': filtersExpanded}">
|
||||
<div class="dropdown">
|
||||
<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" aria-hidden="true" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
<svg class="project-type-svg sort-direction" aria-hidden="true" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
<span class="sr-only">{{ sortDesc() ? 'descending' : 'ascending' }}</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownSort">
|
||||
@@ -69,7 +64,6 @@
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<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' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -90,43 +84,48 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Result count + active filter chips -->
|
||||
<div class="dashboard-meta" v-if="!loading">
|
||||
<div class="dashboard-view-tabs" role="group" aria-label="Project view">
|
||||
<button type="button" :aria-pressed="projectView === 'all'" @click="setProjectView('all')" aria-label="All projects"><span class="dashboard-view-long">All projects</span><span class="dashboard-view-short" aria-hidden="true">All</span></button>
|
||||
<button type="button" :aria-pressed="projectView === 'attention'" @click="setProjectView('attention')" :aria-label="'Needs attention ' + attentionCount()"><span class="dashboard-attention-prefix">Needs </span><span>attention</span> <span class="attention-count" :class="{'has-attention': attentionCount() > 0}">{{ attentionCount() }}</span></button>
|
||||
</div>
|
||||
<span class="dashboard-count">
|
||||
<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 v-if="(dashboardTotals?.filtered ?? filteredItems?.length) === (dashboardTotals?.total ?? items?.length)">{{ fmt?.number(dashboardTotals?.total ?? items?.length) }} project<span v-if="(dashboardTotals?.total ?? items?.length) !== 1">s</span></span>
|
||||
<span v-else>{{ fmt?.number(dashboardTotals?.filtered ?? filteredItems?.length) }} of {{ fmt?.number(dashboardTotals?.total ?? items?.length) }} shown</span>
|
||||
</span>
|
||||
<template v-for="(v, f, index) in filters?.status"><span class="filter-chip" v-if="v === false">
|
||||
Hiding {{ statusKeyLabels[f] }}
|
||||
<button type="button" class="filter-chip-close" @click="filters.status[f] = true;" :aria-label=""Show " + (statusKeyLabels[f]) + " again"">×</button>
|
||||
</span></template>
|
||||
<button type="button" class="btn-link-inline" v-if="hasActiveFilters()" @click="clearFilters()">Clear filters</button>
|
||||
<div class="dashboard-active-filters">
|
||||
<template v-for="(v, f, index) in filters?.status"><button type="button" class="filter-chip" v-if="v === false" @click="filters.status[f] = true" :aria-label="'Show ' + statusKeyLabels[f] + ' again'" :title="'Show ' + statusKeyLabels[f] + ' again'">{{ statusKeyLabels[f] }} hidden<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button></template>
|
||||
<button type="button" class="filter-chip" v-if="typeFilter !== 'all'" @click="typeFilter = 'all'" aria-label="Show all project types">{{ typeFilter === 'repo' ? 'Repositories' : typeFilter === 'pr' ? 'Pull requests' : 'Gists' }}<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button>
|
||||
<button type="button" class="btn-link-inline" v-if="showClearFilters()" @click="clearFilters()">Clear filters</button>
|
||||
</div>
|
||||
</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" 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" :class="{active: isSortedBy('_name')}" @click="setSort('_name')">
|
||||
Anonymization <i class="fas" aria-hidden="true" v-if="isSortedBy('_name')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('_label') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('_label')}" @click="setSort('_label')">
|
||||
Project <svg class="project-type-svg sort-direction" aria-hidden="true" v-if="isSortedBy('_label')" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('status') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('status')}" @click="setSort('status')">
|
||||
Status <i class="fas" aria-hidden="true" v-if="isSortedBy('status')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
Status <svg class="project-type-svg sort-direction" aria-hidden="true" v-if="isSortedBy('status')" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader" class="num" :aria-sort="(isSortedBy('pageView') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('pageView')}" @click="setSort('pageView')">
|
||||
Views <i class="fas" aria-hidden="true" v-if="isSortedBy('pageView')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
Views <svg class="project-type-svg sort-direction" aria-hidden="true" v-if="isSortedBy('pageView')" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('options.expirationDate') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('options.expirationDate')}" @click="setSort('options.expirationDate')">
|
||||
Expires <i class="fas" aria-hidden="true" v-if="isSortedBy('options.expirationDate')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
Expires <svg class="project-type-svg sort-direction" aria-hidden="true" v-if="isSortedBy('options.expirationDate')" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader"><span class="sr-only">Actions</span></div>
|
||||
@@ -143,27 +142,21 @@
|
||||
|
||||
<template v-for="(item, index) in filteredItems" :key="(item?._type + ':' + (item?._id || index))"><div class="paper-table-row" role="row" v-if="!loading" :class="{'repo-inactive': item?._statusKey == 'expired' || item?._statusKey == 'removed', 'repo-error': item?.status == 'error', 'row-clickable': !!item?._viewUrl}" @click="openItem(item, $event)">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge" :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="project-type-icon" role="img" :aria-label="item?._type === 'repo' ? 'Repository' : item?._type === 'pr' ? 'Pull request' : 'Gist'" :title="item?._type === 'repo' ? 'Repository' : item?._type === 'pr' ? 'Pull request' : 'Gist'"><svg class="project-type-svg" aria-hidden="true"><use :href="'#project-type-' + item._type"></use></svg></span>
|
||||
<div class="anon-text">
|
||||
<div class="anon-title">
|
||||
<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>
|
||||
<a class="repo-name" v-if="item?._viewUrl" :title="item._label" v-text="item?._label" :href="safeUrl((item?._viewUrl))"></a>
|
||||
<span class="repo-name repo-name-static" v-if="!item?._viewUrl" v-text="item?._label"></span>
|
||||
<span class="type-badge type-coauthor" title="You are a co-author on this anonymization" v-if="item?.role === 'coauthor'">Co-author</span>
|
||||
</div>
|
||||
<div class="anon-sub" v-if="!item?._broken || item?.conference">
|
||||
<span class="anon-source" v-if="item?._type === 'repo' && item?.source?.fullName && !item?._broken">
|
||||
<a target="_blank" rel="noopener" v-text="item?.source?.fullName" :href="safeUrl("https://github.com/" + (item?.source?.fullName) + "/")"></a><span v-if="item?.options?.update && item?.source?.branch"> · <a target="_blank" rel="noopener" v-text="item?.source?.branch" :href="safeUrl("https://github.com/" + (item?.source?.fullName) + "/tree/" + (item?.source?.branch))"></a></span><span v-if="item?.source?.commit"> · <a class="commit-hash" target="_blank" rel="noopener" :href="safeUrl("https://github.com/" + (item?.source?.fullName) + "/tree/" + (item?.source?.commit))">@{{ item?.source?.commit?.substring(0, 8) }}</a></span>
|
||||
</span>
|
||||
<span class="anon-source" v-if="item?._type === 'pr' && item?.source?.repositoryFullName && !item?._broken">
|
||||
<a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl("https://github.com/" + (item?.source?.repositoryFullName) + "/pull/" + (item?.source?.pullRequestId))"></a>
|
||||
</span>
|
||||
<span class="anon-source" v-if="item?._type === 'gist' && item?.source?.gistId && !item?._broken">
|
||||
<a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl("https://gist.github.com/" + (item?.source?.gistId))"></a>
|
||||
</span>
|
||||
<span class="cell-conf conf-tag" v-if="item?.conference" :title="(item?.conference)"><i class="fas fa-university" aria-hidden="true"></i> <span v-text="item?.conference"></span></span>
|
||||
<div class="anon-sub">
|
||||
<code class="anonymous-id" v-if="item?._id" :title="item._id">{{ item._id }}</code>
|
||||
<span class="commit-hash" v-if="item?._type === 'repo' && item?.source?.commit" :title="item.source.commit">{{ item.source.commit.substring(0, 8) }}</span>
|
||||
<span class="cell-conf conf-tag" v-if="item?.conference" :title="(item?.conference)"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-conference"></use></svg> <span v-text="item?.conference"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-row-meta" role="presentation">
|
||||
<div class="cell-status" role="cell">
|
||||
<div class="status-line">
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + item?._statusKey"></span>
|
||||
@@ -173,62 +166,99 @@
|
||||
<div class="status-sub status-sub-warn" v-if="item?._stale" :title=""Last activity " + (fmt?.humanTime(item?.anonymizeDate || item?.lastView))">
|
||||
Last activity {{ fmt?.humanTime(item?.anonymizeDate || item?.lastView) }} · may be stuck
|
||||
</div>
|
||||
<div class="status-sub" v-if="item?.status != 'error' && !item?._stale && item?.anonymizeDate">
|
||||
<a class="project-attention-action" v-if="item.status === 'error' && item._editUrl" :href="safeUrl(item._editUrl)">Fix source</a>
|
||||
<button type="button" class="project-attention-action" v-if="item._stale || item._broken" @click.stop="showProjectDetails(item)">{{ item._broken ? 'Review details' : 'Check progress' }}</button>
|
||||
<div class="status-sub status-sub-date" v-if="item?.status != 'error' && !item?._stale && item?.anonymizeDate">
|
||||
anonymized {{ fmt?.humanTime(item?.anonymizeDate) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(item?.pageView)"></div>
|
||||
<div class="cell-views num" role="cell" :aria-label="fmt.number(item.pageView) + ' views'"><svg class="project-type-svg mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg><span>{{ fmt.number(item.pageView) }}</span></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<svg class="project-type-svg mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
<span class="expires-never" v-if="item?._expiry?.kind === 'never'">Never</span>
|
||||
<span v-if="item?._expiry?.kind === 'date'" v-text="fmt?.humanTime(item?._expiry?.date)"></span>
|
||||
<span class="expires-past" v-if="item?._expiry?.kind === 'expired' && item?._expiry?.date">Expired {{ fmt?.humanTime(item?._expiry?.date) }}</span>
|
||||
<span class="expires-past" v-if="item?._expiry?.kind === 'expired' && !item?._expiry?.date">Expired</span>
|
||||
<span class="empty-dash" aria-label="Not applicable" v-if="item?._expiry?.kind === 'none'">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<button type="button" class="copy-link-inline" v-if="item._viewUrl" @click.stop="copyLink(item)" :disabled="!!copyingId" :title="'Copy anonymous link for ' + item._label" :aria-label="'Copy anonymous link for ' + item._label"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-copy"></use></svg></button>
|
||||
<button type="button" class="project-details-button" v-if="item._broken" @click.stop="showProjectDetails(item)" :data-project-actions="encodeURIComponent(item._type + ':' + item._id)" :aria-controls="projectDetailsId(item)" :aria-expanded="selectedProject === item">Details</button>
|
||||
<div class="dropdown" v-if="!item?._broken">
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :aria-label=""Actions for " + (item?._name)">
|
||||
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :data-project-actions="encodeURIComponent(item._type + ':' + item._id)" :aria-label=""Actions for " + (item?._name)">
|
||||
<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<button type="button" class="dropdown-item" @click="showProjectDetails(item)" :aria-controls="projectDetailsId(item)" :aria-expanded="selectedProject === item">Project details & name</button>
|
||||
<a class="dropdown-item" :href="safeUrl((item?._viewUrl))">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View
|
||||
View
|
||||
</a>
|
||||
<a class="dropdown-item" target="_self" v-if="item?._type === 'repo' && item?.options?.page && item?.status == 'ready'" :href="safeUrl("/w/" + (item?.repoId) + "/")">
|
||||
<i class="fas fa-globe" aria-hidden="true"></i> View page
|
||||
View page
|
||||
</a>
|
||||
<button type="button" class="dropdown-item" @click="copyLink(item)" :disabled="!!copyingId" v-if="item?._viewUrl">
|
||||
Copy anonymous link
|
||||
</button>
|
||||
<a class="dropdown-item" :href="safeUrl((item?._editUrl))">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
Edit
|
||||
</a>
|
||||
<button type="button" class="dropdown-item" v-if="item?.status == 'ready' || item?.status == 'error'" @click="refreshItem(item)">
|
||||
<i class="fas fa-sync" aria-hidden="true"></i> Force update
|
||||
Force update
|
||||
</button>
|
||||
<button type="button" class="dropdown-item" v-if="item?.status == 'removed'" @click="refreshItem(item)">
|
||||
<i class="fas fa-check-circle" aria-hidden="true"></i> Enable
|
||||
Enable
|
||||
</button>
|
||||
<button type="button" class="dropdown-item" v-if="item?.status == 'expired'" @click="extendItem(item)">
|
||||
<i class="fas fa-calendar-plus" aria-hidden="true"></i> Extend 6 months
|
||||
Extend 6 months
|
||||
</button>
|
||||
<div v-if="(item?.status == 'ready' || item?.status == 'expired' || item?.status == 'error') && item?.role !== 'coauthor'">
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="button" class="dropdown-item dropdown-item-danger" @click="removeItem(item)">
|
||||
<i class="fas fa-trash-alt" aria-hidden="true"></i> Remove
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-details-row" role="row" v-if="!loading && selectedProject === item">
|
||||
<div class="project-details" role="cell" aria-colspan="5" :id="projectDetailsId(item)" tabindex="-1" @keydown.esc.stop="closeProjectDetails(item)">
|
||||
<div class="project-details-heading"><h2>Project details</h2><button type="button" class="project-close" @click="closeProjectDetails(item)" aria-label="Close project details"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button></div>
|
||||
<dl class="project-details-list">
|
||||
<div><dt>Source</dt><dd>{{ item._source || 'Not available' }}</dd></div>
|
||||
<div><dt>Anonymous ID</dt><dd><code>{{ item._id || 'Missing identifier' }}</code></dd></div>
|
||||
<div v-if="item.source?.commit"><dt>Commit</dt><dd><code>{{ item.source.commit }}</code></dd></div>
|
||||
<div v-if="item.conference"><dt>Conference</dt><dd>{{ item.conference }}</dd></div>
|
||||
<div><dt>Status</dt><dd>{{ fmt.statusLabel(item.status) }}<span v-if="item.statusMessage"> · {{ fmt.statusMsg(item.statusMessage) }}</span></dd></div>
|
||||
<div v-if="item.anonymizeDate"><dt>Last anonymized</dt><dd>{{ fmt.humanTime(item.anonymizeDate) }}</dd></div>
|
||||
</dl>
|
||||
<p class="project-progress-help" v-if="item._stale">This download has not progressed recently. Check the source access and selected branch before retrying.</p>
|
||||
<a class="project-attention-action" v-if="item._editUrl" :href="safeUrl(item._editUrl)">{{ needsAttention(item) ? 'Check source settings' : 'Edit anonymization settings' }}</a>
|
||||
<form class="project-name-form" v-if="item._id" @submit.prevent="saveProjectName(item)">
|
||||
<label :for="'project-name-' + item._type + '-' + item._id">Project name</label>
|
||||
<p :id="'project-name-help-' + item._type + '-' + item._id">Only you see this name. Leave it blank to use the default.</p>
|
||||
<div class="project-name-controls"><input class="form-control" type="text" :id="'project-name-' + item._type + '-' + item._id" :aria-describedby="'project-name-help-' + item._type + '-' + item._id" maxlength="100" v-model="projectName" :disabled="nameSaving"><button class="btn btn-ink" type="submit" :disabled="nameSaving">{{ nameSaving ? 'Saving…' : 'Save name' }}</button></div>
|
||||
<p class="project-name-error" role="alert" v-if="nameError">{{ nameError }}</p><p role="status" v-if="nameMessage">{{ nameMessage }}</p>
|
||||
</form>
|
||||
</div>
|
||||
</div></template>
|
||||
|
||||
<div class="paper-table-empty" v-if="!loading && filteredItems?.length == 0">
|
||||
<i class="fas fa-inbox" aria-hidden="true"></i>
|
||||
<span v-if="items?.length == 0">You have no anonymizations yet.</span>
|
||||
<span v-if="items?.length > 0">Nothing matches the current filters.</span>
|
||||
<svg class="project-type-svg project-empty-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span v-if="!hasProjects()">You have no anonymizations yet.</span>
|
||||
<span v-if="hasProjects()">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions">
|
||||
<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>
|
||||
<a href="/anonymize" class="btn btn-ink" v-if="!hasProjects()"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-plus"></use></svg> New anonymization</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-pagination" v-if="dashboardCursor || dashboardError">
|
||||
<p role="alert" v-if="dashboardError">{{ dashboardError }}</p>
|
||||
<button type="button" class="btn" v-if="dashboardCursor" @click="loadMoreProjects()" :disabled="loadingMore">
|
||||
{{ loadingMore ? 'Loading…' : 'Load more projects' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="explorer-page">
|
||||
<button class="sidebar-toggle" v-show="files?.length" @click="sidebarCollapsed = !sidebarCollapsed" :aria-label="(sidebarCollapsed ? 'Show files' : 'Hide files')">
|
||||
<i class="fas" :class="sidebarCollapsed ? 'fa-folder-open' : 'fa-times'"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use :href="sidebarCollapsed ? '#ui-icon-folder' : '#project-icon-close'"></use></svg>
|
||||
<span v-text="sidebarCollapsed ? 'Files' : 'Close'"></span>
|
||||
</button>
|
||||
|
||||
@@ -8,28 +8,32 @@
|
||||
<div class="leftCol-head">
|
||||
<span class="leftCol-eyebrow">Files</span>
|
||||
<button class="leftCol-close" aria-label="Close files" @click="sidebarCollapsed = true">
|
||||
<i class="fas fa-times"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="leftCol-search">
|
||||
<div class="tree-search-box">
|
||||
<i class="fas tree-search-icon" :class="fileSearchLoading ? 'fa-spinner fa-spin' : 'fa-search'"></i>
|
||||
<svg class="ui-icon tree-search-icon" aria-hidden="true" :class="{'fa-spin': fileSearchLoading}"><use :href="fileSearchLoading ? '#ui-icon-spinner' : '#ui-icon-search'"></use></svg>
|
||||
<input type="text" class="tree-search-input" placeholder="Search files" aria-label="Search files" v-field="{ state: viewState, set: value => { fileSearchQuery = value }, value: fileSearchQuery, form: null, options: { debounce: 300 }, change: () => { onFileSearchChange() } }">
|
||||
<kbd class="tree-search-kbd" v-show="!(fileSearchQuery)">{{ isMac ? '⌘' : 'Ctrl+' }}K</kbd>
|
||||
<button class="tree-search-clear" aria-label="Clear search" v-show="fileSearchQuery" @click="fileSearchQuery = ''; onFileSearchChange()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="leftCol-project-header">
|
||||
<span class="project-name" v-text="repoId"></span>
|
||||
<span class="project-name" v-text="repoId" :title="repoId"></span>
|
||||
<span class="project-file-count">{{ fileCounts?.[''] || files?.length }} files</span>
|
||||
<span class="project-expiry" v-if="options?.expirationDate" :title="options.expirationDate">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
Available until {{ fmt?.date(options.expirationDate) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="leftCol-body">
|
||||
<div class="paper-inline-warning" role="alert" v-if="options?.truncatedFolders?.length > 0">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
||||
{{ fmt?.translate('WARNINGS.repo_truncated') }}
|
||||
</div>
|
||||
<div class="paper-inline-warning" role="alert" v-if="options?.hasSubmodules">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
||||
{{ fmt?.translate('WARNINGS.submodules_not_included') }}
|
||||
</div>
|
||||
<tree class="files" :file="files" :search-query="fileSearchQuery" :search-results="fileSearchResults" :page="viewState"></tree>
|
||||
@@ -53,24 +57,24 @@
|
||||
<div class="leftCol-backdrop" v-show="files?.length && !sidebarCollapsed" @click="sidebarCollapsed = true"></div>
|
||||
|
||||
<div class="explorer-main">
|
||||
<div class="status-bar">
|
||||
<div class="status-bar" v-show="type != 'error' || files?.length">
|
||||
<ol class="breadcrumb paths" aria-label="Path">
|
||||
<li class="breadcrumb-item" v-for="(p, index) in paths" v-text="p"></li>
|
||||
</ol>
|
||||
<div class="status-bar-actions">
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/anonymize/" + (repoId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/anonymize/" + (repoId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg><span class="d-none d-md-inline"> Edit</span></a>
|
||||
<button class="btn btn-sm" aria-label="Allow this document to run JavaScript" title="Scripts in this file are blocked by default. Enable them only if you trust the repository — the document stays isolated from your session either way." v-if="type == 'html-doc' && !showSource" :class="{'btn-active': allowScripts}" @click="toggleAllowScripts()">
|
||||
<i class="fab fa-js"></i><span class="d-none d-md-inline">
|
||||
{{ allowScripts ? "JS on" : "JS off" }}</span>
|
||||
</button>
|
||||
<button class="btn btn-sm" aria-label="Toggle between the rendered document and its source" title="Toggle between the rendered document and its source" v-if="type == 'html-doc'" @click="toggleSource()">
|
||||
<i class="fas" :class="showSource ? 'fa-eye' : 'fa-code'"></i><span class="d-none d-md-inline">
|
||||
<svg class="ui-icon" aria-hidden="true"><use :href="showSource ? '#project-icon-eye' : '#ui-icon-code'"></use></svg><span class="d-none d-md-inline">
|
||||
{{ showSource ? "Rendered" : "Source" }}</span>
|
||||
</button>
|
||||
<a target="_self" class="btn btn-sm" aria-label="View raw current file" title="View the raw content of the current file" v-show="content != null" :href="safeUrl((url))"><i class="fas fa-file-alt"></i><span class="d-none d-md-inline"> Raw</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Download current file" title="Download the current file" v-show="content != null" :href="safeUrl((url) + "&download=true")"><i class="fas fa-download"></i><span class="d-none d-md-inline"> Download</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Download full repository as ZIP" title="Download the full repository as a ZIP archive" v-if="options?.download" :href="safeUrl("/api/repo/" + (repoId) + "/zip")"><i class="fas fa-file-archive"></i><span class="d-none d-md-inline"> Full repo ZIP</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Website" v-if="options?.hasWebsite" :href="safeUrl("/w/" + (repoId) + "/")"><i class="fas fa-globe"></i><span class="d-none d-md-inline"> Website</span></a>
|
||||
<a target="_self" class="btn btn-sm btn-icon-only" aria-label="View raw current file" title="View raw file" v-show="content != null && type != 'error' && type != 'rate_limited'" :href="safeUrl((url))"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-file"></use></svg></a>
|
||||
<a target="_self" class="btn btn-sm btn-icon-only" aria-label="Download current file" title="Download this file" v-show="content != null && type != 'error' && type != 'rate_limited'" :href="safeUrl((url) + "&download=true")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-download"></use></svg></a>
|
||||
<a target="_self" class="btn btn-sm btn-ink" aria-label="Download repository as ZIP" title="Download the whole repository as a ZIP archive" v-if="options?.download" :href="safeUrl("/api/repo/" + (repoId) + "/zip")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-archive"></use></svg><span class="d-none d-md-inline"> Download repository</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Website" v-if="options?.hasWebsite" :href="safeUrl("/w/" + (repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-globe"></use></svg><span class="d-none d-md-inline"> Website</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="explorer-content">
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<!-- General -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-info-circle mr-2"></i>General
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#ui-icon-info"></use></svg>General
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-general" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -113,7 +113,7 @@
|
||||
|
||||
<!-- Features -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-cogs mr-2"></i>Features
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#project-icon-filter"></use></svg>Features
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-features" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -285,7 +285,7 @@ anonymous_github</code></pre>
|
||||
|
||||
<!-- Limitations -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>Limitations
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>Limitations
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-limitations" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -322,7 +322,7 @@ anonymous_github</code></pre>
|
||||
|
||||
<!-- Privacy & Security -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-shield-alt mr-2"></i>Privacy & Security
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#ui-icon-shield"></use></svg>Privacy & Security
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-privacy" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -355,28 +355,9 @@ anonymous_github</code></pre>
|
||||
</div>
|
||||
<div id="permissions" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingPermissions">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Anonymous GitHub only reads your repositories — it
|
||||
never pushes commits, opens issues, modifies settings, or
|
||||
deletes anything. From your perspective as a user, the
|
||||
service is read-only.
|
||||
</p>
|
||||
<p>
|
||||
However, GitHub's OAuth scopes do not offer a read-only
|
||||
option for private repositories: the only scope that grants
|
||||
access to private repos is <code>repo</code>, which is
|
||||
documented as full read/write access. To support users who
|
||||
want to anonymize a private repository, Anonymous GitHub
|
||||
must request that scope, and GitHub then displays the
|
||||
broader permission text at sign-in. The application itself
|
||||
only ever performs read operations against the GitHub API.
|
||||
</p>
|
||||
<p>
|
||||
If you only anonymize public repositories, the source code
|
||||
is open and can be audited on the
|
||||
<a href="https://github.com/tdurieux/anonymous_github/" target="_blank">GitHub repository</a>,
|
||||
or you can self-host your own instance.
|
||||
</p>
|
||||
<p>The GitHub App has read-only access to the repositories you choose. You can change repository access on your <a href="/connections">GitHub connections</a> page.</p>
|
||||
<p>Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection.</p>
|
||||
<p>Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,7 +384,7 @@ anonymous_github</code></pre>
|
||||
|
||||
<!-- Self-Hosting -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-server mr-2"></i>Self-Hosting
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#project-icon-storage"></use></svg>Self-Hosting
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-hosting" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
|
||||
@@ -6,40 +6,42 @@
|
||||
</div>
|
||||
|
||||
<header class="pr-header">
|
||||
<div class="pr-title-row">
|
||||
<h1 class="paper-page-title pr-title">
|
||||
<span v-if="details?.description" v-text="details?.description"></span>
|
||||
<span class="text-muted" v-if="!details?.description">Untitled gist</span>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/gist-anonymize/" + (gistId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</h1>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/gist-anonymize/" + (gistId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</div>
|
||||
<div class="pr-header-meta">
|
||||
<span class="paper-pill" :class="{'good': details?.isPublic, 'warn': !details?.isPublic}">
|
||||
{{ details?.isPublic ? 'Public' : 'Secret' }}
|
||||
</span>
|
||||
<span class="pr-meta-item" v-if="details?.ownerLogin">
|
||||
<i class="far fa-user"></i> @<span v-text="details?.ownerLogin"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="details?.ownerLogin"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" v-if="details?.updatedDate">
|
||||
<i class="far fa-clock"></i> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
</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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="paper-tabs" role="tablist" v-if="(details?.files && details?.files?.length) || (details?.comments && details?.comments?.length)">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.files" :class="{'active': tabState?.active == 'files'}" @click="tabState.active = 'files'">
|
||||
<i class="fas fa-file-code"></i>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.files" :class="{'active': tabState?.active == 'files'}" id="gist-files-tab" :aria-selected="tabState?.active === 'files'" aria-controls="gist-files-panel" :tabindex="tabState?.active === 'files' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="tabState.active = 'files'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-type-gist"></use></svg>
|
||||
<span v-text="fmt.plural(details?.files?.length, {'0': 'No files', 'one': '1 file', 'other': '{} files'})"></span>
|
||||
</button>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" @click="tabState.active = 'comments'">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" id="gist-comments-tab" :aria-selected="tabState?.active === 'comments'" aria-controls="gist-comments-panel" :tabindex="tabState?.active === 'comments' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="tabState.active = 'comments'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span v-text="fmt.plural(details?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="paper-tab-content">
|
||||
<div v-if="details?.files && tabState?.active =='files'">
|
||||
<div role="tabpanel" id="gist-files-panel" aria-labelledby="gist-files-tab" v-if="details?.files && tabState?.active =='files'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(file, index) in details?.files">
|
||||
<div class="pr-comment-head">
|
||||
@@ -49,18 +51,18 @@
|
||||
<gist-file :file="file"></gist-file>
|
||||
</li>
|
||||
<li class="paper-table-empty" v-if="!details?.files?.length">
|
||||
<i class="fas fa-file"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-file"></use></svg>
|
||||
<span>No files in this gist.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="details?.comments && tabState?.active =='comments'">
|
||||
<div role="tabpanel" id="gist-comments-panel" aria-labelledby="gist-comments-tab" v-if="details?.comments && tabState?.active =='comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="comment?.author">
|
||||
<i class="far fa-user"></i> @<span v-text="comment?.author"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="comment?.author"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="comment?.updatedDate" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
@@ -69,7 +71,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="paper-table-empty" v-if="!details?.comments?.length">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span>No comments on this gist.</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<nav class="navbar navbar-expand-lg" :class="{'navbar-dark': isDarkMode}">
|
||||
<a class="navbar-brand" href="/">Anonymous <em>GitHub</em></a>
|
||||
<a class="nav-support" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener" title="Donate on Ko-fi to help cover hosting costs" aria-label="Donate on Ko-fi to help cover hosting costs">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg>
|
||||
<span class="nav-support-label">Support</span>
|
||||
</a>
|
||||
<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>
|
||||
@@ -13,57 +17,57 @@
|
||||
</li>
|
||||
<li class="nav-item" v-if="user">
|
||||
<a class="nav-link" href="/dashboard" :class="{'active': path == '/dashboard'}">
|
||||
<i class="fas fa-th-large d-lg-none mr-1"></i>
|
||||
<svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-grid"></use></svg>
|
||||
My work
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" v-if="user">
|
||||
<a class="nav-link" href="/anonymize" :class="{'active':path == '/anonymize' || path == '/pull-request-anonymize'}">
|
||||
<i class="fas fa-user-secret d-lg-none mr-1"></i>
|
||||
<svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg>
|
||||
Anonymize
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" v-if="user">
|
||||
<a class="nav-link" href="/conferences" :class="{'active':path == '/conferences'}">
|
||||
<i class="fas fa-chalkboard-teacher d-lg-none mr-1"></i>
|
||||
<svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#project-icon-conference"></use></svg>
|
||||
Conferences
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/faq" :class="{'active':path == '/faq'}"><i class="fas fa-question-circle d-lg-none mr-1"></i>FAQ</a>
|
||||
<a class="nav-link" href="/faq" :class="{'active':path == '/faq'}"><svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-help"></use></svg>FAQ</a>
|
||||
</li>
|
||||
<li class="nav-item" v-if="user && user?.isAdmin">
|
||||
<a class="nav-link" href="/admin/" :class="{'active':path?.indexOf('/admin') === 0}">
|
||||
<i class="fas fa-cog d-lg-none mr-1"></i>
|
||||
<svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#project-icon-filter"></use></svg>
|
||||
Admin
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" v-if="user || !isReviewerRoute">
|
||||
<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"><svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-bug"></use></svg>Report a bug
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-moon"></use></svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-sun"></use></svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" v-if="!user">
|
||||
<li class="nav-item" v-if="!user && !isReviewerRoute">
|
||||
<a class="nav-link btn-signin" target="_self" href="/signin" data-offset="30"><i class="fab fa-github mr-1"></i>
|
||||
Sign in
|
||||
</a>
|
||||
@@ -76,7 +80,7 @@
|
||||
<div class="dropdown-menu dropdown-menu-right user-menu" aria-labelledby="navbarDropdownMenuLink">
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg>
|
||||
<span>Settings<small>Defaults, quotas, account</small></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="/connections">
|
||||
@@ -84,21 +88,25 @@
|
||||
<span>GitHub connections<small>Repository access and OAuth</small></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="/claim">
|
||||
<i class="fas fa-link" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg>
|
||||
<span>Claim an anonymization<small>Attach an existing mirror to your account</small></span>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">
|
||||
<i class="fas fa-bug" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg>
|
||||
<span>Report a bug<small>About this service, not a paper</small></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">
|
||||
<i class="fab fa-github" aria-hidden="true"></i>
|
||||
<span>Source code</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg>
|
||||
<span>Support the project</span>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="/api/user/logout" target="_self">
|
||||
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-logout"></use></svg>
|
||||
<span>Sign out</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
Anonymous GitHub makes a read-only mirror of your repository, pull
|
||||
request, or gist with every trace of identity removed, so you can
|
||||
attach a link to your submission without breaking anonymity.
|
||||
request, or gist. It redacts the terms you choose in text files,
|
||||
so reviewers can browse your artifact through an anonymous link.
|
||||
</p>
|
||||
|
||||
<div class="paper-cta-row">
|
||||
@@ -34,7 +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/redaction-preview.png" width="1360" height="960" alt="A reviewer preview with the author, affiliation, and email replaced by anonymous terms">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -117,9 +117,9 @@
|
||||
<h2>Anonymize, review, <em>manage.</em></h2>
|
||||
</div>
|
||||
<p>
|
||||
Every mirror is read-only and passes through the same filter, from the
|
||||
README to a PDF's link targets. Monitor it, update it, or remove it the
|
||||
day the decision is out.
|
||||
Terms are redacted in text files. Enabled images and PDFs are served
|
||||
unchanged, so review them for identifying content before sharing.
|
||||
Monitor your mirror, update it, or remove it when the review is over.
|
||||
</p>
|
||||
</section>
|
||||
<section class="paper-features paper-wrap" aria-label="Product features">
|
||||
@@ -160,19 +160,19 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="q">
|
||||
<a href="/faq#howWork">Why does GitHub ask for write access at sign-in?</a>
|
||||
<a href="/faq#permissions">What access does GitHub sign-in require?</a>
|
||||
<p>
|
||||
GitHub's OAuth scopes have no read-only option for private
|
||||
repositories. Anonymous GitHub only ever reads your repositories; it
|
||||
never pushes, modifies, or deletes anything.
|
||||
The GitHub App has read-only access to gists and the repositories you choose.
|
||||
Legacy OAuth uses broader permissions.
|
||||
Anonymous GitHub never writes to your repositories.
|
||||
</p>
|
||||
</div>
|
||||
<div class="q">
|
||||
<a href="/faq#formats">Which file formats are supported?</a>
|
||||
<p>
|
||||
Source code with syntax highlighting, Markdown, Jupyter notebooks,
|
||||
PDFs, images, and GitHub Pages sites. Everything passes through the
|
||||
same redaction filter.
|
||||
PDFs, images, and GitHub Pages sites. Text files are anonymized;
|
||||
enabled images and PDFs are served unchanged.
|
||||
</p>
|
||||
</div>
|
||||
<div class="q">
|
||||
@@ -191,7 +191,7 @@
|
||||
<div class="paper-footer-mark">Anonymous <em>GitHub</em></div>
|
||||
<p class="paper-footer-tag">
|
||||
A read-only, identity-stripped mirror of your repository, built for
|
||||
double-blind peer review.
|
||||
double-anonymous peer review.
|
||||
</p>
|
||||
</div>
|
||||
<div class="paper-footer-col">
|
||||
@@ -215,7 +215,7 @@
|
||||
</div>
|
||||
<div class="paper-footer-rule"></div>
|
||||
<div class="paper-footer-foot">
|
||||
<span>© {{ year || 2026 }} Anonymous GitHub · MIT licensed</span>
|
||||
<span>© {{ year || 2026 }} Anonymous GitHub · GPL-3.0 licensed</span>
|
||||
<span class="paper-footer-meta">Built for reviewers, by researchers.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<svg class="ui-icon-definitions" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<symbol id="project-type-repo" viewBox="0 0 24 24">
|
||||
<path d="M6 3h13v18H6a3 3 0 0 1 0-6h13M3 18V6a3 3 0 0 1 3-3M8 7h6"></path>
|
||||
</symbol>
|
||||
<symbol id="project-type-pr" viewBox="0 0 24 24">
|
||||
<circle cx="6" cy="5" r="2.5"></circle><circle cx="6" cy="19" r="2.5"></circle><circle cx="18" cy="19" r="2.5"></circle>
|
||||
<path d="M6 7.5v9M18 16.5V9a4 4 0 0 0-4-4h-3m3-3-3 3 3 3"></path>
|
||||
</symbol>
|
||||
<symbol id="project-type-gist" viewBox="0 0 24 24">
|
||||
<path d="M14 3H5v18h14V8l-5-5v5h5M9 12l-2 2 2 2m6-4 2 2-2 2"></path>
|
||||
</symbol>
|
||||
<symbol id="project-icon-copy" viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="13" rx="2"></rect><path d="M15 8V3H3v13h5"></path></symbol>
|
||||
<symbol id="project-icon-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle></symbol>
|
||||
<symbol id="project-icon-eye" viewBox="0 0 24 24"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle></symbol>
|
||||
<symbol id="project-icon-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></symbol>
|
||||
<symbol id="project-icon-conference" viewBox="0 0 24 24"><path d="m3 8 9-5 9 5H3Zm2 3v7m5-7v7m4-7v7m5-7v7M3 21h18"></path></symbol>
|
||||
<symbol id="project-icon-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"></path></symbol>
|
||||
<symbol id="project-icon-filter" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"></path><circle cx="8" cy="6" r="2"></circle><circle cx="16" cy="12" r="2"></circle><circle cx="9" cy="18" r="2"></circle></symbol>
|
||||
<symbol id="project-icon-arrow" viewBox="0 0 24 24"><path d="M12 4v16m-5-5 5 5 5-5"></path></symbol>
|
||||
<symbol id="project-icon-close" viewBox="0 0 24 24"><path d="m6 6 12 12M6 18 18 6"></path></symbol>
|
||||
<symbol id="project-icon-chevron" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"></path></symbol>
|
||||
<symbol id="project-icon-storage" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="7" rx="2"></rect><rect x="3" y="13" width="18" height="7" rx="2"></rect><path d="M7 7.5h.01M7 16.5h.01M11 7.5h6M11 16.5h6"></path></symbol>
|
||||
<symbol id="project-icon-file" viewBox="0 0 24 24"><path d="M14 3H5v18h14V8l-5-5v5h5M9 12h6M9 16h6"></path></symbol>
|
||||
<symbol id="project-icon-inbox" viewBox="0 0 24 24"><path d="m3 14 3-9h12l3 9v6H3v-6h5l2 3h4l2-3h5"></path></symbol>
|
||||
<symbol id="ui-icon-arrow-left" viewBox="0 0 24 24"><path d="M20 12H4m6-6-6 6 6 6"/></symbol>
|
||||
<symbol id="ui-icon-arrow-right" viewBox="0 0 24 24"><path d="M4 12h16m-6-6 6 6-6 6"/></symbol>
|
||||
<symbol id="ui-icon-check" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6"/></symbol>
|
||||
<symbol id="ui-icon-check-circle" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="m7 12 3 3 7-7"/></symbol>
|
||||
<symbol id="ui-icon-edit" viewBox="0 0 24 24"><path d="m15 4 5 5M4 20l5-1L21 7l-5-5L4 14v6Z"/></symbol>
|
||||
<symbol id="ui-icon-external-link" viewBox="0 0 24 24"><path d="M14 3h7v7m0-7L10 14M10 3H3v18h18v-7"/></symbol>
|
||||
<symbol id="ui-icon-key" viewBox="0 0 24 24"><circle cx="8" cy="8" r="5"/><path d="m12 12 9 9m-6-6 3-3m0 6 3-3"/></symbol>
|
||||
<symbol id="ui-icon-refresh" viewBox="0 0 24 24"><path d="M20 8a8 8 0 0 0-14-3L3 8m0-5v5h5M4 16a8 8 0 0 0 14 3l3-3m0 5v-5h-5"/></symbol>
|
||||
<symbol id="ui-icon-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7h.01"/></symbol>
|
||||
<symbol id="ui-icon-warning" viewBox="0 0 24 24"><path d="m12 3 10 18H2L12 3Zm0 6v5m0 3h.01"/></symbol>
|
||||
<symbol id="ui-icon-help" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M9 9a3 3 0 0 1 6 0c0 2-3 2-3 4m0 4h.01"/></symbol>
|
||||
<symbol id="ui-icon-user" viewBox="0 0 24 24"><circle cx="12" cy="7" r="4"/><path d="M4 21v-3a8 8 0 0 1 16 0v3"/></symbol>
|
||||
<symbol id="ui-icon-users" viewBox="0 0 24 24"><circle cx="9" cy="7" r="4"/><path d="M2 21v-3a7 7 0 0 1 14 0v3M16 3a4 4 0 0 1 0 8m3 3a6 6 0 0 1 3 5v2"/></symbol>
|
||||
<symbol id="ui-icon-shield" viewBox="0 0 24 24"><path d="m12 3 9 4v6c0 4-9 9-9 9S3 17 3 13V7l9-4Z"/></symbol>
|
||||
<symbol id="ui-icon-code" viewBox="0 0 24 24"><path d="m7 6-6 6 6 6m10-12 6 6-6 6m-4-14-2 16"/></symbol>
|
||||
<symbol id="ui-icon-comment" viewBox="0 0 24 24"><path d="M21 3H3v14h5l4 4v-4h9V3Z"/><path d="M7 8h10M7 12h6"/></symbol>
|
||||
<symbol id="ui-icon-save" viewBox="0 0 24 24"><path d="M3 3h14l4 4v14H3V3Zm4 0v7h10V3M7 21v-7h10v7"/></symbol>
|
||||
<symbol id="ui-icon-heart" viewBox="0 0 24 24"><path d="M12 21 3 12C-2 6 7-1 12 6c5-7 14 0 9 6l-9 9Z"/></symbol>
|
||||
<symbol id="ui-icon-globe" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><ellipse cx="12" cy="12" rx="4" ry="9"/><path d="M3 12h18"/></symbol>
|
||||
<symbol id="ui-icon-hourglass" viewBox="0 0 24 24"><path d="M5 3h14M5 21h14M6 3v4l6 5 6-5V3M6 21v-4l6-5 6 5v4"/></symbol>
|
||||
<symbol id="ui-icon-moon" viewBox="0 0 24 24"><path d="M20 14A9 9 0 0 1 10 3a9 9 0 1 0 10 11Z"/></symbol>
|
||||
<symbol id="ui-icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 1v2m0 18v2M1 12h2m18 0h2M4 4l2 2m12 12 2 2M4 20l2-2M18 6l2-2"/></symbol>
|
||||
<symbol id="ui-icon-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></symbol>
|
||||
<symbol id="ui-icon-logout" viewBox="0 0 24 24"><path d="M9 3H3v18h6M9 12h12m-4-4 4 4-4 4"/></symbol>
|
||||
<symbol id="ui-icon-link" viewBox="0 0 24 24"><path d="m10 7 3-3a5 5 0 0 1 7 7l-3 3M7 10l-3 3a5 5 0 0 0 7 7l3-3M8 16l8-8"/></symbol>
|
||||
<symbol id="ui-icon-chart" viewBox="0 0 24 24"><path d="M3 3v18h18M7 16V9m5 7V5m5 11v-5"/></symbol>
|
||||
<symbol id="ui-icon-list" viewBox="0 0 24 24"><path d="M9 5h12M9 12h12M9 19h12m-18-14 1 1 2-2m-3 8 1 1 2-2m-3 8 1 1 2-2"/></symbol>
|
||||
<symbol id="ui-icon-bug" viewBox="0 0 24 24"><rect x="7" y="7" width="10" height="14" rx="5"/><path d="M9 7V5a3 3 0 0 1 6 0v2M12 7v14M3 8l4 2m-4 5h4m-4 7 4-4M21 8l-4 2m4 5h-4m4 7-4-4"/></symbol>
|
||||
<symbol id="ui-icon-chevron-left" viewBox="0 0 24 24"><path d="m15 5-7 7 7 7"/></symbol>
|
||||
<symbol id="ui-icon-chevron-right" viewBox="0 0 24 24"><path d="m9 5 7 7-7 7"/></symbol>
|
||||
<symbol id="ui-icon-download" viewBox="0 0 24 24"><path d="M12 3v12m-5-5 5 5 5-5M3 16v5h18v-5"/></symbol>
|
||||
<symbol id="ui-icon-export" viewBox="0 0 24 24"><path d="M13 3H3v18h14v-6M12 9h9m-4-4 4 4-4 4"/></symbol>
|
||||
<symbol id="ui-icon-clipboard" viewBox="0 0 24 24"><path d="M8 5H4v16h16V5h-4"/><rect x="8" y="2" width="8" height="6" rx="2"/></symbol>
|
||||
<symbol id="ui-icon-ban" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="m6 6 12 12"/></symbol>
|
||||
<symbol id="ui-icon-trash" viewBox="0 0 24 24"><path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/></symbol>
|
||||
<symbol id="ui-icon-broom" viewBox="0 0 24 24"><path d="m16 3-5 8m-7 5 6-6 6 4-3 7H3l1-5Zm1 5 4-6"/></symbol>
|
||||
<symbol id="ui-icon-database" viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 4 18 4 18 0V5M3 12c0 4 18 4 18 0"/></symbol>
|
||||
<symbol id="ui-icon-user-minus" viewBox="0 0 24 24"><circle cx="9" cy="7" r="4"/><path d="M2 21v-3a7 7 0 0 1 14 0v3m0-11h7"/></symbol>
|
||||
<symbol id="ui-icon-user-shield" viewBox="0 0 24 24"><circle cx="8" cy="6" r="4"/><path d="M1 21v-4a7 7 0 0 1 10-6m7 0 5 2v4c0 3-5 6-5 6s-5-3-5-6v-4l5-2Z"/></symbol>
|
||||
<symbol id="ui-icon-spinner" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-9-9"/></symbol>
|
||||
<symbol id="ui-icon-archive" viewBox="0 0 24 24"><path d="M3 3h18v5H3V3Zm2 5v13h14V8M10 3v3m4 3v3m-4 3v3m4 0v3"/></symbol>
|
||||
<symbol id="ui-icon-calendar" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M7 2v6m10-6v6M3 11h18"/></symbol>
|
||||
<symbol id="ui-icon-search" viewBox="0 0 24 24"><circle cx="10" cy="10" r="7"></circle><path d="m15 15 7 7"></path></symbol>
|
||||
<symbol id="ui-icon-folder" viewBox="0 0 24 24"><path d="M3 20V4h6l2 3h10v13H3Z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -37,7 +37,7 @@
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="url">Website</label>
|
||||
<input type="url" class="form-control" name="url" id="url" placeholder="https://example.org" v-field="{ state: viewState, set: value => { options.url = value }, value: options?.url, form: "conference", options: {} }" :class="{'is-invalid': conference?.url?.invalid && (conference?.url?.touched || conference?.submitted)}">
|
||||
<div class="invalid-feedback" v-show="conference?.url?.errors?.invalid">
|
||||
<div class="invalid-feedback" v-show="conference?.url?.errors?.url">
|
||||
The url of the conference is invalid.
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,7 +108,8 @@
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.options.notebook = value }, value: options?.options?.notebook, form: "conference", options: {} }">
|
||||
<label class="form-check-label" for="notebook">Display Notebooks</label>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
@@ -189,8 +190,8 @@
|
||||
<div class="paper-settings-footer">
|
||||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||||
<div class="alert alert-success" role="alert" v-if="message" v-text="message"></div>
|
||||
<button id="send" type="submit" class="btn btn-ink" @click.prevent="submitForm($event, () => { submit($event) })">
|
||||
{{ editionMode ? "Update" : "Create" }} conference
|
||||
<button id="send" :disabled="saving" type="submit" class="btn btn-ink" @click.prevent="submitForm($event, () => { submit($event) })">
|
||||
{{ saving ? (editionMode ? "Updating…" : "Creating…") : (editionMode ? "Update conference" : "Create conference") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div v-code-editor="{ content: content, options: aceOption }" v-if="type == 'text'"></div>
|
||||
<div class="file-content markdown-body" v-if="type == 'html'" v-html="sanitize(content)"></div>
|
||||
<div class="file-content markdown-body" v-if="type == 'html'" v-html="highlightRedactions(sanitize(content))"></div>
|
||||
<html-doc v-if="type == 'html-doc' && !showSource && content != null" :content="content" :base-url="(fileBaseUrl)" :allow-scripts="allowScripts"></html-doc>
|
||||
<div v-code-editor="{ content: content, options: aceOption }" v-if="(type == 'code' || (type == 'html-doc' && showSource)) && content != null"></div>
|
||||
<img class="image-content" v-if="type == 'image'" :src="safeUrl((url))">
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'rate_limited'">
|
||||
<div class="paper-ratelimit-card m-auto" style="max-width:520px;">
|
||||
<div class="paper-ratelimit-head">
|
||||
<i class="fas fa-hourglass-half"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-hourglass"></use></svg>
|
||||
<div>
|
||||
<div class="paper-error-eyebrow">Temporarily paused</div>
|
||||
<div class="paper-error-title">GitHub API rate limit reached</div>
|
||||
@@ -19,7 +19,17 @@
|
||||
<p class="paper-error-msg">This repository will be available in <strong>{{ rateLimitCountdown }}</strong>. The page will reload automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'error'"><h1 class="paper-empty-title m-auto" v-text="fmt.translate("ERRORS." + (content))">Error</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'error'">
|
||||
<div class="paper-error-card file-error-card m-auto" role="alert">
|
||||
<div class="paper-error-eyebrow">Unavailable</div>
|
||||
<div class="paper-error-title">This content can’t be shown</div>
|
||||
<p class="file-error-msg" v-text="fmt.translate("ERRORS." + (content))"></p>
|
||||
<div class="file-error-actions">
|
||||
<a href="/" class="btn btn-sm">Back to home</a>
|
||||
<a href="/faq" class="btn btn-sm">Read the FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'loading' && !error"><h1 class="paper-empty-title m-auto">Loading…</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'empty'"><h1 class="paper-empty-title m-auto">Empty <em>repository</em>.</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="content == null && type != 'empty'"><h1 class="paper-empty-title m-auto">Empty <em>file</em>.</h1></div>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
</div>
|
||||
<div class="d-flex flex-wrap" style="gap: 6px">
|
||||
<a href="/anonymize" class="btn btn-ink">
|
||||
<i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> Anonymize Repo
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-plus"></use></svg> Anonymize Repo
|
||||
</a>
|
||||
<a href="/pull-request-anonymize" class="btn btn-ink">
|
||||
<i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> Anonymize PR
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-plus"></use></svg> Anonymize PR
|
||||
</a>
|
||||
<a title="Claim the ownership of an existing anonymized repository." data-toggle="tooltip" data-placement="bottom" href="/claim" class="btn">
|
||||
Claim
|
||||
@@ -113,19 +113,19 @@
|
||||
</div>
|
||||
<div class="repo-meta">
|
||||
<span title="Conference" v-if="pr?.conference">
|
||||
<i class="fas fa-chalkboard-teacher"></i> {{ pr?.conference }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ pr?.conference }}
|
||||
</span>
|
||||
<span data-toggle="tooltip" data-placement="bottom" :title=""Terms: " + (pr?.options?.terms?.join(', '))">
|
||||
<i class="fas fa-shield-alt"></i> {{ fmt?.number(pr?.options?.terms?.length) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> {{ fmt?.number(pr?.options?.terms?.length) }}
|
||||
</span>
|
||||
<span data-toggle="tooltip" data-placement="bottom" :title=""Views: " + (fmt?.number(pr?.pageView))">
|
||||
<i class="far fa-eye"></i> {{ fmt?.number(pr?.pageView) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> {{ fmt?.number(pr?.pageView) }}
|
||||
</span>
|
||||
<span data-toggle="tooltip" data-placement="bottom" :title=""Last view: " + (fmt?.date(pr?.lastView))">
|
||||
<i class="far fa-calendar-alt"></i> {{ fmt?.humanTime(pr?.lastView) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-calendar"></use></svg> {{ fmt?.humanTime(pr?.lastView) }}
|
||||
</span>
|
||||
<span v-if="pr?.options?.expirationMode!='never' && pr?.status == 'ready'">
|
||||
<i class="far fa-clock"></i> Expire: {{ fmt?.humanTime(pr?.options?.expirationDate) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> Expire: {{ fmt?.humanTime(pr?.options?.expirationDate) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,26 +136,26 @@
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/pull-request-anonymize/" + (pr?.pullRequestId))">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" v-show="pr?.status == 'ready' || pr?.status == 'error'" @click.prevent="updatePullRequest(pr)">
|
||||
<i class="fas fa-sync"></i> Force update
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force update
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" v-show="pr?.status == 'removed'" @click.prevent="updatePullRequest(pr)">
|
||||
<i class="fas fa-check-circle"></i> Enable
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Enable
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" v-show="pr?.status == 'ready'" @click.prevent="removePullRequest(pr)">
|
||||
<i class="fas fa-trash-alt"></i> Remove
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove
|
||||
</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/pr/" + (pr?.pullRequestId) + "/")">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View PR
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View PR
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="repo-list-empty" v-if="filteredPullRequests?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No pull requests to display.</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -17,32 +17,11 @@
|
||||
</aside>
|
||||
|
||||
<div class="paper-settings-main">
|
||||
<!-- Quota: same markup as the dashboard, driven by quotaService -->
|
||||
<!-- Account usage shares the dashboard component. -->
|
||||
<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" v-if="quota">
|
||||
<div class="quota-item" v-for="(q, index) in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes'},
|
||||
{key: 'file', label: 'Files', kind: 'count'}
|
||||
]">
|
||||
<div class="quota-header">
|
||||
<span class="quota-label">{{ q?.label }}</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'count'">
|
||||
{{ fmt?.number(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.number(quota[q?.key].total) }}</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'bytes'">
|
||||
{{ fmt?.humanFileSize(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.humanFileSize(quota[q?.key].total) }}</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="quota-track" role="progressbar" aria-valuemin="0" :class="'quota-' + quota[q?.key].level" :aria-label="(q?.label) + " quota"" :aria-valuenow="(quota[q?.key].used)" :aria-valuemax="(quota[q?.key].unlimited ? quota[q?.key].used : quota[q?.key].total)" :aria-valuetext="(quota[q?.key].unlimited ? 'unlimited' : fmt.number(quota[q?.key].percent, 0) + '% used')">
|
||||
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<quota-usage :quota="quota" :fmt="fmt" v-if="quota"></quota-usage>
|
||||
<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>
|
||||
@@ -143,11 +122,13 @@ jane.smith@example.org" v-field="{ state: viewState, set: value => { terms =
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
|
||||
<div class="draft-status" v-else-if="unsavedChanges">{{ draftSaved ? "Draft saved in this tab for 30 minutes." : "Unsaved changes. Keep this tab open until you save." }} <button type="button" class="draft-discard" @click="discardDraft()">{{ isUpdate ? "Restore saved settings" : "Discard changes" }}</button></div>
|
||||
<div class="paper-settings-footer">
|
||||
<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-inline-alert paper-inline-alert-error" role="alert" v-if="error"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> <span v-text="error"></span></div>
|
||||
<div class="paper-settings-actions">
|
||||
<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') }}
|
||||
<svg class="ui-icon mr-1" aria-hidden="true" v-if="message"><use href="#ui-icon-check"></use></svg>{{ saving ? 'Saving…' : (message ? 'Saved' : 'Save defaults') }}
|
||||
</button>
|
||||
<span class="paper-settings-hint">Applies to new anonymizations only. Existing ones keep their settings.</span>
|
||||
</div>
|
||||
@@ -178,9 +159,9 @@ jane.smith@example.org" v-field="{ state: viewState, set: value => { terms =
|
||||
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" v-if="deleteError"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <span v-text="deleteError"></span></div>
|
||||
<div class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="deleteError"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> <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' }}
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-trash"></use></svg>{{ deletingAccount ? 'Deleting…' : 'Delete my account' }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
</div>
|
||||
|
||||
<header class="pr-header">
|
||||
<div class="pr-title-row">
|
||||
<h1 class="paper-page-title pr-title">
|
||||
<span v-if="details?.title" v-text="details?.title"></span>
|
||||
<span class="text-muted" v-if="!details?.title">Untitled pull request</span>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/pull-request-anonymize/" + (pullRequestId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</h1>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/pull-request-anonymize/" + (pullRequestId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</div>
|
||||
<div class="pr-header-meta">
|
||||
<span class="paper-pill" :class="{'good': details?.merged, 'warn': details?.state == 'open', 'bad': details?.state == 'closed' && !details?.merged}">
|
||||
<span class="status-dot" :class="{'status-ready': details?.merged, 'status-error': details?.state == 'closed' && !details?.merged}"></span>
|
||||
@@ -20,10 +22,10 @@
|
||||
<i class="fab fa-github"></i> <span v-text="details?.baseRepositoryFullName"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" v-if="details?.updatedDate">
|
||||
<i class="far fa-clock"></i> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
</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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
@@ -34,26 +36,26 @@
|
||||
</section>
|
||||
|
||||
<nav class="paper-tabs" role="tablist" v-if="details?.diff || details?.comments">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.diff" :class="{'active': tabState?.active == 'diff'}" @click="tabState.active = 'diff'">
|
||||
<i class="fas fa-code"></i> Diff
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.diff" :class="{'active': tabState?.active == 'diff'}" id="pr-diff-tab" :aria-selected="tabState?.active === 'diff'" aria-controls="pr-diff-panel" :tabindex="tabState?.active === 'diff' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="tabState.active = 'diff'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Diff
|
||||
</button>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" @click="tabState.active = 'comments'">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" id="pr-comments-tab" :aria-selected="tabState?.active === 'comments'" aria-controls="pr-comments-panel" :tabindex="tabState?.active === 'comments' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="tabState.active = 'comments'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span v-text="fmt.plural(details?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="paper-tab-content">
|
||||
<div v-if="details?.diff && tabState?.active =='diff'">
|
||||
<div role="tabpanel" id="pr-diff-panel" aria-labelledby="pr-diff-tab" v-if="details?.diff && tabState?.active =='diff'">
|
||||
<div class="pr-diff" v-html="sanitize(fmt?.diff(details?.diff))"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="details?.comments && tabState?.active =='comments'">
|
||||
<div role="tabpanel" id="pr-comments-panel" aria-labelledby="pr-comments-tab" v-if="details?.comments && tabState?.active =='comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="comment?.author">
|
||||
<i class="far fa-user"></i> @<span v-text="comment?.author"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="comment?.author"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="comment?.updatedDate" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
@@ -62,7 +64,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="paper-table-empty" v-if="!details?.comments?.length">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span>No comments on this pull request.</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="quota-row quota-usage" v-if="quota">
|
||||
<div class="quota-item" :class="'quota-' + quota[q.key].level" :key="q.key" v-for="q in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count', icon: 'project-type-repo'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes', icon: 'project-icon-storage'},
|
||||
{key: 'file', label: 'Files', kind: 'count', icon: 'project-icon-file'}
|
||||
]">
|
||||
<div class="quota-header">
|
||||
<span class="quota-label"><svg class="project-type-svg" aria-hidden="true"><use :href="'#' + q.icon"></use></svg>{{ q.label }}</span>
|
||||
<span class="quota-percent" v-if="!quota[q.key].unlimited">{{ fmt.number(quota[q.key].percent, 0) }}% used</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q.key].unlimited">Unlimited</span>
|
||||
</div>
|
||||
<div class="quota-value"><strong>{{ formatAmount(quota[q.key].used, q.kind) }}</strong><span v-if="!quota[q.key].unlimited"> / {{ formatAmount(quota[q.key].total, q.kind) }}</span></div>
|
||||
<div class="quota-track" v-if="!quota[q.key].unlimited" role="progressbar" aria-valuemin="0" :class="'quota-' + quota[q.key].level" :aria-label="q.label + ' quota'" :aria-valuenow="Math.min(quota[q.key].used, quota[q.key].total)" :aria-valuemax="quota[q.key].total" :aria-valuetext="fmt.number(quota[q.key].percent, 0) + '% used'">
|
||||
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q.key].percent + '%'}"></div>
|
||||
</div>
|
||||
<p class="quota-remaining" v-if="!quota[q.key].unlimited && quota[q.key].used < quota[q.key].total">{{ formatAmount(quota[q.key].total - quota[q.key].used, q.kind) }} {{ q.key === 'repository' ? 'repositories available' : q.key === 'file' ? 'files available' : 'available' }}</p>
|
||||
<p class="quota-remaining quota-limit" v-if="!quota[q.key].unlimited && quota[q.key].used >= quota[q.key].total">{{ quota[q.key].used > quota[q.key].total ? 'Limit exceeded' : 'Limit reached' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container paper-page auth-page">
|
||||
<a class="auth-back" href="/"><i class="fas fa-arrow-left" aria-hidden="true"></i> Anonymous GitHub</a>
|
||||
<a class="auth-back" href="/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-left"></use></svg> Anonymous GitHub</a>
|
||||
<div class="auth-layout">
|
||||
<header class="auth-intro">
|
||||
<div class="paper-eyebrow">Your research, ready for review</div>
|
||||
@@ -18,16 +18,16 @@
|
||||
<h2>Continue with GitHub</h2>
|
||||
<p>Connect through the GitHub App with read-only repository access.</p>
|
||||
<ul class="auth-permissions">
|
||||
<li><i class="fas fa-check" aria-hidden="true"></i> You choose which repositories to share</li>
|
||||
<li><i class="fas fa-check" aria-hidden="true"></i> No permission to write to your code</li>
|
||||
<li><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check"></use></svg> You choose which repositories to share</li>
|
||||
<li><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check"></use></svg> No permission to write to your code</li>
|
||||
</ul>
|
||||
<a class="btn btn-ink auth-continue" target="_self" href="/github/app/login">Sign in with GitHub <i class="fas fa-arrow-right" aria-hidden="true"></i></a>
|
||||
<a class="btn btn-ink auth-continue" target="_self" href="/github/app/login">Sign in with GitHub <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></a>
|
||||
<small>Your existing anonymizations and connections stay with your account.</small>
|
||||
</section>
|
||||
<section class="auth-legacy" v-if="!site_options?.GITHUB_APP_ENABLED && site_options?.GITHUB_OAUTH_ENABLED && !accountRecovery">
|
||||
<div class="connection-title"><h2>Continue with GitHub</h2></div>
|
||||
<p>Sign in to access your account. GitHub OAuth includes write access to private repositories.</p>
|
||||
<a class="connection-link" target="_self" href="/github/login">Sign in with GitHub <i class="fas fa-arrow-right" aria-hidden="true"></i></a>
|
||||
<a class="connection-link" target="_self" href="/github/login">Sign in with GitHub <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></a>
|
||||
</section>
|
||||
<p role="status" v-if="site_options && !site_options.GITHUB_APP_ENABLED && !site_options.GITHUB_OAUTH_ENABLED">GitHub sign-in is currently unavailable. Please try again later.</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="container paper-page">
|
||||
<div class="container paper-page status-page">
|
||||
<div class="paper-crumbs">Anonymization / <span class="here">Status</span></div>
|
||||
<div class="d-flex align-items-end justify-content-between flex-wrap" style="gap: 12px;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap" style="gap: 12px;">
|
||||
<div>
|
||||
<h1 class="paper-page-title">Status of <em>{{ repoId }}</em></h1>
|
||||
<p class="paper-page-lede">Track progress as your anonymization is prepared.</p>
|
||||
@@ -13,16 +13,16 @@
|
||||
</div>
|
||||
|
||||
<section class="paper-settings-section">
|
||||
<div class="paper-section-eyebrow">Progress</div>
|
||||
<div class="paper-section-eyebrow" v-text="repo?.status == 'ready' ? 'Your anonymous link' : 'Progress'"></div>
|
||||
|
||||
<p class="paper-section-copy">
|
||||
<p class="paper-section-copy" v-if="repo?.status != 'ready'">
|
||||
The repository will take a few minutes to get ready, depending on its
|
||||
size. Visit the <a href="/faq">FAQ</a> for more information.
|
||||
</p>
|
||||
|
||||
<div class="paper-ratelimit-card" role="status" v-if="rateLimitResetAt">
|
||||
<div class="paper-ratelimit-head">
|
||||
<i class="fas fa-hourglass-half"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-hourglass"></use></svg>
|
||||
<div>
|
||||
<div class="paper-error-eyebrow">Temporarily paused</div>
|
||||
<div class="paper-error-title">GitHub API rate limit reached</div>
|
||||
@@ -31,7 +31,7 @@
|
||||
<p class="paper-error-msg">Anonymization will resume automatically in <strong>{{ rateLimitCountdown }}</strong>. No action needed — the job is queued and will continue where it left off.</p>
|
||||
</div>
|
||||
|
||||
<div class="paper-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" v-if="repo?.status != 'error' && !rateLimitResetAt" :class="{'paper-progress-ready': repo?.status == 'ready'}" :aria-valuenow="(progress)">
|
||||
<div class="paper-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" v-if="repo?.status != 'error' && repo?.status != 'ready' && !rateLimitResetAt" :class="{'paper-progress-ready': repo?.status == 'ready'}" :aria-valuenow="(progress)">
|
||||
<div class="paper-progress-bar" :style=""width: " + (progress) + "%;""></div>
|
||||
<div class="paper-progress-label">
|
||||
<span v-text="fmt?.title(repo?.status)"></span><span v-if="repo?.statusMessage"> · <span v-text="repo?.statusMessage"></span></span>
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<div class="paper-error-card" role="alert" v-if="repo?.status == 'error' && !rateLimitResetAt">
|
||||
<div class="paper-error-head">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
||||
<div>
|
||||
<div class="paper-error-eyebrow">Anonymization failed</div>
|
||||
<div class="paper-error-title">Something went wrong while preparing this repository.</div>
|
||||
@@ -55,31 +55,30 @@
|
||||
<li>If you just signed in, the access token may need a moment to propagate — try again.</li>
|
||||
</ul>
|
||||
<div class="paper-error-actions">
|
||||
<a class="btn btn-ink" :href="safeUrl("/anonymize/" + (repoId))"><i class="far fa-edit mr-1"></i> Edit anonymization</a>
|
||||
<a class="btn" href="/faq"><i class="far fa-question-circle mr-1"></i> Read the FAQ</a>
|
||||
<a class="btn" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><i class="fas fa-bug mr-1" aria-hidden="true"></i> Report a bug in Anonymous GitHub</a>
|
||||
<a class="btn btn-ink" :href="safeUrl("/anonymize/" + (repoId))"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit anonymization</a>
|
||||
<a class="btn" href="/faq"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-help"></use></svg> Read the FAQ</a>
|
||||
<a class="btn" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Report a bug in Anonymous GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paper-detail-grid">
|
||||
<div class="detail-label">Repository</div>
|
||||
<div class="detail-value">
|
||||
<a target="_self" :href="safeUrl("/r/" + (repoId) + "/")">/r/{{ repoId }}/</a>
|
||||
<div class="share-link" v-if="repo?.status == 'ready'">
|
||||
<p class="paper-section-copy">Reviewers can browse the repository at this address without seeing who you are.</p>
|
||||
<div class="share-link-row">
|
||||
<input id="share-url" class="form-control share-link-input" readonly aria-label="Anonymous link" :value="shareUrl" @focus="$event.target.select()">
|
||||
<button type="button" class="btn btn-ink" @click="copyShareUrl()">
|
||||
<svg class="ui-icon" aria-hidden="true"><use :href="copied ? '#ui-icon-check' : '#project-icon-copy'"></use></svg>
|
||||
<span aria-live="polite" v-text="copied ? 'Copied' : 'Copy link'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="detail-label" v-if="repo?.options?.page">GitHub Page</div>
|
||||
<div class="detail-value" v-if="repo?.options?.page">
|
||||
<a target="_self" :href="safeUrl("/w/" + (repoId) + "/")">/w/{{ repoId }}/</a>
|
||||
<div class="share-link-actions">
|
||||
<a class="btn" target="_self" :href="safeUrl("/r/" + (repoId) + "/")">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> Open repository
|
||||
</a>
|
||||
<a class="btn" target="_self" v-if="repo?.options?.page" :href="safeUrl("/w/" + (repoId) + "/")">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-globe"></use></svg> Open GitHub page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="anonymize-submit-bar" v-if="repo?.status == 'ready'">
|
||||
<a class="btn btn-ink" target="_self" :href="safeUrl("/r/" + (repoId) + "/")">
|
||||
<i class="far fa-eye mr-1"></i> Go to anonymized repository
|
||||
</a>
|
||||
<a class="btn" target="_self" v-if="repo?.options?.page" :href="safeUrl("/w/" + (repoId) + "/")">
|
||||
<i class="fas fa-globe mr-1"></i> Go to anonymized GitHub page
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="paper-settings-section">
|
||||
@@ -91,24 +90,20 @@
|
||||
|
||||
<div class="paper-support-grid">
|
||||
<a class="paper-support-card" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">
|
||||
<div class="paper-support-eyebrow"><i class="fas fa-code-branch"></i> Contribute</div>
|
||||
<div class="paper-support-eyebrow"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Contribute</div>
|
||||
<p>Collaborate by implementing new features and fixing bugs. Help with new file formats or deployment is welcome.</p>
|
||||
<span class="paper-support-cta">Open on GitHub <i class="fas fa-arrow-right"></i></span>
|
||||
<span class="paper-support-cta">Open on GitHub <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></span>
|
||||
</a>
|
||||
<a class="paper-support-card" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">
|
||||
<div class="paper-support-eyebrow"><i class="far fa-comment-dots"></i> Feedback</div>
|
||||
<div class="paper-support-eyebrow"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg> Feedback</div>
|
||||
<p>Tell us about bugs and missing features in Anonymous GitHub itself. Questions about an anonymized repository go to its authors.</p>
|
||||
<span class="paper-support-cta">File an issue <i class="fas fa-arrow-right"></i></span>
|
||||
<span class="paper-support-cta">File an issue <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></span>
|
||||
</a>
|
||||
<a class="paper-support-card" href="https://github.com/sponsors/tdurieux/" target="_blank" rel="noopener">
|
||||
<div class="paper-support-eyebrow"><i class="fas fa-heart"></i> Sponsor</div>
|
||||
<div class="paper-support-eyebrow"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg> Sponsor</div>
|
||||
<p>Server costs hover around $25 a month. Any contribution helps keep the lights on.</p>
|
||||
<span class="paper-support-cta">GitHub Sponsors <i class="fas fa-arrow-right"></i></span>
|
||||
<span class="paper-support-cta">GitHub Sponsors <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></span>
|
||||
</a>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1734,6 +1734,50 @@ export const overviewAdminController = function (state, http, location, interval
|
||||
state.data = null;
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
state.performance = null;
|
||||
state.performanceMinutes = 15;
|
||||
state.performanceTab = "routes";
|
||||
state.performanceError = null;
|
||||
// 18400 ms reads better as 18.4 s once a value passes one second.
|
||||
state.formatMs = function (ms) {
|
||||
if (ms == null) return "";
|
||||
return ms < 1000 ? ms + " ms" : +(ms / 1000).toFixed(1) + " s";
|
||||
};
|
||||
state.latencyBound = function (row, field) {
|
||||
if (!row.count) return "No samples";
|
||||
return row[field] == null ? "> 60 s" : "\u2264 " + state.formatMs(row[field]);
|
||||
};
|
||||
state.firstByteBound = function (row) {
|
||||
return state.latencyBound({ count: row.firstByteCount, p95UpperMs: row.firstByteP95UpperMs }, "p95UpperMs");
|
||||
};
|
||||
state.sampleAge = function (sample) { return Math.max(0, Math.round((Date.now() - sample.sampledAt) / 1000)); };
|
||||
state.peakRss = function (sample) {
|
||||
return (state.performance.runtimeSeries || []).filter(function (point) { return point.instance === sample.instance; })
|
||||
.reduce(function (peak, point) { return Math.max(peak, point.rss); }, sample.memory.rss);
|
||||
};
|
||||
state.performanceWarning = function (sample) {
|
||||
return state.sampleAge(sample) > 45 || sample.droppedBatches || sample.droppedMetrics ||
|
||||
sample.sockets?.closeWait > 0 || sample.eventLoop.p95Ms > 100 ||
|
||||
sample.memoryLimitBytes && sample.memory.rss > sample.memoryLimitBytes * 0.8;
|
||||
};
|
||||
var performanceLoading = false;
|
||||
state.loadPerformance = function () {
|
||||
var minutes = state.performanceMinutes;
|
||||
if (state.performance?.windowMinutes && state.performance.windowMinutes !== minutes) state.performance = null;
|
||||
if (performanceLoading) return;
|
||||
performanceLoading = true;
|
||||
http.get("/api/admin/performance", { params: { minutes: minutes } }).then(function (r) {
|
||||
if (minutes !== state.performanceMinutes) return;
|
||||
state.performance = r.data;
|
||||
state.performanceError = r.data.available ? null : "Performance monitoring is unavailable. Request history may be incomplete.";
|
||||
}, function () {
|
||||
if (minutes !== state.performanceMinutes) return;
|
||||
state.performanceError = "Could not refresh performance monitoring. Displayed samples may be stale.";
|
||||
}).finally(function () {
|
||||
performanceLoading = false;
|
||||
if (minutes !== state.performanceMinutes) state.loadPerformance();
|
||||
});
|
||||
};
|
||||
|
||||
function humanBytes(b) {
|
||||
if (b == null) return "—";
|
||||
@@ -1823,10 +1867,22 @@ export const overviewAdminController = function (state, http, location, interval
|
||||
}
|
||||
|
||||
var historyMaxes = {};
|
||||
var historyMins = {};
|
||||
state.historyBarH = function (d, field) {
|
||||
if (!d || !historyMaxes[field]) return 0;
|
||||
return Math.max(1, Math.round((d[field] / historyMaxes[field]) * 140));
|
||||
};
|
||||
// Cumulative totals barely move against a zero baseline, so they are
|
||||
// drawn between the period's minimum and maximum (shown in the legend).
|
||||
state.historyRangeH = function (d, field) {
|
||||
if (!d || historyMaxes[field] == null) return 0;
|
||||
var span = historyMaxes[field] - historyMins[field];
|
||||
if (!span) return 70;
|
||||
return Math.round(8 + ((d[field] - historyMins[field]) / span) * 132);
|
||||
};
|
||||
state.historyRange = function (field) {
|
||||
return { min: historyMins[field], max: historyMaxes[field] };
|
||||
};
|
||||
state.historyLabel = function (d) {
|
||||
if (!d || !d.date) return "";
|
||||
var dt = new Date(d.date);
|
||||
@@ -1834,6 +1890,7 @@ export const overviewAdminController = function (state, http, location, interval
|
||||
};
|
||||
|
||||
function load() {
|
||||
state.loadPerformance();
|
||||
http.get("/api/admin/overview").then(function (r) {
|
||||
r.data.history = computeDailyHistory(r.data.history);
|
||||
r.data.daily = {
|
||||
@@ -1843,9 +1900,11 @@ export const overviewAdminController = function (state, http, location, interval
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
historyMaxes = {};
|
||||
historyMins = {};
|
||||
(r.data.history || []).forEach(function (d) {
|
||||
["dailyPageViews", "dailyRepositories", "dailyUsers", "nbUsers"].forEach(function (k) {
|
||||
if (!historyMaxes[k] || d[k] > historyMaxes[k]) historyMaxes[k] = d[k];
|
||||
if (historyMins[k] == null || d[k] < historyMins[k]) historyMins[k] = d[k];
|
||||
});
|
||||
});
|
||||
}, function (err) {
|
||||
|
||||
@@ -2,6 +2,18 @@ import { loadLibrary, loadEditor } from "./lazy-assets.js";
|
||||
import { h, ref, watch, onMounted, onBeforeUnmount, nextTick, defineAsyncComponent } from "vue";
|
||||
import HtmlDoc from "./html-doc.js";
|
||||
import PdfViewer from "./pdf-viewer.js";
|
||||
import { render as quotaUsageTemplate } from "../partials/quotaUsage.htm";
|
||||
|
||||
const QuotaUsage = {
|
||||
props: ["quota", "fmt"],
|
||||
setup(props) {
|
||||
return { formatAmount: (value, kind) => kind === "bytes"
|
||||
? props.fmt.humanFileSize(value).replace(/([a-zA-Z]+)$/, " $1") : props.fmt.number(value) };
|
||||
},
|
||||
render: quotaUsageTemplate,
|
||||
};
|
||||
|
||||
const editorModes = new Set(__ACE_MODES__);
|
||||
|
||||
const Markdown = {
|
||||
props: ["content", "terms", "options"],
|
||||
@@ -66,7 +78,7 @@ const Loc = {
|
||||
};
|
||||
},
|
||||
};
|
||||
export const components = { Markdown, GistFile, Notebook, Loc, HtmlDoc, Pdfviewer: defineAsyncComponent(async () => {
|
||||
export const components = { Markdown, GistFile, Notebook, Loc, HtmlDoc, QuotaUsage, Pdfviewer: defineAsyncComponent(async () => {
|
||||
await loadLibrary("pdf");
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "/script/external/pdf.worker.js";
|
||||
return PdfViewer;
|
||||
@@ -83,7 +95,7 @@ export const codeEditor = {
|
||||
el._editor = editor;
|
||||
editor.setValue(String(latest.content ?? ""), -1);
|
||||
applyEditorOptions(el, latest.options);
|
||||
latest.options?.onLoad?.(editor);
|
||||
el._editorCleanup = latest.options?.onLoad?.(editor);
|
||||
} catch (error) {
|
||||
if (!el._editorDisposed) el.textContent = error.message;
|
||||
}
|
||||
@@ -94,10 +106,11 @@ export const codeEditor = {
|
||||
if (el._editor.getValue() !== String(value.content ?? "")) el._editor.setValue(String(value.content ?? ""), -1);
|
||||
applyEditorOptions(el, value.options);
|
||||
},
|
||||
beforeUnmount(el) { el._editorDisposed = true; el._editor?.destroy(); },
|
||||
beforeUnmount(el) { el._editorDisposed = true; el._editorCleanup?.(); el._editor?.destroy(); },
|
||||
};
|
||||
function applyEditorOptions(el, options = {}) {
|
||||
if (options.mode) el._editor.session.setMode("ace/mode/" + options.mode);
|
||||
const mode = editorModes.has(options.mode) ? options.mode : "text";
|
||||
el._editor.session.setMode("ace/mode/" + mode);
|
||||
if (options.theme) el._editor.setTheme("ace/theme/" + options.theme);
|
||||
el._editor.setReadOnly(options.readOnly !== false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Drafts stay in this browser tab and are tied to the signed-in account.
|
||||
export function formDraft(state, fields, key) {
|
||||
let baseline;
|
||||
let watching = false;
|
||||
const storageKey = "form-draft:" + key;
|
||||
const values = () => Object.fromEntries(fields.map(field => [field, state[field]]));
|
||||
const serialize = () => JSON.stringify(values());
|
||||
const apply = (saved) => {
|
||||
for (const field of fields) {
|
||||
if (Object.prototype.hasOwnProperty.call(saved, field)) state[field] = saved[field];
|
||||
}
|
||||
if (state.options?.expirationDate) state.options.expirationDate = new Date(state.options.expirationDate);
|
||||
};
|
||||
const remove = () => { try { window.sessionStorage.removeItem(storageKey); } catch { /* Storage may be disabled. */ } };
|
||||
const save = (flush = false) => {
|
||||
if (baseline === undefined) return;
|
||||
if (flush) document.querySelectorAll("input, select, textarea").forEach(el => {
|
||||
if (el._field?.binding.state === state && el._field.timer) el._field.commit();
|
||||
});
|
||||
state.unsavedChanges = serialize() !== baseline;
|
||||
state.draftSaved = false;
|
||||
if (!state.unsavedChanges) { remove(); return; }
|
||||
try {
|
||||
window.sessionStorage.setItem(storageKey, JSON.stringify({
|
||||
account: state.user?.username || null, savedAt: Date.now(), values: values(),
|
||||
}));
|
||||
state.draftSaved = true;
|
||||
} catch { /* Keep the form usable when storage is disabled or full. */ }
|
||||
};
|
||||
const saveBeforeLeaving = () => save(true);
|
||||
state.on("routeLeave", saveBeforeLeaving);
|
||||
window.addEventListener("beforeunload", saveBeforeLeaving);
|
||||
state.on("dispose", () => window.removeEventListener("beforeunload", saveBeforeLeaving));
|
||||
return {
|
||||
restore() {
|
||||
baseline = serialize();
|
||||
let saved;
|
||||
try { saved = JSON.parse(window.sessionStorage.getItem(storageKey) || "null"); } catch { /* Ignore invalid drafts. */ }
|
||||
const restored = saved && saved.account === (state.user?.username || null)
|
||||
&& Date.now() - saved.savedAt < 30 * 60000 && saved.values;
|
||||
if (restored) {
|
||||
apply(saved.values);
|
||||
state.draftRestored = true;
|
||||
}
|
||||
if (!watching) {
|
||||
watching = true;
|
||||
state.watch(() => values(), () => save(), true);
|
||||
}
|
||||
state.unsavedChanges = serialize() !== baseline;
|
||||
return !!restored;
|
||||
},
|
||||
checkpoint() { return JSON.parse(serialize()); },
|
||||
accept(saved) {
|
||||
baseline = JSON.stringify(saved);
|
||||
state.draftRestored = false;
|
||||
save();
|
||||
},
|
||||
discard() {
|
||||
document.querySelectorAll("input, select, textarea").forEach(el => {
|
||||
if (el._field?.binding.state !== state || !el._field.timer) return;
|
||||
clearTimeout(el._field.timer);
|
||||
el._field.timer = null;
|
||||
el.value = el._field.binding.value ?? "";
|
||||
});
|
||||
if (baseline !== undefined) apply(JSON.parse(baseline));
|
||||
state.draftRestored = false;
|
||||
save();
|
||||
},
|
||||
clear() {
|
||||
baseline = serialize();
|
||||
state.unsavedChanges = false;
|
||||
state.draftRestored = false;
|
||||
state.draftSaved = false;
|
||||
remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import { createApp, h, watch, provide, inject, onBeforeUnmount } from "vue";
|
||||
import { createRouter, createWebHistory, RouterView } from "vue-router";
|
||||
import { pageRoutes } from "./routes.js";
|
||||
import { templates } from "./templates.js";
|
||||
import { render as iconsTemplate } from "../partials/icons.htm";
|
||||
import { initializeTemplate } from "./template-state.js";
|
||||
import { createPageState, createTimers } from "./state.js";
|
||||
import { createHttp, promises } from "./http.js";
|
||||
import { mainController } from "./app.js";
|
||||
import { mainController, highlightRedactions } from "./app.js";
|
||||
import { createQuotaService } from "./quota.js";
|
||||
import * as formatters from "./formatters.js";
|
||||
import translations from "../i18n/locale-en.json";
|
||||
@@ -18,7 +19,9 @@ import { fieldDirective, formDirective, submitForm } from "./forms.js";
|
||||
const sessionKey = Symbol("session");
|
||||
export const serverPaths = /^\/(w|api|github)(\/|$)/;
|
||||
function translate(key, params = {}) {
|
||||
const message = key?.split(".").reduce((value, part) => value?.[part], translations);
|
||||
let message = key?.split(".").reduce((value, part) => value?.[part], translations);
|
||||
// Unknown error codes would otherwise surface as the raw "ERRORS.code" key.
|
||||
if (message == null && key?.startsWith("ERRORS.")) message = translations.ERRORS.unknown_error;
|
||||
return String(message ?? key ?? "").replace(/{{\s*([^}]+?)\s*}}/g, (_, name) => params[name] ?? "");
|
||||
}
|
||||
const fmt = { ...formatters, translate };
|
||||
@@ -83,6 +86,8 @@ export function mountApplication(target = "#app", options = {}) {
|
||||
root.window = window;
|
||||
root.Math = Math;
|
||||
root.sanitize = value => DOMPurify.sanitize(value ?? "");
|
||||
// Reading site_options re-renders Markdown once the configured mask loads.
|
||||
root.highlightRedactions = html => (root.site_options, highlightRedactions(html));
|
||||
root.submitForm = submitForm;
|
||||
root.safeUrl = safeUrl;
|
||||
const context = services();
|
||||
@@ -104,6 +109,7 @@ export function mountApplication(target = "#app", options = {}) {
|
||||
document.addEventListener("click", navigate);
|
||||
onBeforeUnmount(() => document.removeEventListener("click", navigate));
|
||||
return () => [
|
||||
iconsTemplate(root, []),
|
||||
isolatedConsent ? null : h("header", { class: "app-header" }, templates["partials/header.htm"](root, headerCache)),
|
||||
h("main", { class: "app-view align-items-stretch w-100" }, h(RouterView, null, {
|
||||
default: ({ Component, route }) => Component ? h(Component, { key: route.meta.preserveExplorer ? route.matched[0]?.path : route.path }) : null,
|
||||
|
||||
@@ -33,6 +33,21 @@ export default {
|
||||
// Bumped on every load so pages still resolving from a previous
|
||||
// document are dropped instead of appended to the new one.
|
||||
let generation = 0;
|
||||
let loadingTask = null;
|
||||
const renders = new Map();
|
||||
let rendering = 0;
|
||||
function releaseSlot(slot) {
|
||||
const record = renders.get(slot);
|
||||
if (record) {
|
||||
record.cancelled = true;
|
||||
record.task?.cancel?.();
|
||||
renders.delete(slot);
|
||||
}
|
||||
const canvas = slot.querySelector("canvas");
|
||||
if (canvas) { canvas.width = 0; canvas.height = 0; }
|
||||
slot.innerHTML = "";
|
||||
delete slot.dataset.state;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Toolbar
|
||||
@@ -183,6 +198,8 @@ export default {
|
||||
// ---------------------------------------------------------------
|
||||
function teardown() {
|
||||
generation++;
|
||||
slots.forEach(releaseSlot);
|
||||
if (loadingTask) { Promise.resolve(loadingTask.destroy?.()).catch(() => {}); loadingTask = null; }
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = null;
|
||||
@@ -214,7 +231,7 @@ export default {
|
||||
|
||||
// Match the canvas backing store to the device pixel ratio, else text
|
||||
// is visibly soft on HiDPI screens.
|
||||
function renderPage(page, slot) {
|
||||
function renderPage(page, slot, record) {
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
const unscaled = page.getViewport({ scale: 1 });
|
||||
const cssWidth = parseFloat(slot.style.width);
|
||||
@@ -231,11 +248,12 @@ export default {
|
||||
slot.appendChild(canvas);
|
||||
// The placeholder height was an estimate; the canvas is now the
|
||||
// authority on how tall this page is.
|
||||
slot.style.height = "";
|
||||
return page.render({
|
||||
slot.style.height = canvas.style.height;
|
||||
record.task = page.render({
|
||||
canvasContext: canvas.getContext("2d"),
|
||||
viewport: viewport,
|
||||
}).promise;
|
||||
});
|
||||
return record.task.promise;
|
||||
}
|
||||
|
||||
function renderVisible() {
|
||||
@@ -249,36 +267,35 @@ export default {
|
||||
const view = pages.getBoundingClientRect();
|
||||
const top = view.top - PRERENDER_MARGIN;
|
||||
const bottom = view.top + pages.clientHeight + PRERENDER_MARGIN;
|
||||
slots.forEach(function (slot, i) {
|
||||
if (slot.dataset.state) return;
|
||||
const rect = slot.getBoundingClientRect();
|
||||
if (rect.top > bottom || rect.bottom < top) return;
|
||||
const visible = slots.map((slot, i) => ({ slot, i, rect: slot.getBoundingClientRect() }))
|
||||
.filter(item => item.rect.top <= bottom && item.rect.bottom >= top)
|
||||
.sort((a, b) => Math.abs(a.rect.top - view.top) - Math.abs(b.rect.top - view.top)).slice(0, 6);
|
||||
const keep = new Set(visible.map(item => item.slot));
|
||||
for (const slot of renders.keys()) if (!keep.has(slot)) releaseSlot(slot);
|
||||
for (const { slot, i } of visible) {
|
||||
if (slot.dataset.state || rendering >= 2) continue;
|
||||
const record = { cancelled: false, task: null };
|
||||
renders.set(slot, record);
|
||||
rendering++;
|
||||
slot.dataset.state = "rendering";
|
||||
doc
|
||||
.getPage(i + 1)
|
||||
.then(function (page) {
|
||||
if (myGeneration !== generation) return;
|
||||
return renderPage(page, slot);
|
||||
})
|
||||
.then(function () {
|
||||
if (myGeneration !== generation) return;
|
||||
slot.dataset.state = "done";
|
||||
// A real page height replaces an estimate, moving everything
|
||||
// below it — re-pin the page the reader asked for.
|
||||
if (anchorPage && Date.now() < anchorUntil) {
|
||||
scrollToPage(anchorPage);
|
||||
}
|
||||
// The shift can also bring further pages into view. Called
|
||||
// directly rather than through schedule() so this doesn't
|
||||
// depend on a frame callback firing.
|
||||
renderVisible();
|
||||
})
|
||||
.catch(function () {
|
||||
// A single unrenderable page shouldn't blank the whole
|
||||
// document — leave the placeholder empty and move on.
|
||||
slot.dataset.state = "failed";
|
||||
});
|
||||
});
|
||||
let page;
|
||||
doc.getPage(i + 1).then(value => {
|
||||
page = value;
|
||||
if (record.cancelled || myGeneration !== generation) return;
|
||||
return renderPage(page, slot, record);
|
||||
}).then(() => {
|
||||
if (record.cancelled || myGeneration !== generation) return;
|
||||
slot.dataset.state = "done";
|
||||
if (anchorPage && Date.now() < anchorUntil) scrollToPage(anchorPage);
|
||||
}).catch(() => {
|
||||
if (!record.cancelled && myGeneration === generation) slot.dataset.state = "failed";
|
||||
}).finally(() => {
|
||||
rendering--;
|
||||
page?.cleanup?.();
|
||||
if (doc) renderVisible();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function schedule() {
|
||||
@@ -309,6 +326,8 @@ export default {
|
||||
// whatever is on screen. Used for zoom changes and window resizes,
|
||||
// both of which invalidate canvases rasterised at the old width.
|
||||
function relayout() {
|
||||
generation++;
|
||||
slots.forEach(releaseSlot);
|
||||
const width = pageWidth();
|
||||
slots.forEach(function (slot) {
|
||||
delete slot.dataset.state;
|
||||
@@ -320,7 +339,7 @@ export default {
|
||||
}
|
||||
|
||||
function setZoom(value) {
|
||||
if (!doc || value === zoom) return;
|
||||
if (!doc || !slots.length || value === zoom) return;
|
||||
const anchor = currentPage;
|
||||
zoom = value;
|
||||
syncToolbar();
|
||||
@@ -333,7 +352,7 @@ export default {
|
||||
if (resizeTimer) clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function () {
|
||||
resizeTimer = null;
|
||||
if (doc) relayout();
|
||||
if (doc && slots.length) relayout();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
@@ -342,8 +361,7 @@ export default {
|
||||
if (!url) return;
|
||||
const myGeneration = generation;
|
||||
|
||||
pdfjsLib
|
||||
.getDocument({
|
||||
loadingTask = pdfjsLib.getDocument({
|
||||
url: url,
|
||||
isEvalSupported: false,
|
||||
// Needed for PDFs that reference the 14 standard fonts without
|
||||
@@ -352,8 +370,8 @@ export default {
|
||||
standardFontDataUrl: "/script/external/pdf-standard-fonts/",
|
||||
cMapUrl: "/script/external/pdf-cmaps/",
|
||||
cMapPacked: true,
|
||||
})
|
||||
.promise.then(function (_doc) {
|
||||
});
|
||||
loadingTask.promise.then(function (_doc) {
|
||||
if (myGeneration !== generation) {
|
||||
_doc.destroy();
|
||||
return;
|
||||
|
||||
@@ -1,2 +1,22 @@
|
||||
((t,e)=>{"object"==typeof exports&&"object"==typeof module?module.exports=t.pdfjsLib=e():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf",[],()=>t.pdfjsLib=e()):"object"==typeof exports?exports["pdfjs-dist/build/pdf"]=t.pdfjsLib=e():t["pdfjs-dist/build/pdf"]=t.pdfjsLib=e()})(globalThis,()=>(()=>{var __webpack_modules__=[,(t,e,r)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.VerbosityLevel=e.Util=e.UnknownErrorException=e.UnexpectedResponseException=e.TextRenderingMode=e.RenderingIntentFlag=e.PromiseCapability=e.PermissionFlag=e.PasswordResponses=e.PasswordException=e.PageActionEventType=e.OPS=e.MissingPDFException=e.MAX_IMAGE_SIZE_TO_CACHE=e.LINE_FACTOR=e.LINE_DESCENT_FACTOR=e.InvalidPDFException=e.ImageKind=e.IDENTITY_MATRIX=e.FormatError=e.FeatureTest=e.FONT_IDENTITY_MATRIX=e.DocumentActionEventType=e.CMapCompressionType=e.BaseException=e.BASELINE_FACTOR=e.AnnotationType=e.AnnotationReplyType=e.AnnotationPrefix=e.AnnotationMode=e.AnnotationFlag=e.AnnotationFieldFlag=e.AnnotationEditorType=e.AnnotationEditorPrefix=e.AnnotationEditorParamsType=e.AnnotationBorderStyleType=e.AnnotationActionEventType=e.AbortException=void 0,e.assert=function(t,e){t||a(e)},e.bytesToString=h,e.createValidAbsoluteUrl=function(e){let t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:null,r=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;if(e)try{if(r&&"string"==typeof e){if(r.addDefaultProtocol&&e.startsWith("www.")){let t=e.match(/\./g);2<=t?.length&&(e="http://"+e)}if(r.tryConvertEncoding)try{e=p(e)}catch{}}var i=t?new URL(e,t):new URL(e);if((t=>{switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return 1;default:return}})(i))return i}catch{}return null},e.getModificationDate=function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:new Date;return[t.getUTCFullYear().toString(),(t.getUTCMonth()+1).toString().padStart(2,"0"),t.getUTCDate().toString().padStart(2,"0"),t.getUTCHours().toString().padStart(2,"0"),t.getUTCMinutes().toString().padStart(2,"0"),t.getUTCSeconds().toString().padStart(2,"0")].join("")},e.getUuid=function(){if("undefined"!=typeof crypto&&"function"==typeof crypto?.randomUUID)return crypto.randomUUID();var e=new Uint8Array(32);if("undefined"!=typeof crypto&&"function"==typeof crypto?.getRandomValues)crypto.getRandomValues(e);else for(let t=0;t<32;t++)e[t]=Math.floor(255*Math.random());return h(e)},e.getVerbosityLevel=function(){return s},e.info=function(t){s>=i.INFOS&&console.log("Info: "+t)},e.isArrayBuffer=function(t){return"object"==typeof t&&void 0!==t?.byteLength},e.isArrayEqual=function(r,i){if(r.length!==i.length)return!1;for(let t=0,e=r.length;t<e;t++)if(r[t]!==i[t])return!1;return!0},e.isNodeJS=void 0,e.normalizeUnicode=function(t){return f||(f=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu,g=new Map([["ſt","ſt"]])),t.replaceAll(f,(t,e,r)=>e?e.normalize("NFKC"):g.get(r))},e.objectFromMap=function(t){var e,r,i=Object.create(null);for([e,r]of t)i[e]=r;return i},e.objectSize=function(t){return Object.keys(t).length},e.setVerbosityLevel=function(t){Number.isInteger(t)&&(s=t)},e.shadow=o,e.string32=function(t){return String.fromCharCode(t>>24&255,t>>16&255,t>>8&255,255&t)},e.stringToBytes=d,e.stringToPDFString=function(r){if("ï"<=r[0]){let t;if("þ"===r[0]&&"ÿ"===r[1]?t="utf-16be":"ÿ"===r[0]&&"þ"===r[1]?t="utf-16le":"ï"===r[0]&&"»"===r[1]&&"¿"===r[2]&&(t="utf-8"),t)try{var e=new TextDecoder(t,{fatal:!0}),i=d(r);return e.decode(i)}catch(r){n(`stringToPDFString: "${r}".`)}}var s=[];for(let t=0,e=r.length;t<e;t++){var a=u[r.charCodeAt(t)];s.push(a?String.fromCharCode(a):r.charAt(t))}return s.join("")},e.stringToUTF8String=p,e.unreachable=a,e.utf8StringToString=function(t){return unescape(encodeURIComponent(t))},e.warn=n,r(2),r(84),r(86),r(87),r(89),r(93),r(101),r(102),r(105),r(107),r(109),r(113),r(116),r(123);r=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type);e.isNodeJS=r,e.IDENTITY_MATRIX=[1,0,0,1,0,0],e.FONT_IDENTITY_MATRIX=[.001,0,0,.001,0,0],e.MAX_IMAGE_SIZE_TO_CACHE=1e7,e.LINE_FACTOR=1.35,e.LINE_DESCENT_FACTOR=.35,e.BASELINE_FACTOR=.25925925925925924,e.RenderingIntentFlag={ANY:1,DISPLAY:2,PRINT:4,SAVE:8,ANNOTATIONS_FORMS:16,ANNOTATIONS_STORAGE:32,ANNOTATIONS_DISABLE:64,OPLIST:256},e.AnnotationMode={DISABLE:0,ENABLE:1,ENABLE_FORMS:2,ENABLE_STORAGE:3},e.AnnotationEditorPrefix="pdfjs_internal_editor_",e.AnnotationEditorType={DISABLE:-1,NONE:0,FREETEXT:3,STAMP:13,INK:15},e.AnnotationEditorParamsType={RESIZE:1,CREATE:2,FREETEXT_SIZE:11,FREETEXT_COLOR:12,FREETEXT_OPACITY:13,INK_COLOR:21,INK_THICKNESS:22,INK_OPACITY:23},e.PermissionFlag=Line truncated
|
||||
`}setFont(t){var e=this.current,r=this.commonObjs.get(t[0]);let i=t[1],s=(e.font=r,!this.embedFonts||r.missingFile||this.embeddedFonts[r.loadedName]||(this.addFontStyle(r),this.embeddedFonts[r.loadedName]=r),e.fontMatrix=r.fontMatrix||v.FONT_IDENTITY_MATRIX,"normal");r.black?s="900":r.bold&&(s="bold");t=r.italic?"italic":"normal";i<0?(i=-i,e.fontDirection=-1):e.fontDirection=1,e.fontSize=i,e.fontFamily=r.loadedName,e.fontWeight=s,e.fontStyle=t,e.tspan=this.svgFactory.createElement("svg:tspan"),e.tspan.setAttributeNS(null,"y",b(-e.y)),e.xcoords=[],e.ycoords=[]}endText(){var t=this.current;t.textRenderingMode&v.TextRenderingMode.ADD_TO_PATH_FLAG&&t.txtElement?.hasChildNodes()&&(t.element=t.txtElement,this.clip("nonzero"),this.endPath())}setLineWidth(t){0<t&&(this.current.lineWidth=t)}setLineCap(t){this.current.lineCap=s[t]}setLineJoin(t){this.current.lineJoin=a[t]}setMiterLimit(t){this.current.miterLimit=t}setStrokeAlpha(t){this.current.strokeAlpha=t}setStrokeRGBColor(t,e,r){this.current.strokeColor=v.Util.makeHexColor(t,e,r)}setFillAlpha(t){this.current.fillAlpha=t}setFillRGBColor(t,e,r){this.current.fillColor=v.Util.makeHexColor(t,e,r),this.current.tspan=this.svgFactory.createElement("svg:tspan"),this.current.xcoords=[],this.current.ycoords=[]}setStrokeColorN(t){this.current.strokeColor=this._makeColorN_Pattern(t)}setFillColorN(t){this.current.fillColor=this._makeColorN_Pattern(t)}shadingFill(t){var{width:e,height:r}=this.viewport,i=v.Util.inverseTransform(this.transformMatrix),[e,r,i,s]=v.Util.getAxialAlignedBoundingBox([0,0,e,r],i),a=this.svgFactory.createElement("svg:rect");a.setAttributeNS(null,"x",e),a.setAttributeNS(null,"y",r),a.setAttributeNS(null,"width",i-e),a.setAttributeNS(null,"height",s-r),a.setAttributeNS(null,"fill",this._makeShadingPattern(t)),this.current.fillAlpha<1&&a.setAttributeNS(null,"fill-opacity",this.current.fillAlpha),this._ensureTransformGroup().append(a)}_makeColorN_Pattern(t){return"TilingPattern"===t[0]?this._makeTilingPattern(t):this._makeShadingPattern(t)}_makeTilingPattern(t){var e=t[1],r=t[2],i=t[3]||v.IDENTITY_MATRIX,[s,a,n,o]=t[4],l=t[5],h=t[6],t=t[7],d="shading"+f++,[s,a,n,o]=v.Util.normalizeRect([...v.Util.applyTransform([s,a],i),...v.Util.applyTransform([n,o],i)]),[c,u]=v.Util.singularValueDecompose2dScale(i),l=l*c,c=h*u,h=this.svgFactory.createElement("svg:pattern"),u=(h.setAttributeNS(null,"id",d),h.setAttributeNS(null,"patternUnits","userSpaceOnUse"),h.setAttributeNS(null,"width",l),h.setAttributeNS(null,"height",c),h.setAttributeNS(null,"x",""+s),h.setAttributeNS(null,"y",""+a),this.svg),l=this.transformMatrix,c=this.current.fillColor,p=this.current.strokeColor,n=this.svgFactory.create(n-s,o-a);if(this.svg=n,this.transformMatrix=i,2===t){let t=v.Util.makeHexColor(...e);this.current.fillColor=t,this.current.strokeColor=t}return this.executeOpTree(this.convertOpList(r)),this.svg=u,this.transformMatrix=l,this.current.fillColor=c,this.current.strokeColor=p,h.append(n.childNodes[0]),this.defs.append(h),`url(#${d})`}_makeShadingPattern(i){switch((i="string"==typeof i?this.objs.get(i):i)[0]){case"RadialAxial":var s="shading"+f++,e=i[3];let r;switch(i[1]){case"axial":let t=i[4],e=i[5];(r=this.svgFactory.createElement("svg:linearGradient")).setAttributeNS(null,"id",s),r.setAttributeNS(null,"gradientUnits","userSpaceOnUse"),r.setAttributeNS(null,"x1",t[0]),r.setAttributeNS(null,"y1",t[1]),r.setAttributeNS(null,"x2",e[0]),r.setAttributeNS(null,"y2",e[1]);break;case"radial":var a=i[4],n=i[5],o=i[6],l=i[7];(r=this.svgFactory.createElement("svg:radialGradient")).setAttributeNS(null,"id",s),r.setAttributeNS(null,"gradientUnits","userSpaceOnUse"),r.setAttributeNS(null,"cx",n[0]),r.setAttributeNS(null,"cy",n[1]),r.setAttributeNS(null,"r",l),r.setAttributeNS(null,"fx",a[0]),r.setAttributeNS(null,"fy",a[1]),r.setAttributeNS(null,"fr",o);break;default:throw new Error("Unknown RadialAxial type: "+i[1])}for(let t of e){var h=this.svgFactory.createElement("svg:stop");h.setAttributeNS(null,"offset",t[0]),h.setAttributeNS(null,"stop-color",t[1]),r.append(h)}return this.defs.append(r),`url(#${s})`;case"Mesh":return(0,v.warn)("Unimplemented pattern Mesh"),null;case"Dummy":return"hotpink";default:throw new Error("Unknown IR type: "+i[0])}}setDash(t,e){this.current.dashArray=t,this.current.dashPhase=e}constructPath(e,r){let t=this.current,i=t.x,s=t.y,a=[],n=0;for(let t of e)switch(0|t){case v.OPS.rectangle:i=r[n++],s=r[n++];let t=i+r[n++],e=s+r[n++];a.push("M",b(i),b(s),"L",b(t),b(s),"L",b(t),b(e),"L",b(i),b(e),"Z");break;case v.OPS.moveTo:i=r[n++],s=r[n++],a.push("M",b(i),b(s));break;case v.OPS.lineTo:i=r[n++],s=r[n++],a.push("L",b(i),b(s));break;case v.OPS.curveTo:i=r[n+4],s=r[n+5],a.push("C",b(r[n]),b(r[n+1]),b(r[n+2]),b(r[n+3]),b(i),b(s)),n+=6;break;case v.OPS.curveTo2:a.push("C",b(i),b(s),b(r[n]),b(r[n+1]),b(r[n+2]),b(r[n+3])),i=r[n+2],s=r[n+3],n+=4;break;case v.OPS.curveTo3:i=r[n+2],s=r[n+3],a.push("C",b(r[n]),b(r[n+1]),b(i),b(s),b(i),b(s)),n+=4;break;case v.OPS.closePath:a.push("Z")}
Line truncated
|
||||
/**
|
||||
* @licstart The following is the entire license notice for the
|
||||
* JavaScript code in this page
|
||||
*
|
||||
* Copyright 2023 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @licend The above is the entire license notice for the
|
||||
* JavaScript code in this page
|
||||
*/
|
||||
!function webpackUniversalModuleDefinition(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=t.pdfjsLib=e():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf",[],(()=>t.pdfjsLib=e())):"object"==typeof exports?exports["pdfjs-dist/build/pdf"]=t.pdfjsLib=e():t["pdfjs-dist/build/pdf"]=t.pdfjsLib=e()}(globalThis,(()=>(()=>{"use strict";var __webpack_modules__=[,(t,e,n)=>{Object.defineProperty(e,"__esModule",{value:!0});e.VerbosityLevel=e.Util=e.UnknownErrorException=e.UnexpectedResponseException=e.TextRenderingMode=e.RenderingIntentFlag=e.PromiseCapability=e.PermissionFlag=e.PasswordResponses=e.PasswordException=e.PageActionEventType=e.OPS=e.MissingPDFException=e.MAX_IMAGE_SIZE_TO_CACHE=e.LINE_FACTOR=e.LINE_DESCENT_FACTOR=e.InvalidPDFException=e.ImageKind=e.IDENTITY_MATRIX=e.FormatError=e.FeatureTest=e.FONT_IDENTITY_MATRIX=e.DocumentActionEventType=e.CMapCompressionType=e.BaseException=e.BASELINE_FACTOR=e.AnnotationType=e.AnnotationReplyType=e.AnnotationPrefix=e.AnnotationMode=e.AnnotationFlag=e.AnnotationFieldFlag=e.AnnotationEditorType=e.AnnotationEditorPrefix=e.AnnotationEditorParamsType=e.AnnotationBorderStyleType=e.AnnotationActionEventType=e.AbortException=void 0;e.assert=function assert(t,e){t||unreachable(e)};e.bytesToString=bytesToString;e.createValidAbsoluteUrl=function createValidAbsoluteUrl(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(!t)return null;try{if(n&&"string"==typeof t){if(n.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(n.tryConvertEncoding)try{t=stringToUTF8String(t)}catch{}}const i=e?new URL(t,e):new URL(t);if(function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(i))return i}catch{}return null};e.getModificationDate=function getModificationDate(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:new Date;return[t.getUTCFullYear().toString(),(t.getUTCMonth()+1).toString().padStart(2,"0"),t.getUTCDate().toString().padStart(2,"0"),t.getUTCHours().toString().padStart(2,"0"),t.getUTCMinutes().toString().padStart(2,"0"),t.getUTCSeconds().toString().padStart(2,"0")].join("")};e.getUuid=function getUuid(){if("undefined"!=typeof crypto&&"function"==typeof crypto?.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);if("undefined"!=typeof crypto&&"function"==typeof crypto?.getRandomValues)crypto.getRandomValues(t);else for(let e=0;e<32;e++)t[e]=Math.floor(255*Math.random());return bytesToString(t)};e.getVerbosityLevel=function getVerbosityLevel(){return s};e.info=function info(t){s>=r.INFOS&&console.log(`Info: ${t}`)};e.isArrayBuffer=function isArrayBuffer(t){return"object"==typeof t&&void 0!==t?.byteLength};e.isArrayEqual=function isArrayEqual(t,e){if(t.length!==e.length)return!1;for(let n=0,i=t.length;n<i;n++)if(t[n]!==e[n])return!1;return!0};e.isNodeJS=void 0;e.normalizeUnicode=function normalizeUnicode(t){if(!c){c=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;h=new Map([["ſt","ſt"]])}return t.replaceAll(c,((t,e,n)=>e?e.normalize("NFKC"):h.get(n)))};e.objectFromMap=function objectFromMap(t){const e=Object.create(null);for(const[n,i]of t)e[n]=i;return e};e.objectSize=function objectSize(t){return Object.keys(t).length};e.setVerbosityLevel=function setVerbosityLevel(t){Number.isInteger(t)&&(s=t)};e.shadow=shadow;e.string32=function string32(t){return String.fromCharCode(t>>24&255,t>>16&255,t>>8&255,255&t)};e.stringToBytes=stringToBytes;e.stringToPDFString=function stringToPDFString(t){if(t[0]>="ï"){let e;"þ"===t[0]&&"ÿ"===t[1]?e="utf-16be":"ÿ"===t[0]&&"þ"===t[1]?e="utf-16le":"ï"===t[0]&&"»"===t[1]&&"¿"===t[2]&&(e="utf-8");if(e)try{const n=new TextDecoder(e,{fatal:!0}),i=stringToBytes(t);return n.decode(i)}catch(t){warn(`stringToPDFString: "${t}".`)}}const e=[];for(let n=0,i=t.length;n<i;n++){const i=l[t.charCodeAt(n)];e.push(i?String.fromCharCode(i):t.charAt(n))}return e.join("")};e.stringToUTF8String=stringToUTF8String;e.unreachable=unreachable;e.utf8StringToString=function utf8StringToString(t){return unescape(encodeURIComponent(t))};e.warn=warn;n(2);n(84);n(86);n(87);n(89);n(93);n(101);n(102);n(105);n(107);n(109);n(113);n(116);n(123);const i=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type);e.isNodeJS=i;e.IDENTITY_MATRIX=[1,0,0,1,0,0];e.FONT_IDENTITY_MATRIX=[.001,0,0,.001,0,0];e.MAX_IMAGE_SIZE_TO_CACHE=1e7;e.LINE_FACTOR=1.35;e.LINE_DESCENT_FACTOR=.35;e.BASELINE_FACTOR=.25925925925925924;e.RenderingIntentFlag={ANY:1,DISPLAY:2,PRLine truncated
|
||||
@@ -19,14 +19,14 @@ export function createPageState(parent = null, events = new Map()) {
|
||||
});
|
||||
Object.defineProperty(own, "parent", { value: parent, configurable: true });
|
||||
Object.defineProperty(own, "viewState", { get: () => state });
|
||||
state.watch = (source, callback, deep = false) => {
|
||||
state.watch = (source, callback, deep = false, immediate = true) => {
|
||||
const read = typeof source === "function" ? () => source(state) : () => getPath(state, source);
|
||||
const stop = watch(read, callback, { deep, flush: "post" });
|
||||
cleanups.push(stop);
|
||||
nextTick(() => { if (!disposed) callback(read(), read()); });
|
||||
if (immediate) nextTick(() => { if (!disposed) callback(read(), read()); });
|
||||
return stop;
|
||||
};
|
||||
state.watchGroup = (sources, callback) => state.watch(() => sources.map(s => getPath(state, s)), callback, true);
|
||||
state.watchGroup = (sources, callback, immediate = true) => state.watch(() => sources.map(s => getPath(state, s)), callback, true, immediate);
|
||||
state.on = (name, callback) => {
|
||||
if (name === "dispose") { cleanups.push(callback); return () => {}; }
|
||||
if (!events.has(name)) events.set(name, new Set());
|
||||
@@ -69,10 +69,15 @@ export function createTimers() {
|
||||
}
|
||||
|
||||
export function createListeners() {
|
||||
const cleanups = [];
|
||||
onScopeDispose(() => cleanups.forEach(fn => fn()));
|
||||
const cleanups = new Set();
|
||||
onScopeDispose(() => { cleanups.forEach(fn => fn()); cleanups.clear(); });
|
||||
return (target, type, callback, options) => {
|
||||
target.addEventListener(type, callback, options);
|
||||
cleanups.push(() => target.removeEventListener(type, callback, options));
|
||||
const off = () => {
|
||||
target.removeEventListener(type, callback, options);
|
||||
cleanups.delete(off);
|
||||
};
|
||||
cleanups.add(off);
|
||||
return off;
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,12 @@ export function initializeTemplate(name, state) {
|
||||
if (name === "partials/explorer.htm") {
|
||||
state.sidebarCollapsed = window.matchMedia?.("(max-width: 767px)").matches || false;
|
||||
}
|
||||
if (name === "partials/anonymize.htm") state.prTabState = { active: state.options.diff ? "diff" : "comments" };
|
||||
if (name === "partials/anonymize.htm") {
|
||||
state.prTabState = { active: state.options.diff ? "diff" : "comments" };
|
||||
state.watchGroup(["options.diff", "options.comments"], () => {
|
||||
if (!state.options[state.prTabState.active]) state.prTabState.active = state.options.diff ? "diff" : state.options.comments ? "comments" : null;
|
||||
});
|
||||
}
|
||||
if (name === "partials/gist.htm") state.tabState = { active: state.details?.files ? "files" : "comments" };
|
||||
if (["partials/conferences.htm", "partials/conference.htm"].includes(name)) {
|
||||
state.statusLabels = { ready: "Ready", expired: "Expired", removed: "Removed" };
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
"use strict";(()=>{var zl=Object.defineProperty;var Hl=(e,t)=>{for(var o in t)zl(e,o,{get:t[o],enumerable:!0})};var ye={notebook:"/script/notebook.c3144c18ae.min.js",org:"/script/org.f61f262915.min.js",markdown:"/script/markdown.44f2e93820.min.js",editor:"/script/editor.4f142d3adc.min.js",pdf:"/script/pdf.0175d53d2f.min.js"};var rn=new Map,Gl=ye;function Wt(e){if(!rn.has(e)){let t=document.createElement("script");t.src=Gl[e];let o=new Promise((r,n)=>{t.onload=r,t.onerror=()=>{rn.delete(e),t.remove(),n(new Error(`Unable to load ${e}. Please retry.`))}});rn.set(e,o),document.head.appendChild(t)}return rn.get(e)}async function tr(){await Wt("editor"),ace.config.set("basePath","/script/external/ace/")}function ds(e){let t=Object.create(null);for(let o of e.split(","))t[o]=1;return o=>o in t}var Me={},Kt=[],rt=()=>{},Xn=()=>!1,us=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),cs=e=>e.startsWith("onUpdate:"),Ge=Object.assign,Ss=(e,t)=>{let o=e.indexOf(t);o>-1&&e.splice(o,1)},jl=Object.prototype.hasOwnProperty,Ae=(e,t)=>jl.call(e,t),Ee=Array.isArray,Pt=e=>Ds(e)==="[object Map]",ls=e=>Ds(e)==="[object Set]",sr=e=>Ds(e)==="[object Date]";var _e=e=>typeof e=="function",Fe=e=>typeof e=="string",ct=e=>typeof e=="symbol",Ve=e=>e!==null&&typeof e=="object",Zn=e=>(Ve(e)||_e(e))&&_e(e.then)&&_e(e.catch),rr=Object.prototype.toString,Ds=e=>rr.call(e),xn=e=>Ds(e).slice(8,-1),an=e=>Ds(e)==="[object Object]",ln=e=>Fe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ps=ds(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted");var dn=e=>{let t=Object.create(null);return(o=>t[o]||(t[o]=e(o)))},Wl=/-\w/g,Ke=dn(e=>e.replace(Wl,t=>t.slice(1).toUpperCase())),Kl=/\B([A-Z])/g,qt=dn(e=>e.replace(Kl,"-$1").toLowerCase()),Yt=dn(e=>e.charAt(0).toUpperCase()+e.slice(1)),Rs=dn(e=>e?`on${Yt(e)}`:""),ht=(e,t)=>!Object.is(e,t),Ts=(e,...t)=>{for(let o=0;o<e.length;o++)e[o](...t)},un=(e,t,o,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:o})},eo=e=>{let t=parseFloat(e);return isNaN(t)?e:t};var nr,Os=()=>nr||(nr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Pe(e){if(Ee(e)){let t={};for(let o=0;o<e.length;o++){let r=e[o],n=Fe(r)?Xl(r):Pe(r);if(n)for(let i in n)t[i]=n[i]}return t}else if(Fe(e)||Ve(e))return e}var Yl=/;(?![^(]*\))/g,Jl=/:([^]+)/,Ql=/\/\*[^]*?\*\//g;function Xl(e){let t={};return e.replace(Ql,"").split(Yl).forEach(o=>{if(o){let r=o.split(Jl);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function O(e){let t="";if(Fe(e))t=e;else if(Ee(e))for(let o=0;o<e.length;o++){let r=O(e[o]);r&&(t+=r+" ")}else if(Ve(e))for(let o in e)e[o]&&(t+=o+" ");return t.trim()}var ir="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",ar=ds(ir),Zl=ds(ir+",async,autofocus,autoplay,controls,default,defer,disabled,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");function cn(e){return!!e||e===""}function xl(e,t){if(e.length!==t.length)return!1;let o=!0;for(let r=0;o&&r<e.length;r++)o=fs(e[r],t[r]);return o}function or(e,t){if(e.size!==t.size)return!1;let o=Array.from(t),r=new Uint8Array(o.length);for(let n of e){let i=-1;for(let a=0;a<o.length;a++)if(!r[a]&&fs(n,o[a])){i=a;break}if(i<0)return!1;r[i]=1}return!0}function fs(e,t){if(e===t)return!0;let o=sr(e),r=sr(t);if(o||r)return o&&r?e.getTime()===t.getTime():!1;if(o=ct(e),r=ct(t),o||r)return e===t;if(o=Ee(e),r=Ee(t),o||r)return o&&r?xl(e,t):!1;if(o=Ve(e),r=Ve(t),o||r){if(!o||!r)return!1;if(o=Pt(e),r=Pt(t),o||r||(o=ls(e),r=ls(t),o||r))return o&&r?or(e,t):!1;let n=Object.keys(e).length,i=Object.keys(t).length;if(n!==i)return!1;for(let a in e){let u=e.hasOwnProperty(a),m=t.hasOwnProperty(a);if(u&&!m||!u&&m||!fs(e[a],t[a]))return!1}}return String(e)===String(t)}var lr=e=>!!(e&&e.__v_isRef===!0),c=e=>Fe(e)?e:e==null?"":Ee(e)||Ve(e)&&(e.toString===rr||!_e(e.toString))?lr(e)?c(e.value):JSON.stringify(e,dr,2):String(e),dr=(e,t)=>lr(t)?dr(e,t.value):Pt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((o,[r,n],i)=>(o[Qn(r,i)+" =>"]=n,o),{})}:ls(t)?{[`Set(${t.size})`]:[...t.values()].map(o=>Qn(o))}:ct(t)?Qn(t):Ve(t)&&!Ee(t)&&!an(t)?String(t):t,Qn=(e,t="")=>{var o;return ct(e)?`Symbol(${(o=e.description)!=null?o:t})`:e};function ed(e,...t){console.warn(`[Vue warn] ${e}`,...t)}var Ye,Ps=class{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&Ye&&(Ye.active?(this.parent=Ye,this.index=(Ye.scopes||(Ye.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,o;if(this.scopes){let r=this.scopes.slice();for(t=0,o=r.length;t<o;t++)r[t].pause()}for(t=0,o=this.effects.length;t<o;t++)this.effects[t].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!Line truncated
|
||||
"use strict";(()=>{var ld=Object.defineProperty;var dd=(e,t)=>{for(var s in t)ld(e,s,{get:t[s],enumerable:!0})};var he=["abap","abc","actionscript","ada","alda","apache_conf","apex","applescript","aql","asciidoc","asl","assembly_x86","autohotkey","batchfile","c9search","c_cpp","cirru","clojure","cobol","coffee","coldfusion","crystal","csharp","csound_document","csound_orchestra","csound_score","csp","css","curly","d","dart","diff","django","dockerfile","dot","drools","edifact","eiffel","ejs","elixir","elm","erlang","forth","fortran","fsharp","fsl","ftl","gcode","gherkin","gitignore","glsl","gobstones","golang","graphqlschema","groovy","haml","handlebars","haskell","haskell_cabal","haxe","hjson","html","html_elixir","html_ruby","ini","io","jack","jade","java","javascript","json","json5","jsoniq","jsp","jssm","jsx","julia","kotlin","latex","less","liquid","lisp","livescript","logiql","logtalk","lsl","lua","luapage","lucene","makefile","markdown","mask","matlab","maze","mediawiki","mel","mixal","mushcode","mysql","nginx","nim","nix","nsis","nunjucks","objectivec","ocaml","pascal","perl","perl6","pgsql","php","php_laravel_blade","pig","plain_text","powershell","praat","prisma","prolog","properties","protobuf","puppet","python","qml","r","razor","rdoc","red","redshift","rhtml","rst","ruby","rust","sass","scad","scala","scheme","scss","sh","sjs","slim","smarty","snippets","soy_template","space","sparql","sql","sqlserver","stylus","svg","swift","tcl","terraform","tex","text","textile","toml","tsx","turtle","twig","typescript","vala","vbscript","velocity","verilog","vhdl","visualforce","wollok","xml","xquery","yaml","zeek"];var ve={editor:"/script/editor.e243722d87.min.js",markdown:"/script/markdown.ad7b1d71c3.min.js",notebook:"/script/notebook.8844e2735f.min.js",org:"/script/org.f4e2a3f59f.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js"};var cs=new Map,pd=ve;function Xt(e){if(!cs.has(e)){let t=document.createElement("script");t.src=pd[e];let s=new Promise((r,n)=>{t.onload=r,t.onerror=()=>{cs.delete(e),t.remove(),n(new Error(`Unable to load ${e}. Please retry.`))}});cs.set(e,s),document.head.appendChild(t)}return cs.get(e)}async function pr(){await Xt("editor"),ace.config.set("basePath","/script/external/ace/")}function ho(e){let t=Object.create(null);for(let s of e.split(","))t[s]=1;return s=>s in t}var Ie={},Zt=[],at=()=>{},an=()=>!1,vo=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),go=e=>e.startsWith("onUpdate:"),Ge=Object.assign,Po=(e,t)=>{let s=e.indexOf(t);s>-1&&e.splice(s,1)},fd=Object.prototype.hasOwnProperty,Ne=(e,t)=>fd.call(e,t),ye=Array.isArray,Lt=e=>Vo(e)==="[object Map]",Ut=e=>Vo(e)==="[object Set]",fr=e=>Vo(e)==="[object Date]";var we=e=>typeof e=="function",Fe=e=>typeof e=="string",mt=e=>typeof e=="symbol",Te=e=>e!==null&&typeof e=="object",ln=e=>(Te(e)||we(e))&&we(e.then)&&we(e.catch),vr=Object.prototype.toString,Vo=e=>vr.call(e),dn=e=>Vo(e).slice(8,-1),ps=e=>Vo(e)==="[object Object]",fs=e=>Fe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,yo=ho(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted");var ms=e=>{let t=Object.create(null);return(s=>t[s]||(t[s]=e(s)))},md=/-\w/g,Je=ms(e=>e.replace(md,t=>t.slice(1).toUpperCase())),hd=/\B([A-Z])/g,St=ms(e=>e.replace(hd,"-$1").toLowerCase()),xt=ms(e=>e.charAt(0).toUpperCase()+e.slice(1)),qo=ms(e=>e?`on${xt(e)}`:""),gt=(e,t)=>!Object.is(e,t),bo=(e,...t)=>{for(let s=0;s<e.length;s++)e[s](...t)},hs=(e,t,s,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:s})},wo=e=>{let t=parseFloat(e);return isNaN(t)?e:t};var mr,Mo=()=>mr||(mr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function qe(e){if(ye(e)){let t={};for(let s=0;s<e.length;s++){let r=e[s],n=Fe(r)?bd(r):qe(r);if(n)for(let i in n)t[i]=n[i]}return t}else if(Fe(e)||Te(e))return e}var vd=/;(?![^(]*\))/g,gd=/:([^]+)/,yd=/\/\*[^]*?\*\//g;function bd(e){let t={};return e.replace(yd,"").split(vd).forEach(s=>{if(s){let r=s.split(gd);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function A(e){let t="";if(Fe(e))t=e;else if(ye(e))for(let s=0;s<e.length;s++){let r=A(e[s]);r&&(t+=r+" ")}else if(Te(e))for(let s in e)e[s]&&(t+=s+" ");return t.trim()}var gr="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",yr=ho(gr),wd=ho(gr+",async,autofocus,autoplay,controls,default,defer,disabled,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");function vs(e){return!!e||e===""}function kd(e,t){if(e.length!==t.length)return!1;let s=!0;for(let r=0;s&&r<e.length;r++)s=Nt(e[r],t[r]);return s}function hr(e,t){if(e.size!==t.size)return!1;let s=Array.from(t),r=new Uint8Array(s.length);for(let n of e){let i=-1;for(let a=0;a<s.length;a++)if(!r[a]&&Nt(n,s[a])){i=a;break}if(i<0)return!1;r[i]=1}return!0}function Nt(e,t){if(e===t)return!0;let s=fr(e),r=fr(t);if(s||r)returLine truncated
|
||||
`))),e[uo]=Ws(n);let i=r||n.props&&n.props.type==="number";lo(e,t?"change":"input",a=>{a.target.composing||e[uo](Bn(e.value,s,i))}),(s||i)&&lo(e,"change",()=>{e.value=Bn(e.value,s,i)}),t||(lo(e,"compositionstart",cc),lo(e,"compositionend",Ji),lo(e,"change",Ji))},mounted(e,{value:t,modifiers:{trim:s,number:r}}){let n=t??"",i=e[js];delete e[js],i!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==i?e[uo](Bn(e.value,s,r)):e.value=n},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:r,trim:n,number:i}},a){if(e[uo]=Ws(a),e.composing)return;let u=(i||e.type==="number")&&!/^0\d/.test(e.value)?wo(e.value):e.value,f=t??"";if(u===f)return;let v=e.getRootNode();(v instanceof Document||v instanceof ShadowRoot)&&v.activeElement===e&&e.type!=="range"&&(r&&t===s||n&&e.value.trim()===f)||(e.value=f)}};var ta={deep:!0,created(e,{value:t,modifiers:{number:s}},r){e._modelValue=t,lo(e,"change",()=>{let n=Array.prototype.filter.call(e.options,f=>f.selected).map(f=>s?wo(Ks(f)):Ks(f)),i=e.multiple,a=i?Ut(e._modelValue)?new Set(n):n:n[0],u=e._pendingValue=[i,i?ye(a)?n.slice():n:a];try{e[uo](a)}finally{st(()=>{e._pendingValue===u&&(e._pendingValue=void 0)})}}),e[uo]=Ws(r)},mounted(e,{value:t}){Qi(e,t)},beforeUpdate(e,{value:t},s){e._modelValue=t,e[uo]=Ws(s)},updated(e,{value:t}){let s=e._pendingValue;e._pendingValue=void 0,(!s||s[0]!==e.multiple||!pc(t,s[1],s[0]))&&Qi(e,t)}};function pc(e,t,s){if(!s)return Nt(e,t);if(ye(e))return Nt(e,t);if(Ut(e)){if(e.size!==t.length)return!1;for(let r of t)if(!e.has(r))return!1;return!0}return!1}function Qi(e,t){let s=e.multiple,r=ye(t);if(!(s&&!r&&!Ut(t))){for(let n=0,i=e.options.length;n<i;n++){let a=e.options[n],u=Ks(a);if(s)if(r){let f=typeof u;f==="string"||f==="number"?a.selected=t.some(v=>String(v)===String(u)):a.selected=br(t,u)>-1}else a.selected=t.has(u);else if(Nt(Ks(a),t)){e.selectedIndex!==n&&(e.selectedIndex=n);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Ks(e){return"_value"in e?e._value:e.value}var fc=["ctrl","shift","alt","meta"],mc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>fc.some(s=>e[`${s}Key`]&&!t.includes(s))},me=(e,t)=>{if(!e)return e;let s=e._withMods||(e._withMods={}),r=t.join(".");return s[r]||(s[r]=((n,...i)=>{for(let a=0;a<t.length;a++){let u=mc[t[a]];if(u&&u(n,t))return}return e(n,...i)}))},hc={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Ro=(e,t)=>{let s=e._withKeys||(e._withKeys={}),r=t.join(".");return s[r]||(s[r]=(n=>{if(!("key"in n))return;let i=St(n.key);if(t.some(a=>a===i||hc[a]===i))return e(n)}))},vc=Ge({patchProp:lc},Wu),Xi;function gc(){return Xi||(Xi=Oi(vc))}var oa=((...e)=>{let t=gc().createApp(...e),{mount:s}=t;return t.mount=r=>{let n=bc(r);if(!n)return;let i=t._component;!we(i)&&!i.render&&!i.template&&(i.template=n.innerHTML),n.nodeType===1&&(n.textContent="");let a=s(n,!1,yc(n));return n instanceof Element&&(n.removeAttribute("v-cloak"),n.setAttribute("data-v-app","")),a},t});function yc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function bc(e){return Fe(e)?document.querySelector(e):e}var fo=typeof document<"u";function na(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function wc(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&na(e.default)}var Ae=Object.assign;function Ys(e,t){let s={};for(let r in t){let n=t[r];s[r]=wt(n)?n.map(e):e(n)}return s}var Ao=()=>{},wt=Array.isArray;function Kn(e,t){let s={};for(let r in e)s[r]=r in t?t[r]:e[r];return s}var ra=/#/g,kc=/&/g,Ec=/\//g,Cc=/=/g,_c=/\?/g,ia=/\+/g,Sc=/%5B/g,Nc=/%5D/g,aa=/%5E/g,Dc=/%60/g,la=/%7B/g,Rc=/%7C/g,da=/%7D/g,Tc=/%20/g;function Yn(e){return e==null?"":encodeURI(""+e).replace(Rc,"|").replace(Sc,"[").replace(Nc,"]")}function ua(e){return Yn(e).replace(la,"{").replace(da,"}").replace(aa,"^")}function Gn(e){return Yn(e).replace(ia,"%2B").replace(Tc,"+").replace(ra,"%23").replace(kc,"%26").replace(Dc,"`").replace(la,"{").replace(da,"}").replace(aa,"^")}function Ac(e){return Gn(e).replace(Cc,"%3D")}function Oc(e){return Yn(e).replace(ra,"%23").replace(_c,"%3F")}function ca(e){return Oc(e).replace(Ec,"%2F")}function To(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}var Ic=/\/$/,Pc=e=>e.replace(Ic,"");function Js(e,t,s="/"){let r,n={},i="",a="",u=t.indexOf("#"),f=t.indexOf("?");return f=u>=0&&f>u?-1:f,f>=0&&(r=t.slice(0,f),i=t.slice(f,u>0?u:t.length),n=e(i.slice(1))),u>=0&&(r=r||t.slice(0,u),a=t.slice(u,t.length)),r=qc(r??t,s),{fullPath:r+i+a,path:r,query:n,hash:To(a)}}function pa(e,t){let s=t.query?e(t.query):"";return t.path+(s&&"?")+s+(t.hash||"")}function Jn(e,t){return!t||!e.toLowerCase().sLine truncated
|
||||
${JSON.stringify(e)}${t?`
|
||||
while being at
|
||||
`+JSON.stringify(t):""}`},[Be.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${vc(t)}" via a navigation guard.`},[Be.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[Be.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[Be.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function os(e,t){return qe(new Error,{type:e,[na]:!0},t)}function Vt(e,t){return e instanceof Error&&na in e&&(t==null||!!(e.type&t))}var hc=["params","query","hash"];function vc(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let o of hc)o in e&&(t[o]=e[o]);return JSON.stringify(t,null,2)}function oa(e){let t={};if(e===""||e==="?")return t;let o=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;r<o.length;++r){let n=o[r].replace(Gi," "),i=n.indexOf("="),a=_s(i<0?n:n.slice(0,i)),u=i<0?null:_s(n.slice(i+1));if(a in t){let m=t[a];gt(m)||(m=t[a]=[m]),m.push(u)}else t[a]=u}return t}function Go(e){let t="";for(let o in e){let r=e[o];if(o=ic(o),r==null){r!==void 0&&(t+=(t.length?"&":"")+o);continue}(gt(r)?r.map(n=>n&&Mo(n)):[r&&Mo(r)]).forEach(n=>{n!==void 0&&(t+=(t.length?"&":"")+o,n!=null&&(t+="="+n))})}return t}function ra(e){let t={};for(let o in e){let r=e[o];r!==void 0&&(t[o]=gt(r)?r.map(n=>n==null?null:""+n):r==null?r:""+r)}return t}var ia=Symbol(""),jo=Symbol(""),Gn=Symbol(""),jn=Symbol(""),Wn=Symbol("");function Cs(){let e=[];function t(r){return e.push(r),()=>{let n=e.indexOf(r);n>-1&&e.splice(n,1)}}function o(){e=[]}return{add:t,list:()=>e.slice(),reset:o}}function Bt(e,t,o,r,n,i=a=>a()){let a=r&&(r.enterCallbacks[n]=r.enterCallbacks[n]||[]);return()=>new Promise((u,m)=>{let h=p=>{p===!1?m(os(Be.NAVIGATION_ABORTED,{from:o,to:t})):p instanceof Error?m(p):mc(p)?m(os(Be.NAVIGATION_GUARD_REDIRECT,{from:t,to:p})):(a&&r.enterCallbacks[n]===a&&typeof p=="function"&&a.push(p),u())},v=i(()=>e.call(r&&r.instances[n],t,o,h)),k=Promise.resolve(v);e.length<3&&(k=k.then(h)),k.catch(p=>m(p))})}function Kn(e,t,o,r,n=i=>i()){let i=[];for(let a of e)for(let u in a.components){let m=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(Hi(m)){let h=(m.__vccOpts||m)[t];h&&i.push(Bt(h,o,r,a,u,n))}else{let h=m();i.push(()=>h.then(v=>{if(!v)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let k=Qu(v)?v.default:v;a.mods[u]=v,a.components[u]=k;let p=(k.__vccOpts||k)[t];return p&&Bt(p,o,r,a,u,n)()}))}}return i}function aa(e,t){let o=[],r=[],n=[],i=Math.max(t.matched.length,e.matched.length);for(let a=0;a<i;a++){let u=t.matched[a];u&&(e.matched.find(h=>ns(h,u))?r.push(u):o.push(u));let m=e.matched[a];m&&(t.matched.find(h=>ns(h,m))||n.push(m))}return[o,r,n]}var yc=()=>location.protocol+"//"+location.host;function ga(e,t){let{pathname:o,search:r,hash:n}=t,i=e.indexOf("#");if(i>-1){let a=n.includes(e.slice(i))?e.slice(i).length:1,u=n.slice(a);return u[0]!=="/"&&(u="/"+u),Fo(u,"")}return Fo(o,e)+r+n}function gc(e,t,o,r){let n=[],i=[],a=null,u=({state:p})=>{let g=ga(e,location),w=o.value,b=t.value,T=0;if(p){if(o.value=g,t.value=p,a&&a===w){a=null;return}T=b?p.position-b.position:0}else r(g);n.forEach(N=>{N(o.value,w,{delta:T,type:Hn.pop,direction:T?T>0?Bn.forward:Bn.back:Bn.unknown})})};function m(){a=o.value}function h(p){n.push(p);let g=()=>{let w=n.indexOf(p);w>-1&&n.splice(w,1)};return i.push(g),g}function v(){if(document.visibilityState==="hidden"){let{history:p}=window;if(!p.state)return;p.replaceState(qe({},p.state,{scroll:on()}),"")}}function k(){for(let p of i)p();i=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",v),document.removeEventListener("visibilitychange",v)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",v),document.addEventListener("visibilitychange",v),{pauseListeners:m,listen:h,destroy:k}}function la(e,t,o,r=!1,n=!1){return{back:e,current:t,forward:o,replaced:r,position:window.history.length,scroll:n?on():null}}function bc(e){let{history:t,location:o}=window,r={value:ga(e,o)},n={value:t.state};n.value||i(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(m,h,v){let k=e.indexOf("#"),p=k>-1?(o.host&&document.querySelector("base")?e:e.slice(k))+m:yc()+e+m;try{t[v?"replaceState":"pushState"](h,"",p),n.value=h}catch(g){console.error(g),o[v?"replace":"assign"](p)}}function a(m,h){i(m,qe({},t.state,la(n.value.back,m,n.value.forward,!0),h,{position:n.value.position}),!0),r.value=m}function u(m,h){let v=qe({},n.value,t.state,{forward:m,scroll:on()});i(v.current,v,!0),i(m,qe({},la(r.value,m,null),{position:v.position+1},h),!1),r.value=m}return{location:r,state:n,push:u,replace:a}}function ba(e){e=Zi(e);let t=bc(e),o=gc(e,t.state,t.location,t.replace);function r(i,a=!0){a||o.pauseListeners(),history.go(i)}let n=qe({locatLine truncated
|
||||
`)),e.options=Object.assign({},e.options,m.options)})}i();let a=null;e.saveDefault=u=>{u&&u.preventDefault&&u.preventDefault();let m={terms:e.terms.split(`
|
||||
`).map(h=>h.trim()).filter(h=>h.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",m).then(()=>{i(),e.saving=!1,e.message="Saved",a&&r.cancel(a),a=r(()=>{e.message=null},2500)},h=>{e.saving=!1;let v=h&&h.data&&h.data.error;o("ERRORS."+v).then(k=>{e.error=k},()=>{e.error="Unable to save your defaults. Please try again."})})},e.deleteAccount=()=>{confirm("Delete your account? All your anonymized repositories, gists, and pull requests will be removed, and your personal data will be erased. This cannot be undone.")&&(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},Ra=function(e,t,o){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(r=>{o.url("/dashboard")},r=>{e.error=r.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},Ta=function(e,t,o,r,n){e.user&&!e.user.status&&o.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&o.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,m){let h={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(h===void 0)return;u.preventDefault();let v=e.features.length,k=h==="first"?0:h==="last"?v-1:(m+h+v)%v;e.feature=e.features[k].key,n(()=>{let p=r.document.getElementById("feature-tab-"+e.feature);p&&p.focus()})},e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}];function i(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests})}i();function a(u){let m={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return m;let h=new Array(u.length-1);for(let b=1;b<u.length;b++)h[b-1]=u[b]-u[b-1];let v=h.length,k=Math.max.apply(null,h),p=Math.min.apply(null,h),g=Math.min(0,p),w=k-g||1;m.viewW=v*2,m.bars=new Array(v);for(let b=0;b<v;b++){let T=(h[b]-g)/w,N=Math.max(1.5,T*34);m.bars[b]={x:(b*2+.25).toFixed(2),y:(36-N).toFixed(2),w:"1.5",h:N.toFixed(2)}}if(m.deltaToday=h[v-1],v>=2){let b=h[v-2];b&&(m.pctChange=(m.deltaToday-b)/b*100)}return m.pctAbs=Math.round(Math.abs(m.pctChange)),m.isUp=m.pctChange>=0,m}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let m=u.data||[];e.history={repositories:a(m.map(h=>h.nbRepositories||0)),users:a(m.map(h=>h.nbUsers||0)),pageViews:a(m.map(h=>h.nbPageViews||0)),pullRequests:a(m.map(h=>h.nbPullRequests||0))}})},Oa=function(e,t,o,r,n,i){let a=mt();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.items=[],e.search="",e.loading=!0,e.statusKeyLabels={ready:"Ready",progress:"In progress",error:"Error",expired:"Expired",removed:"Removed"};let u=["queue","download","downloaded","preparing","anonymizing"];function m(C){return C==="ready"||C==="error"?C:C==="expired"||C==="expiring"?"expired":C==="
Line truncated
|
||||
`)),e.options=Object.assign({},e.options,W.options),e.options.expirationDate=W.options&&W.options.expirationDate?new Date(W.options.expirationDate):m(),D&&D()})}function p(D,V,W){e.anonymize&&e.anonymize[D]&&e.anonymize[D].setValidity(V,W)}function g(D){try{let V=parseGithubUrl(D);if(V&&V.owner&&V.repo)return V.owner+"/"+V.repo}catch{}return null}function w(){return!e.isUpdate||!e._originalRepositoryID?void 0:g(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function b(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function T(){let D;try{D=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!D||D.path!==n.path()||Date.now()-D.savedAt>=30*6e4)return!1;for(let V of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(D.draft,V)&&(e[V]=D.draft[V]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await b(),sessionStorage.removeItem("github-access-draft"),!0}k(()=>{!r.repoId&&!r.pullRequestId&&!r.gistId&&a(T,0),r.repoId&&r.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=r.repoId,t.get("/api/repo/"+e.repoId).then(async D=>{e.githubConnection=D.data.connection||"oauth",e.sourceUrl="https://github.com/"+D.data.source.fullName,e._originalFullName=D.data.source.fullName,e.terms=D.data.options.terms.filter(V=>V).join(`
|
||||
`),e.source=D.data.source,e.role=D.data.role||"owner",e.coauthors=D.data.coauthors||[],e._originalBranch=D.data.source.branch,e.options=Object.assign({},e.options,D.data.options),e.conference=D.data.conference,e._originalConference=D.data.conference,e.repositoryID=D.data.source.repositoryID,e._originalRepositoryID=D.data.source.repositoryID,D.data.options.expirationDate&&(e.options.expirationDate=new Date(D.data.options.expirationDate)),!await T()&&(await Promise.all([N(),R()]),P())},()=>{n.url("/404")})),r.pullRequestId&&r.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=r.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async D=>{if(e.githubConnection=D.data.connection||"oauth",e.sourceUrl="https://github.com/"+D.data.source.repositoryFullName+"/pull/"+D.data.source.pullRequestId,e.terms=D.data.options.terms.filter(V=>V).join(`
|
||||
`),e.source=D.data.source,e.options=Object.assign({},e.options,D.data.options),e.conference=D.data.conference,e._originalConference=D.data.conference,D.data.options.expirationDate&&(e.options.expirationDate=new Date(D.data.options.expirationDate)),!await T())try{e.details=(await t.get(`/api/pr/${D.data.source.repositoryFullName}/${D.data.source.pullRequestId}`,{params:{connection:e.githubConnection}})).data}catch(V){let W=V&&V.data&&V.data.error;W&&(i("ERRORS."+W).then(oe=>{e.addToast({title:"Error",date:new Date,body:oe}),e.error=oe},console.error),Se(W))}},()=>{n.url("/404")})),r.gistId&&r.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=r.gistId,t.get("/api/gist/"+e.gistId).then(async D=>{e.sourceUrl="https://gist.github.com/"+D.data.source.gistId,e.terms=D.data.options.terms.filter(V=>V).join(`
|
||||
`),e.source=D.data.source,e.options=Object.assign({},e.options,D.data.options),e.conference=D.data.conference,e._originalConference=D.data.conference,D.data.options.expirationDate&&(e.options.expirationDate=new Date(D.data.options.expirationDate)),!await T()&&await se()},()=>{n.url("/404")}))}),t.get("/github/connections").then(D=>{e.githubConnections=D.data,!r.repoId&&!r.pullRequestId&&!r.gistId&&e.githubConnection===void 0&&D.data.appEnabled&&D.data.appConnected&&(e.githubConnection="github-app")}).catch(()=>{}),e.urlSelected=async(D=!1)=>{D||(e.terms=e.defaultTerms),!D&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],D||(e.source={type:"GitHubStream",branch:"",commit:""}),e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1;let V;try{V=parseGithubUrl(e.sourceUrl)}catch{p("sourceUrl","github",!1);return}p("sourceUrl","github",!0);try{V.gistId&&!V.repo?(e.detectedType="gist",e.source={gistId:V.gistId},await se()):V.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:V.owner+"/"+V.repo,pullRequestId:V.pullRequestId},await Z()):(e.detectedType="repo",await Promise.all([N(),R()]),P())}catch{return}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let D=e.branches.filter(W=>W.name==e.source.branch)[0];if(!D)return;!(e.isUpdate&&e._originalBranch===e.source.branch&&!!e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=D.commit),e.readme=D.readme,await R(),P()}),e.getBranches=async D=>{let V=parseGithubUrl(e.sourceUrl);try{let W=await t.get(`/api/repo/${V.owner}/${V.repo}/branches`,{params:{anonymizedRepoId:e.isUpdate&&r.repoId&&g(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:D===!0?"1":"0",repositoryID:w()}});e.branches=W.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let oe=e.branches.filter(le=>le.name==e.source.branch);oe.length>0&&(!(!D&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=oe[0].commit),e.readme=oe[0].readme,await R(D))}catch(W){e.branches=[],e.sourceUnreachable=W&&(W.status===404||W.data&&W.data.error==="repo_not_found");let oe=W&&W.data&&W.data.error||(W&&W.status===404?"repo_not_found":"unknown_error");i("ERRORS."+oe).then(le=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:le}),e.error=le},console.error),typeof p=="function"&&p("sourceUrl","missing",!1)}};async function N(){let D=parseGithubUrl(e.sourceUrl);try{Te();let V=await t.get(`/api/repo/${D.owner}/${D.repo}/`,{params:{anonymizedRepoId:e.isUpdate&&r.repoId&&g(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,repositoryID:w(),force:"1"}});e.details=V.data,e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches()}catch(V){throw V.data&&(i("ERRORS."+V.data.error).then(W=>{e.addToast({title:"Error",date:new Date,body:W}),e.error=W},console.error),Se(V.data.error)),p("sourceUrl","missing",!1),V}}async function R(D){if(e.readme&&!D)return e.readme;let V=parseGithubUrl(e.sourceUrl);try{let W=await t.get(`/api/repo/${V.owner}/${V.repo}/readme`,{params:{anonymizedRepoId:e.isUpdate&&r.repoId&&g(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:D===!0?"1":"0",branch:e.source.branch,repositoryID:w()}});e.readme=W.data}catch{e.readme=""}}function I(){let D={terms:e.terms?e.terms.split(`
|
||||
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(D.branchName=e.source.branch);try{let V=parseGithubUrl(e.sourceUrl);D.repoName=`${V.owner}/${V.repo}`}catch{}return D}function C(D,V){let W=null,oe=0;return function(){W&&a.cancel(W),W=a(()=>{W=null;let L=++oe,te=D();te&&t.post("/api/anonymize-preview",te).then(U=>{L===oe&&V(U.data)},()=>{})},200)}}let A=C(()=>e.readme?{content:e.readme,options:I()}:null,D=>{e.anonymize_readme=D.content||"";let V="";try{let oe=parseGithubUrl(e.sourceUrl),le=e.source.branch||e.details&&e.details.defaultBranch||"main";V=`https://github.com/${oe.owner}/${oe.repo}/raw/${le}/`}catch{}let W=renderMD(e.anonymize_readme,V);e.html_readme=W,a(Prism.highlightAll,150)});function P(){!e.anonymize||!e.anonymize.terms||(e.termsRegexWarning=!!e.terms&&!!e.terms.match(/[-[\]{}()*+?.,\\^$|#]/g),A())}async function Z(){let D=parseGithubUrl(e.sourceUrl);try{Te();let V=await t.get(`/api/pr/${D.owner}/${D.repo}/${D.pullRequestId}`,{params:{connection:e.githubConnection}});e.details=V.data,e.pullRequestId||(e.pullRequestId=D.repo+"-PR"+D.pullRequestId+"-"+generateRandomId(4))}catch(V){throw V.data&&(i("ERRORS."+V.data.error).then(W=>{e.addToast({title:"Error",date:new Date,body:W}),e.error=W},console.error),Se(V.data.error)),p("sourceUrl","missing",!1),V}}let Y=ze(new Map),j=new Set;function z(){let D=new Set,V=e.details&&e.details.pullRequest;if(!V)return D;typeof V.title=="string"&&D.add(V.title),typeof V.body=="string"&&D.add(V.body),typeof V.diff=="string"&&D.add(V.diff);let W=V.comments||[];for(let oe of W)typeof oe.author=="string"&&D.add(oe.author),typeof oe.body=="string"&&D.add(oe.body);return D}let ae=C(()=>{let D=z();j=D;let V=Array.from(D);return V.length===0?null:{contents:V,options:I()}},D=>{if(!D||!Array.isArray(D.contents))return;let V=Array.from(j),W=new Map;for(let oe=0;oe<V.length&&oe<D.contents.length;oe++)W.set(V[oe],D.contents[oe]);Y.clear(),W.forEach((oe,le)=>Y.set(le,oe))});e.anonymizePrContent=function(D){return D&&(Y.has(D)?Y.get(D):(j.has(D)||ae(),D))};async function se(){let D=parseGithubUrl(e.sourceUrl);try{if(Te(),e.gistOAuthRequired=!1,e.githubConnections?.oauthConnected===!1){e.gistOAuthRequired=!0,p("sourceUrl","missing",!1);return}let V=await t.get(`/api/gist/source/${D.gistId}`);e.details=V.data,e.gistId||(e.gistId="gist-"+D.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(V){if(V.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,p("sourceUrl","missing",!1);return}throw V.data&&(i("ERRORS."+V.data.error).then(W=>{e.addToast({title:"Error",date:new Date,body:W}),e.error=W},console.error),Se(V.data.error)),p("sourceUrl","missing",!1),V}}let re=ze(new Map),ue=new Set;function ke(){let D=new Set,V=e.details&&e.details.gist;if(!V)return D;typeof V.description=="string"&&D.add(V.description),typeof V.ownerLogin=="string"&&D.add(V.ownerLogin);let W=V.files||[];for(let le of W)typeof le.filename=="string"&&D.add(le.filename),typeof le.content=="string"&&D.add(le.content);let oe=V.comments||[];for(let le of oe)typeof le.author=="string"&&D.add(le.author),typeof le.body=="string"&&D.add(le.body);return D}let he=C(()=>{let D=ke();ue=D;let V=Array.from(D);return V.length===0?null:{contents:V,options:I()}},D=>{if(!D||!Array.isArray(D.contents))return;let V=Array.from(ue),W=new Map;for(let oe=0;oe<V.length&&oe<D.contents.length;oe++)W.set(V[oe],D.contents[oe]);re.clear(),W.forEach((oe,le)=>re.set(le,oe)),me()});e.anonymizeGistContent=function(D){return D&&(re.has(D)?re.get(D):(ue.has(D)||he(),D))},e.previewGistFiles=[];function me(){let D=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=D.map(V=>({filename:e.anonymizeGistContent(V.filename),content:e.anonymizeGistContent(V.content),language:V.language}))}e.watch("details.gist.files",me,!0),e.watch("terms",me);function pe(){let D=e.conference;if(e.conference_data=null,!D)return;let V=e.isUpdate&&D===e._originalConference;t.get("/api/conferences/"+D).then(W=>{e.conference===D&&(e.conference_data=W.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!V&&(e.options.expirationDate=new Date(e.conference_data.endDate),e.options.expirationMode="remove",e.options.update=e.conference_data.options.update,e.options.image=e.conference_data.options.image,e.options.pdf=e.conference_data.options.pdf,e.options.notebook=e.conference_data.options.notebook,e.options.link=e.conference_data.options.link))},()=>{e.conference===D&&(e.conference_data=null)})}function Te(){p("repoId","used",!0),p("repoId","format",!0),p("pullRequestId","used",!0),p("pullRequestId","format",!0),p("gistId","used",!0),p("gistId","format",!0),p("sourceUrl","used",!0),p("sourceUrl","missing",!0),p("sourceUrl","access",!0),p("sourceUrl","github",!0),p("commit","exists",!0),p("conference","activated",!0),p("terms","format",!0),e.termsRegexWarning=!1}function Re(){let D=e.anonymize&&e.anonymize.expirationDate;ret
Line truncated
|
||||
`).filter(le=>le),connection:e.githubConnection,fullName:`${V.owner}/${V.repo}`,repository:e.sourceUrl,options:e.options,source:e.source,conference:e.conference};e.details&&(W.options.pageSource=e.details.pageSource),Te();let oe=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(oe,W,{headers:{"Content-Type":"application/json"}}).then(()=>{window.location.href="/status/"+e.repoId},le=>{le.data&&(i("ERRORS."+le.data.error).then(L=>{e.error=L},console.error),Se(le.data.error))}).finally(()=>{D.target.disabled=!1})},e.anonymizeGist=D=>{if(Re())return;D.target.disabled=!0;let V=parseGithubUrl(e.sourceUrl),W={gistId:e.gistId,terms:e.terms.trim().split(`
|
||||
`).filter(le=>le),source:{gistId:V.gistId},options:e.options,conference:e.conference};Te();let oe=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(oe,W,{headers:{"Content-Type":"application/json"}}).then(()=>{window.location.href="/gist/"+e.gistId},le=>{le.data&&(i("ERRORS."+le.data.error).then(L=>{e.error=L},console.error),Se(le.data.error))}).finally(()=>{D.target.disabled=!1})},e.anonymizePullRequest=D=>{if(Re())return;D.target.disabled=!0;let V=parseGithubUrl(e.sourceUrl),W={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
|
||||
`).filter(le=>le),source:{repositoryFullName:`${V.owner}/${V.repo}`,pullRequestId:V.pullRequestId},options:e.options,conference:e.conference};Te();let oe=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(oe,W,{headers:{"Content-Type":"application/json"}}).then(()=>{window.location.href="/pr/"+e.pullRequestId},le=>{le.data&&(i("ERRORS."+le.data.error).then(L=>{e.error=L},console.error),Se(le.data.error))}).finally(()=>{D.target.disabled=!1})},e.watch("conference",()=>{pe()}),e.watch("terms",()=>{e.detectedType==="repo"&&P(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&he()}),e.watch("options.image",()=>{e.detectedType==="repo"&&P(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&he()}),e.watch("options.link",()=>{e.detectedType==="repo"&&P(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&he()}),e.watch("details",()=>{e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&he()},!0)},Yo=function(e,t,o,r,n,i){let a=mt(),u=as(),m=0,h=!1;e.on("dispose",()=>{h=!0,m++,v&&v.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"keydown",function(z){if((z.metaKey||z.ctrlKey)&&z.key==="k"){z.preventDefault();var ae=document.querySelector(".tree-search-input");ae&&(ae.focus(),ae.select())}});var v=null;e.onFileSearchChange=function(){v&&(v.resolve(),v=null);let z=e.fileSearchQuery;if(!z||z.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let ae=i.defer();v=ae,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(z)}`,{timeout:ae.promise}).then(function(se){if(!(h||v!==ae)){v=null,e.fileSearchLoading=!1;var re={};e.files.forEach(function(L){re[(L.path||"")+"/"+L.name]=!0});for(var ue=[],ke={},he=0;he<se.data.length;he++){var me=se.data[he];if(me.path)for(var pe=me.path.split("/"),Te="",Re=0;Re<pe.length;Re++){var Se=Te;Te=Te?Te+"/"+pe[Re]:pe[Re];var D=Se+"/"+pe[Re];!re[D]&&!ke[D]&&(ke[D]=!0,ue.push({name:pe[Re],path:Se}))}}ue.sort(function(L,te){return(L.path||"").split("/").length-(te.path||"").split("/").length}),ue.length>0&&e.files.push.apply(e.files,ue);for(var V=[],W=0;W<se.data.length;W++){var oe=se.data[W],le=(oe.path||"")+"/"+oe.name;!re[le]&&oe.size!=null&&(V.push(oe),re[le]=!0)}V.length>0&&e.files.push.apply(e.files,V),e.fileSearchResults=se.data}},function(){!h&&v===ae&&(v=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let k={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},p=["license","txt"],g=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],w=["wav","mp3","ogg","wma","flac","aac","m4a"],b=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(z,ae){if(e.repoId!=r.repoId)return j();if((r.path||"")!=e.filePath){e.filePath=r.path||"",e.paths=e.filePath.split("/").filter(se=>se&&se.trim().length>0),Y();for(let se=0;se<e.paths.length-1;se++){let re=se>0?e.paths.slice(0,se).join("/"):"";e.files.some(ke=>ke.path===re)||e.getFiles(re)}}});function T(){if(e.paths[0]!="")return;let z=["readme.md","readme.txt","readme.org","readme.1st","readme"],ae={};for(let re of e.files)re.name.toLowerCase().indexOf("readme")>-1&&(ae[re.name.toLowerCase()]=re.name);let se=null;for(let re of z)if(ae[re]){se=re;break}if(!se&&Object.keys(ae).length>0&&(se=Object.keys(ae)[0]),se){let re=o.url();re[re.length-1]!="/"&&(re+="/"),o.url(re+encodePathForUrl(ae[se]))}}e.fileCounts=null,e.getFiles=function(z){let ae=e.repoId;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(z)}&v=${e.options.lastUpdateDate}`).then(function(se){if(h||ae!==e.repoId)return[];let re=z||"";return e.files=e.files.filter(ue=>ue.path!==re),e.files.push(...se.data),se.data},function(se){if(h||ae!==e.repoId)return[];e.type="error",e.content=se&&se.data&&se.data.error||"unknown_error",e.files=[]})};function N(){let z=e.repoId;t.get(`/api/repo/${e.repoId}/files/counts`).then(function(ae){h||z!==e.repoId||(e.fileCounts=ae.data)},function(){e.fileCounts={}})}function R(){return e.files.filter(z=>z.name==e.paths[e.paths.length-1]&&z.path==e.paths.slice(0,e.paths.length-1).join("/"))[0]}var I=null;e.on("dispose",function(){I&&a.interval.cancel(I)});function C(z){if(h)return;let ae=e.repoId;t.get(`/api/repo/${e.repoId}/options`).then(se=>{if(!(h||ae!==e.repoId)){if(e.options=se.data,e.options.url){window.location=e.options.url;return}z&&z(se.data)}},se=>{if(!(h||ae!==e.repoId)){var re=se.data||{};if(re.error==="rate_limited"&&re.resetAt){let ke=function(){var he=Math.max(0,Math.ceil((e.rateLimitResetAt-Date.now())/1e3));if(he<=0)e.rateLimitCountdown="",e.rateLimitResetAt=0,I&&(a.interval.cancel(I),I=null),C(z);else{var me=Math.floor(he/60),pe=he%60;e.rateLimitCountdown=me>0?me+"m "+pe+"s":pe+"s"}};var ue=ke;e.type="rate_limited",e.rateLimitResetAt=re.resetAt,I&&a.interval.cancel(I),ke(),I=a.interval(ke,1e3)}else re.error==="re
Line truncated
|
||||
`+b.join(`
|
||||
`)],{type:"text/csv"}),N=document.createElement("a");N.href=URL.createObjectURL(T),N.download=r.username+"-repositories.csv",N.click()},e.showStatusMessage=p=>{let g=p.statusMessage||"(no message)";window.prompt(`Status message for ${p.repoId} (${p.status}):`,g)},e.fetchGithubInfo=p=>{let g=window.open("","_blank");g&&g.document.write("<pre>Loading GitHub info for "+p.repoId+"...</pre>"),t.get("/api/admin/repos/"+p.repoId+"/github").then(w=>{g&&(g.document.open(),g.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(w.data,null,2).replace(/[<>]/g,b=>b==="<"?"<":">")+"</pre>"),g.document.close())},w=>{let b=w&&w.data?JSON.stringify(w.data,null,2):String(w);g&&(g.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+b+"</pre>")})};function m(p){t.get("/api/admin/users/"+p+"/repos",{}).then(g=>{e.repositories=g.data},g=>{console.error(g)})}function h(p){t.get("/api/admin/users/"+p,{}).then(g=>{e.userInfo=g.data},g=>{console.error(g)})}h(r.username),m(r.username),e.banUser=()=>{confirm(`Ban user ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/ban`).then(()=>h(r.username),p=>console.error(p))},e.activateUser=()=>{t.post(`/api/admin/users/${r.username}/activate`).then(()=>h(r.username),p=>console.error(p))},e.promoteUser=()=>{confirm(`Promote ${r.username} to admin?`)&&t.post(`/api/admin/users/${r.username}/promote`).then(()=>h(r.username),p=>console.error(p))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/demote`).then(()=>h(r.username),p=>console.error(p))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function v(){t.get("/api/admin/tokens").then(p=>{e.tokens=p.data||[]},p=>{p.status!==401&&p.status!==403&&console.error(p)})}v(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(p=>{e.tokenForm.plaintext=p.data.token,e.tokenForm.name="",v()},p=>console.error(p))},e.revokeToken=p=>{confirm(`Revoke token "${p.name}"?`)&&t.delete("/api/admin/tokens/"+p.id).then(()=>v(),g=>console.error(g))},e.removeCache=p=>{confirm("Remove cached files for "+p.repoId+"?")&&t.delete("/api/admin/repos/"+p.repoId).then(()=>m(r.username),g=>console.error(g))},e.removeRepository=p=>{confirm("Remove repository "+p.repoId+"?")&&t.delete("/api/repo/"+p.repoId+"/").then(()=>m(r.username),g=>console.error(g))},e.updateRepository=p=>{let g=ze({title:`Refreshing ${p.repoId}...`,date:new Date,body:`The repository ${p.repoId} is going to be refreshed.`});e.toasts.push(g),t.post(`/api/repo/${p.repoId}/refresh`).then(w=>{w.data.status=="ready"?g.title=`${p.repoId} is refreshed.`:g.title=`Refreshing of ${p.repoId}.`},w=>{g.title=`Error during the refresh of ${p.repoId}.`,g.body=w.body})},e.getGitHubRepositories=p=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(g=>{e.userInfo.repositories=g.data})};let k=null;e.watch("query",()=>{n.timeout.cancel(k),k=n.timeout(()=>{m(r.username)},500)},!0)},za=function(e,t,o){let r=mt(),n=as();e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let i=w=>{if(w.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){w.preventDefault();let b=document.querySelector('.admin-filter-toolbar input[type="search"]');b&&b.focus()}};n(document,"keydown",i),e.on("dispose",()=>document.removeEventListener("keydown",i)),e.clearFilter=w=>{w==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[w]="",e.query.page=1},e.chips=[];let a=()=>{let w=[];(e.query.dateFrom||e.query.dateTo)&&w.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=w};e.statusCountFor=w=>{let b=(e.statusCounts||[]).find(T=>T._id===w);return b?b.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=w=>{e.query.sort===w?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=w,e.query.direction="desc"),e.query.page=1},e.sortIcon=w=>e.query.sort===w?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",m={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!1,expired:!1,removed:!1,error:!0,preparing:!0},h=loadFilterPrefs(u)||{};e.query=Object.assign({},m,h,{page:1,search:""});let v=o.search();v.search&&(e.query.search=v.search);let k="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(k)||"[]"),e.savePreset=()=>{let w=window.prompt("Preset name:");if(!w)return;let b=Object.assign({},e.query);delete b.page,e.presets=(e.presets||[]).filter(T=>T.name!==w),e.presets.push({name:w,query:b}),localStorage.setItem(k,JSON.stringify(e.presets))},e.applyPreset=w=>{Object.assign(e.query,w.query,{page:1})},e.deletePreset=w=>{e.presets=(e.presets||[]).filter(b=>b.name!==w.name),localStorage.setItem(k,JS
Line truncated
|
||||
`).map((he,me)=>me===0?he:ke+he).join(`
|
||||
`)}else typeof j=="number"||typeof j=="boolean"?ue=String(j):ue=JSON.stringify(j);Z.push(`${se}${ue}${re}`)}),Z.push("}"),Z.join(`
|
||||
`)}function h(N){let R=[],I="",C=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,A=0,P;for(;P=C.exec(N);)I+=N.slice(A,P.index),A=C.lastIndex,R.push({key:P[1],op:P[2]||"=",val:P[3]});return I+=N.slice(A),{filters:R,free:I.trim().toLowerCase()}}function v(N,R){for(let I of R.filters){let C=(P,Z,Y)=>{let j=parseFloat(P),z=parseFloat(Z);return Y==="="?String(P)===String(Z):Y==="!="?String(P)!==String(Z):Y===">="?j>=z:Y==="<="?j<=z:Y===">"?j>z:Y==="<"?j<z:!0},A;if(I.key==="code")A=N.displayMessage;else if(I.key==="module")A=N.module;else if(I.key==="status")A=N._status;else if(I.key==="url")A=N._url;else if(I.key==="repo")A=N._repoId;else if(I.key==="level")A=N.level;else continue;if(A==null||!C(A,I.val,I.op))return!1}return!(R.free&&((N.displayMessage||"")+" "+(N.module||"")+" "+(N._url||"")+" "+JSON.stringify(N.raw||[])).toLowerCase().indexOf(R.free)===-1)}function k(){let N=h(e.query.search||"");e.parsedFilterCount=N.filters.length;let R=e.query.bucket,I=e.entries.filter(A=>R&&A._bucket!==R?!1:v(A,N)),C=e.query.group;if(C){let A=Y=>C==="module"?Y.module:Y.displayMessage||Y.message||"_",P=new Map;for(let Y of I){let j=A(Y);if(P.has(j)){let z=P.get(j);z.count++,z._related.push(Y),new Date(Y.ts)>new Date(z.ts)&&(z.ts=Y.ts,z._url=Y._url,z._status=Y._status),new Date(Y.ts)<new Date(z._firstSeen)&&(z._firstSeen=Y.ts)}else{let z=Object.assign({},Y);z._key=`${C}:${j}`,z._related=[Y],z._firstSeen=Y.ts,z._lastHourCount=0,z.count=1,P.set(j,z)}}let Z=Date.now()-3600*1e3;for(let Y of P.values())Y._lastHourCount=Y._related.filter(j=>new Date(j.ts).getTime()>=Z).length;I=Array.from(P.values())}else I=I.map((A,P)=>(A._key="row:"+P+":"+A.ts,A._related=[A],A._firstSeen=A.ts,A._lastHourCount=0,A.count=1,A));e.query.sort==="count"?I.sort((A,P)=>P.count-A.count||new Date(P.ts)-new Date(A.ts)):I.sort((A,P)=>new Date(P.ts)-new Date(A.ts)),e.visible=I}function p(N){let R=N?e.entries.length:0,I=N?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:R,limit:I}}).then(C=>{let A=(C.data.entries||[]).map(u);e.entries=N?e.entries.concat(A):A,e.available=!!C.data.available,e.cap=C.data.max||e.cap,e.total=C.data.total||e.entries.length,k()},C=>console.error(C))}e.loadMore=()=>p(!0),e.canLoadMore=()=>e.entries.length<e.total;function g(){t.get("/api/admin/errors/stats").then(N=>{let R=N.data||{},I=R.prev24h?Math.round((R.last24h-R.prev24h)/R.prev24h*100):0;e.stats={last24h:R.last24h||0,prev24h:R.prev24h||0,delta:I,severity:R.severity||{error:0,warn:0,info:0},unique:R.unique||{error:0,warn:0,info:0},buckets:R.buckets||[],dropped:R.dropped||0}},N=>console.error(N))}function w(){p(),g()}e.barPx=(N,R)=>{let I=e.stats.buckets||[],C=0;for(let Y of I)C=Math.max(C,(Y.error||0)+(Y.warn||0)+(Y.info||0));if(!C)return 0;let A=(N.error||0)+(N.warn||0)+(N.info||0);if(!A)return 0;let P=Math.round(A/C*60),Z=N[R]||0;return Math.round(Z/A*P)},e.bucketTitle=N=>`${new Date(N.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${N.error||0} err \xB7 ${N.warn||0} warn \xB7 ${N.info||0} info`,e.toggle=N=>{e.expanded[N._key]=!e.expanded[N._key]},e.setBucket=N=>{e.query.bucket=N},e.refreshNow=w,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(w,N=>console.error(N))},e.exportCsv=()=>{let N=["ts","level","module","displayMessage","_status","_url","_repoId"],R=[N.join(",")];for(let P of e.visible)R.push(N.map(Z=>{let Y=P[Z]==null?"":String(P[Z]);return/[",\n]/.test(Y)?`"${Y.replace(/"/g,'""')}"`:Y}).join(","));let I=new Blob([R.join(`
|
||||
`)],{type:"text/csv;charset=utf-8"}),C=URL.createObjectURL(I),A=document.createElement("a");A.href=C,A.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(A),A.click(),document.body.removeChild(A),URL.revokeObjectURL(C)};function b(N){e.copyHint=`${N} copied`,n.timeout(()=>{e.copyHint=""},1500)}e.copyJson=N=>{navigator.clipboard.writeText(N._detailJson).then(()=>b("JSON"))},e.copyCurl=N=>{if(!N._url)return;let I=`curl -X ${N._method||"GET"} '${window.location.origin}${N._url}'`;navigator.clipboard.writeText(I).then(()=>b("curl"))},w();let T=r(()=>{e.query.autoRefresh&&w()},15e3);e.on("dispose",()=>r.cancel(T)),e.watch("query.search",k),e.watch("query.bucket",k),e.watch("query.sort",k),e.watch("query.group",k)},Ga=function(e,t,o,r){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null){o.url("/");return}e.data=null,e.loading=!0,e.error=null;function n(p){if(p==null)return"\u2014";for(var g=["B","KB","MB","GB","TB"],w=0,b=p;b>=1024&&w<g.length-1;)b/=1024,w++;return b.toFixed(w>0?1:0)+" "+g[w]}e.humanBytes=n;function i(p){if(!p)return"\u2014";var g=Math.floor(p/86400),w=Math.floor(p%86400/3600),b=Math.floor(p%3600/60);return g>0?g+"d "+(w<10?"0":"")+w+"h":w>0?w+"h "+(b<10?"0":"")+b+"m":b+"m"}e.humanDuration=i;function a(p){return p==null?"\u2014":p>=1e6?(p/1e6).toFixed(1)+"M":p>=1e3?(p/1e3).toFixed(1)+"K":String(p)}e.humanNum=a,e.queueTotal=function(p){return p?(p.waiting||0)+(p.active||0)+(p.delayed||0)+(p.failed||0):0},e.statusCount=function(p){if(!e.data||!e.data.repos)return 0;for(var g=e.data.repos.statusBreakdown||[],w=0;w<g.length;w++)if(g[w]._id===p)return g[w].count;return 0},e.barPct=function(p){var g=e.data&&e.data.repos?e.data.repos.total:0;if(!g)return 0;var w=[p];p==="expired"&&w.push("expiring"),p==="removed"&&w.push("removing"),p==="preparing"&&w.push("download");var b=0;return w.forEach(function(T){b+=e.statusCount(T)}),Math.max(.4,b/g*100)},e.errPct=function(p){if(!e.data||!e.data.errors)return 0;var g=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.errors.severity.info,1);return e.data.errors.severity[p]/g*100};function u(p){var g=p||[];return g.map(function(w,b){var T=g[b-1]||{},N=Object.assign({},w);return N.dailyRepositories=b?Math.max(0,(w.nbRepositories||0)-(T.nbRepositories||0)):0,N.dailyUsers=b?Math.max(0,(w.nbUsers||0)-(T.nbUsers||0)):0,N.dailyPageViews=b?Math.max(0,(w.nbPageViews||0)-(T.nbPageViews||0)):0,N})}function m(p){var g=p&&p.length?p[p.length-1]:{};return{repositories:g.dailyRepositories||0,users:g.dailyUsers||0,pageViews:g.dailyPageViews||0}}var h={};e.historyBarH=function(p,g){return!p||!h[g]?0:Math.max(1,Math.round(p[g]/h[g]*140))},e.historyLabel=function(p){if(!p||!p.date)return"";var g=new Date(p.date);return g.getUTCMonth()+1+"/"+g.getUTCDate()};function v(){t.get("/api/admin/overview").then(function(p){p.data.history=u(p.data.history),p.data.daily={today:m(p.data.history)},e.data=p.data,e.loading=!1,e.error=null,h={},(p.data.history||[]).forEach(function(g){["dailyPageViews","dailyRepositories","dailyUsers","nbUsers"].forEach(function(w){(!h[w]||g[w]>h[w])&&(h[w]=g[w])})})},function(p){e.loading=!1,e.error=p.data&&p.data.error||"Failed to load overview"})}v();var k=r(v,3e4);e.on("dispose",function(){r.cancel(k)})};var ja=[{path:"/connections",template:"partials/connections.htm",title:"GitHub connections \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Ma(e,t.http)},{path:"/signin",template:"partials/signin.htm",title:"Sign in \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>$a(e,t.http)},{path:"/",template:"partials/home.htm",title:"Anonymous GitHub \u2013 Share the code, not the author",preserveExplorer:!1,setup:(e,t)=>Ta(e,t.http,t.location,t.window,t.timeout)},{path:"/dashboard",template:"partials/dashboard.htm",title:"Your anonymizations \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Oa(e,t.http,t.location,t.promises,t.window,t.quotaService)},{path:"/pr-dashboard",redirect:"/dashboard"},{path:"/anonymize/:repoId?",template:"partials/anonymize.htm",title:"New anonymization \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Yn(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/pull-request-anonymize/:pullRequestId?",template:"partials/anonymize.htm",title:"Anonymize a pull request \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Yn(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/gist-anonymize/:gistId?",template:"partials/anonymize.htm",title:"Anonymize a gist \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Yn(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/status/:repoId",template:"partials/status.htm",title:"Repository status \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Aa(e,t.http,t.params)},{path:"/conferences",template:"partials/conferences.htm",title:"Your conferences \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Pa(e,t.http,t.location)
Line truncated
|
||||
anonymous_github`)])])])])]),f(" Limitations "),s("h4",{class:"faq-cat"},[s("i",{class:"fas fa-exclamation-triangle mr-2"}),y("Limitations ")]),s("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[s("h3",{class:"panel-title"},[s("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? ")])]),s("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[s("div",{class:"panel-body p-3"},[s("ul",null,[s("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),s("li",null," Files larger than 8 MB are not supported. "),s("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),s("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),f(" Privacy & Security "),s("h4",{class:"faq-cat"},[s("i",{class:"fas fa-shield-alt mr-2"}),y("Privacy & Security ")]),s("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[s("h3",{class:"panel-title"},[s("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? ")])]),s("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[s("div",{class:"panel-body p-3"},[s("p",null," Data stored on Anonymous GitHub is never shared or used for any purpose beyond providing the anonymization service. When a repository is removed or expires, only its configuration is retained \u2014 this makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[s("h3",{class:"panel-title"},[s("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? ")])]),s("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[s("div",{class:"panel-body p-3"},[s("p",null," Anonymous GitHub only reads your repositories \u2014 it never pushes commits, opens issues, modifies settings, or deletes anything. From your perspective as a user, the service is read-only. "),s("p",null,[y(" However, GitHub's OAuth scopes do not offer a read-only option for private repositories: the only scope that grants access to private repos is "),s("code",null,"repo"),y(", which is documented as full read/write access. To support users who want to anonymize a private repository, Anonymous GitHub must request that scope, and GitHub then displays the broader permission text at sign-in. The application itself only ever performs read operations against the GitHub API. ")]),s("p",null,[y(" If you only anonymize public repositories, the source code is open and can be audited on the "),s("a",{href:"https://github.com/tdurieux/anonymous_github/",target:"_blank"},"GitHub repository"),y(", or you can self-host your own instance. ")])])])]),s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[s("h3",{class:"panel-title"},[s("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? ")])]),s("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[s("div",{class:"panel-body p-3"},[s("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),f(" Self-Hosting "),s("h4",{class:"faq-cat"},[s("i",{class:"fas fa-server mr-2"}),y("Self-Hosting ")]),s("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[s("h3",{class:"panel-title"},[s("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-ho
Line truncated
|
||||
`+JSON.stringify(t):""}`},[je.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${Fc(t)}" via a navigation guard.`},[je.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[je.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[je.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function po(e,t){return Ae(new Error,{type:e,[ba]:!0},t)}function $t(e,t){return e instanceof Error&&ba in e&&(t==null||!!(e.type&t))}var Uc=["params","query","hash"];function Fc(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let s of Uc)s in e&&(t[s]=e[s]);return JSON.stringify(t,null,2)}function wa(e){let t={};if(e===""||e==="?")return t;let s=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;r<s.length;++r){let n=s[r].replace(ia," "),i=n.indexOf("="),a=To(i<0?n:n.slice(0,i)),u=i<0?null:To(n.slice(i+1));if(a in t){let f=t[a];wt(f)||(f=t[a]=[f]),f.push(u)}else t[a]=u}return t}function xn(e){let t="";for(let s in e){let r=e[s];if(s=Ac(s),r==null){r!==void 0&&(t+=(t.length?"&":"")+s);continue}(wt(r)?r.map(n=>n&&Gn(n)):[r&&Gn(r)]).forEach(n=>{n!==void 0&&(t+=(t.length?"&":"")+s,n!=null&&(t+="="+n))})}return t}function ka(e){let t={};for(let s in e){let r=e[s];r!==void 0&&(t[s]=wt(r)?r.map(n=>n==null?null:""+n):r==null?r:""+r)}return t}var Ea=Symbol(""),er=Symbol(""),Zs=Symbol(""),xs=Symbol(""),en=Symbol("");function Oo(){let e=[];function t(r){return e.push(r),()=>{let n=e.indexOf(r);n>-1&&e.splice(n,1)}}function s(){e=[]}return{add:t,list:()=>e.slice(),reset:s}}function Kt(e,t,s,r,n,i=a=>a()){let a=r&&(r.enterCallbacks[n]=r.enterCallbacks[n]||[]);return()=>new Promise((u,f)=>{let v=m=>{m===!1?f(po(je.NAVIGATION_ABORTED,{from:s,to:t})):m instanceof Error?f(m):Lc(m)?f(po(je.NAVIGATION_GUARD_REDIRECT,{from:t,to:m})):(a&&r.enterCallbacks[n]===a&&typeof m=="function"&&a.push(m),u())},h=i(()=>e.call(r&&r.instances[n],t,s,v)),k=Promise.resolve(h);e.length<3&&(k=k.then(v)),k.catch(m=>f(m))})}function tn(e,t,s,r,n=i=>i()){let i=[];for(let a of e)for(let u in a.components){let f=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(na(f)){let v=(f.__vccOpts||f)[t];v&&i.push(Kt(v,s,r,a,u,n))}else{let v=f();i.push(()=>v.then(h=>{if(!h)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let k=wc(h)?h.default:h;a.mods[u]=h,a.components[u]=k;let m=(k.__vccOpts||k)[t];return m&&Kt(m,s,r,a,u,n)()}))}}return i}function Ca(e,t){let s=[],r=[],n=[],i=Math.max(t.matched.length,e.matched.length);for(let a=0;a<i;a++){let u=t.matched[a];u&&(e.matched.find(v=>co(v,u))?r.push(u):s.push(u));let f=e.matched[a];f&&(t.matched.find(v=>co(v,f))||n.push(f))}return[s,r,n]}var zc=()=>location.protocol+"//"+location.host;function Va(e,t){let{pathname:s,search:r,hash:n}=t,i=e.indexOf("#");if(i>-1){let a=n.includes(e.slice(i))?e.slice(i).length:1,u=n.slice(a);return u[0]!=="/"&&(u="/"+u),Jn(u,"")}return Jn(s,e)+r+n}function Hc(e,t,s,r){let n=[],i=[],a=null,u=({state:m})=>{let E=Va(e,location),y=s.value,w=t.value,D=0;if(m){if(s.value=E,t.value=m,a&&a===y){a=null;return}D=w?m.position-w.position:0}else r(E);n.forEach(R=>{R(s.value,y,{delta:D,type:Qs.pop,direction:D?D>0?Xs.forward:Xs.back:Xs.unknown})})};function f(){a=s.value}function v(m){n.push(m);let E=()=>{let y=n.indexOf(m);y>-1&&n.splice(y,1)};return i.push(E),E}function h(){if(document.visibilityState==="hidden"){let{history:m}=window;if(!m.state)return;m.replaceState(Ae({},m.state,{scroll:us()}),"")}}function k(){for(let m of i)m();i=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",h),document.removeEventListener("visibilitychange",h)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",h),document.addEventListener("visibilitychange",h),{pauseListeners:f,listen:v,destroy:k}}function _a(e,t,s,r=!1,n=!1){return{back:e,current:t,forward:s,replaced:r,position:window.history.length,scroll:n?us():null}}function Bc(e){let{history:t,location:s}=window,r={value:Va(e,s)},n={value:t.state};n.value||i(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(f,v,h){let k=e.indexOf("#"),m=k>-1?(s.host&&document.querySelector("base")?e:e.slice(k))+f:zc()+e+f;try{t[h?"replaceState":"pushState"](v,"",m),n.value=v}catch(E){console.error(E),s[h?"replace":"assign"](m)}}function a(f,v){i(f,Ae({},t.state,_a(n.value.back,f,n.value.forward,!0),v,{position:n.value.position}),!0),r.value=f}function u(f,v){let h=Ae({},n.value,t.state,{forward:f,scroll:us()});i(h.current,h,!0),i(f,Ae({},_a(r.value,f,null),{position:h.position+1},v),!1),r.value=f}return{location:r,state:n,push:u,replace:a}}function qa(e){e=ma(e);let t=Bc(e),s=Hc(e,t.state,t.location,t.replace);function r(i,a=!0){a||s.pauseListeners(),history.go(i)}let n=Ae({locatLine truncated
|
||||
`)),e.options=Object.assign({},e.options,v.options),i.restore()})}a();let u=null;e.saveDefault=f=>{if(f&&f.preventDefault&&f.preventDefault(),e.saving)return;let v=i.checkpoint(),h={terms:e.terms.split(`
|
||||
`).map(k=>k.trim()).filter(k=>k.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",h).then(()=>{i.accept(v),e.saving=!1,e.message=e.unsavedChanges?null:"Saved",u&&r.cancel(u),u=r(()=>{e.message=null},2500)},k=>{e.saving=!1;let m=k&&k.data&&k.data.error,E="Unable to save your defaults. Please try again.";Promise.all([s("ERRORS."+m),s("ERRORS.unknown_error")]).then(([y,w])=>{e.error=m&&y!==w?y:E},()=>{e.error=E})})},e.deleteAccount=()=>{confirm("Delete your account? All your anonymized repositories, gists, and pull requests will be removed, and your personal data will be erased. This cannot be undone.")&&(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},Ya=function(e,t,s){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(r=>{s.url("/dashboard")},r=>{e.error=r.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},Ja=function(e,t,s,r,n){e.user&&!e.user.status&&s.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&s.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,f){let v={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(v===void 0)return;u.preventDefault();let h=e.features.length,k=v==="first"?0:v==="last"?h-1:(f+v+h)%h;e.feature=e.features[k].key,n(()=>{let m=r.document.getElementById("feature-tab-"+e.feature);m&&m.focus()})},e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}];function i(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests})}i();function a(u){let f={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return f;let v=new Array(u.length-1);for(let w=1;w<u.length;w++)v[w-1]=u[w]-u[w-1];let h=v.length,k=Math.max.apply(null,v),m=Math.min.apply(null,v),E=Math.min(0,m),y=k-E||1;f.viewW=h*2,f.bars=new Array(h);for(let w=0;w<h;w++){let D=(v[w]-E)/y,R=Math.max(1.5,D*34);f.bars[w]={x:(w*2+.25).toFixed(2),y:(36-R).toFixed(2),w:"1.5",h:R.toFixed(2)}}if(f.deltaToday=v[h-1],h>=2){let w=v[h-2];w&&(f.pctChange=(f.deltaToday-w)/w*100)}return f.pctAbs=Math.round(Math.abs(f.pctChange)),f.isUp=f.pctChange>=0,f}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let f=u.data||[];e.history={repositories:a(f.map(v=>v.nbRepositories||0)),users:a(f.map(v=>v.nbUsers||0)),pageViews:a(f.map(v=>v.nbPageViews||0)),pullRequests:a(f.map(v=>v.nbPullRequests||0))}})},Qa=function(e,t,s,r,n,i){let a=ct();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&s.url("/")}),e.user==null&&s.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.quotaExpanded=!1,e.closeQuota=()=>{e.quotaExpanded=!1,st(()=>document.querySelector(".dashboard-quota > summary")?.focus())},Jt()(document,"click",_=>{e.quotaExpanded&&!_.target.closest?.(".dashboard-quota")&
Line truncated
|
||||
`)),e.options=Object.assign({},e.options,B.options),e.options.expirationDate=B.options&&B.options.expirationDate?new Date(B.options.expirationDate):T(),!r.repoId&&!r.pullRequestId&&!r.gistId&&!e.anonymize?.terms?.dirty&&(e.terms=e.defaultTerms),C&&C()})}function U(C,P,B){e.anonymize&&e.anonymize[C]&&e.anonymize[C].setValidity(P,B)}function ae(C){try{let P=parseGithubUrl(C);if(P&&P.owner&&P.repo)return P.owner+"/"+P.repo}catch{}return null}e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,e.reconnectSource=async()=>{if(confirm("Reconnect to the repository currently at this GitHub URL? Review its branch and commit before saving. Saving will replace the published snapshot.")){e.reconnectingSource=!0,e.reconnectRepositoryId=void 0,e.source.commit="",e.source.branch="",e.readme="",e.branches=[];try{await Y()}catch{}}};function ie(){return e.reconnectingSource||!e.isUpdate||!e._originalRepositoryID?void 0:ae(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function _(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function O(){let C;try{C=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!C||C.path!==n.path()||Date.now()-C.savedAt>=30*6e4)return D.restore()?(e.sourceUrl&&await _(),!0):!1;D.restore();for(let P of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(C.draft,P)&&(e[P]=C.draft[P]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await _(),sessionStorage.removeItem("github-access-draft"),!0}q(()=>{!r.repoId&&!r.pullRequestId&&!r.gistId&&a(O,0),r.repoId&&r.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=r.repoId,t.get("/api/repo/"+e.repoId).then(async C=>{e.githubConnection=C.data.connection||"oauth",e.sourceUrl="https://github.com/"+C.data.source.fullName,e._originalFullName=C.data.source.fullName,e.terms=C.data.options.terms.filter(P=>P).join(`
|
||||
`),e.source=C.data.source,e.role=C.data.role||"owner",e.coauthors=C.data.coauthors||[],e._originalBranch=C.data.source.branch,e.options=Object.assign({},e.options,C.data.options),e.conference=C.data.conference,e._originalConference=C.data.conference,e.repositoryID=C.data.source.repositoryID,e._originalRepositoryID=C.data.source.repositoryID,C.data.options.expirationDate&&(e.options.expirationDate=new Date(C.data.options.expirationDate)),y=w(parseGithubUrl(e.sourceUrl)),!await O()&&(await Promise.all([Y(),Z()]),ue())},()=>{n.url("/404")})),r.pullRequestId&&r.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=r.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async C=>{if(e.githubConnection=C.data.connection||"oauth",e.sourceUrl="https://github.com/"+C.data.source.repositoryFullName+"/pull/"+C.data.source.pullRequestId,e.terms=C.data.options.terms.filter(P=>P).join(`
|
||||
`),e.source=C.data.source,e.options=Object.assign({},e.options,C.data.options),e.conference=C.data.conference,e._originalConference=C.data.conference,C.data.options.expirationDate&&(e.options.expirationDate=new Date(C.data.options.expirationDate)),y=w(parseGithubUrl(e.sourceUrl)),!await O())try{await pe()}catch{}},()=>{n.url("/404")})),r.gistId&&r.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=r.gistId,t.get("/api/gist/"+e.gistId).then(async C=>{e.sourceUrl="https://gist.github.com/"+C.data.source.gistId,e.terms=C.data.options.terms.filter(P=>P).join(`
|
||||
`),e.source=C.data.source,e.options=Object.assign({},e.options,C.data.options),e.conference=C.data.conference,e._originalConference=C.data.conference,C.data.options.expirationDate&&(e.options.expirationDate=new Date(C.data.options.expirationDate)),y=w(parseGithubUrl(e.sourceUrl)),!await O()&&await Ue()},()=>{n.url("/404")}))}),t.get("/github/connections").then(C=>{e.githubConnections=C.data,!r.repoId&&!r.pullRequestId&&!r.gistId&&e.githubConnection===void 0&&C.data.appEnabled&&C.data.appConnected&&(e.githubConnection="github-app",e.sourceUrl&&e.workspaceOpen&&_())}).catch(()=>{}),e.urlSelected=async(C=!1)=>{let P=++f,B;try{B=parseGithubUrl(e.sourceUrl.trim())}catch{e.sourceLoading=!1,se.cancel(),_e.cancel(),Pe.cancel(),e.previewStatus="idle",e.sourceUrlError=e.sourceUrl?"Please provide a valid GitHub URL.":"",U("sourceUrl","github",!1);return}let M=w(B),g=C||y===M;y=M,e.sourceUrlError="",U("sourceUrl","github",!0);let S=document.activeElement?.id==="sourceUrl-landing";e.workspaceOpen=!0,S&&a(()=>document.getElementById("sourceUrl")?.focus(),0),e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,!g&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],g||(e.source={type:"GitHubStream",branch:"",commit:""}),se.cancel(),_e.cancel(),Pe.cancel(),le.clear(),Re.clear(),e.previewStatus="idle",e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1,e.gistAppRequired=!1,e._preservingDraft=g;try{B.gistId&&!B.repo?(e.detectedType="gist",e.source={gistId:B.gistId},await Ue()):B.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:B.owner+"/"+B.repo,pullRequestId:B.pullRequestId},await pe()):(e.detectedType="repo",await Promise.all([Y(),Z()]),ue())}catch{}finally{P===f&&(e._preservingDraft=!1)}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let C=e.branches.filter(B=>B.name==e.source.branch)[0];if(!C)return;!(!e.reconnectingSource&&e.isUpdate&&e._originalBranch===e.source.branch&&!!e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=C.commit),e.readme=C.readme,await Z(),ue()}),e.getBranches=async C=>{let P=parseGithubUrl(e.sourceUrl),B=m();try{let M=await t.get(`/api/repo/${P.owner}/${P.repo}/branches`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&ae(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:C===!0||e.reconnectingSource?"1":"0",repositoryID:ie()}});if(!k(B))return;e.branches=M.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let g=e.branches.filter(S=>S.name==e.source.branch);g.length>0&&(!(!e.reconnectingSource&&!C&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=g[0].commit),e.readme=g[0].readme,await Z(C))}catch(M){if(!k(B))return;e.branches=[],e.sourceUnreachable=M&&(M.status===404||M.data&&M.data.error==="repo_not_found");let g=M&&M.data&&M.data.error||(M&&M.status===404?"repo_not_found":"unknown_error");i("ERRORS."+g).then(S=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:S}),e.error=S},console.error),typeof U=="function"&&U("sourceUrl","missing",!1)}finally{E(B)}};async function Y(){let C=parseGithubUrl(e.sourceUrl),P=m();try{F();let B=await t.get(`/api/repo/${C.owner}/${C.repo}/`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&ae(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,repositoryID:ie(),force:"1"}});if(!k(P))return;e.details=B.data,e.reconnectingSource&&(e.reconnectRepositoryId=e.details.externalId),e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches(e.reconnectingSource)}catch(B){if(!k(P))return;throw B.data&&(i("ERRORS."+B.data.error).then(M=>{e.addToast({title:"Error",date:new Date,body:M}),e.error=M},console.error),Q(B.data.error)),U("sourceUrl","missing",!1),B}finally{E(P)}}async function Z(C){if(e.readme&&!C)return e.readme;let P=parseGithubUrl(e.sourceUrl),B=m();try{let M=await t.get(`/api/repo/${P.owner}/${P.repo}/readme`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&ae(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:C===!0?"1":"0",branch:e.source.branch,repositoryID:ie()}});k(B,!0)&&(e.readme=M.data)}catch{k(B,!0)&&(e.readme="")}finally{E(B)}}function te(){let C={terms:e.terms?e.terms.split(`
|
||||
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(C.branchName=e.source.branch);try{let P=parseGithubUrl(e.sourceUrl);C.repoName=`${P.owner}/${P.repo}`}catch{}return C}function J(C,P,B){let M=null,g=0,S=()=>{if(e.detectedType!==C)return;M&&a.cancel(M);let V=++g;e.previewStatus="loading",M=a(()=>{M=null;let z=P();if(!z){e.previewStatus="idle";return}t.post("/api/anonymize-preview",z).then(j=>{V===g&&(B(j.data,z),e.previewStatus="ready")}).catch(()=>{V===g&&(e.previewStatus="error")})},200)};return S.cancel=()=>{g++,M&&a.cancel(M),M=null},e.on("dispose",S.cancel),S}e.retryPreview=()=>{e.detectedType==="repo"&&se(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Pe()};let se=J("repo",()=>e.readme?{content:e.readme,options:te()}:null,C=>{if(typeof C?.content!="string")throw Error("Invalid preview response");e.anonymize_readme=C.content;let P="";try{let M=parseGithubUrl(e.sourceUrl),g=e.source.branch||e.details&&e.details.defaultBranch||"main";P=`https://github.com/${M.owner}/${M.repo}/raw/${g}/`}catch{}let B=renderMD(e.anonymize_readme,P);e.html_readme=B,a(Prism.highlightAll,150)});function ue(){!e.anonymize||!e.anonymize.terms||(e.termsRegexWarning=!!e.terms&&!!e.terms.match(/[-[\]{}()*+?.,\\^$|#]/g),se())}async function pe(){let C=parseGithubUrl(e.sourceUrl),P=m();try{F();let B=await t.get(`/api/pr/${C.owner}/${C.repo}/${C.pullRequestId}`,{params:{connection:e.githubConnection}});if(!k(P))return;e.details=B.data,e.pullRequestId||(e.pullRequestId=C.repo+"-PR"+C.pullRequestId+"-"+generateRandomId(4))}catch(B){if(!k(P))return;throw B.data&&(i("ERRORS."+B.data.error).then(M=>{e.addToast({title:"Error",date:new Date,body:M}),e.error=M},console.error),Q(B.data.error)),U("sourceUrl","missing",!1),B}finally{E(P)}}let le=ze(new Map);function Oe(){let C=new Set,P=e.details&&e.details.pullRequest;if(!P)return C;typeof P.title=="string"&&C.add(P.title),typeof P.body=="string"&&C.add(P.body),typeof P.diff=="string"&&C.add(P.diff);let B=P.comments||[];for(let M of B)typeof M.author=="string"&&C.add(M.author),typeof M.body=="string"&&C.add(M.body);return C}let _e=J("pr",()=>{let C=Oe(),P=Array.from(C);return P.length===0?null:{contents:P,options:te()}},(C,P)=>{if(!Array.isArray(C?.contents)||C.contents.length!==P.contents.length||C.contents.some(g=>typeof g!="string"))throw Error("Invalid preview response");let B=P.contents,M=new Map;for(let g=0;g<B.length&&g<C.contents.length;g++)M.set(B[g],C.contents[g]);le.clear(),M.forEach((g,S)=>le.set(S,g))});e.anonymizePrContent=function(C){return C&&(le.has(C)?le.get(C):"")};async function Ue(){let C=parseGithubUrl(e.sourceUrl),P=m();try{F(),e.gistOAuthRequired=!1,e.gistAppRequired=!1;let B=await t.get(`/api/gist/source/${C.gistId}`);if(!k(P))return;e.details=B.data,e.gistId||(e.gistId="gist-"+C.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(B){if(!k(P))return;if(B.data?.error==="github_app_reconnect_required"){e.gistAppRequired=!0,U("sourceUrl","missing",!1);return}if(B.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,U("sourceUrl","missing",!1);return}throw B.data&&(i("ERRORS."+B.data.error).then(M=>{e.addToast({title:"Error",date:new Date,body:M}),e.error=M},console.error),Q(B.data.error)),U("sourceUrl","missing",!1),B}finally{E(P)}}let Re=ze(new Map);function $e(){let C=new Set,P=e.details&&e.details.gist;if(!P)return C;typeof P.description=="string"&&C.add(P.description),typeof P.ownerLogin=="string"&&C.add(P.ownerLogin);let B=P.files||[];for(let g of B)typeof g.filename=="string"&&C.add(g.filename),typeof g.content=="string"&&C.add(g.content);let M=P.comments||[];for(let g of M)typeof g.author=="string"&&C.add(g.author),typeof g.body=="string"&&C.add(g.body);return C}let Pe=J("gist",()=>{let C=$e(),P=Array.from(C);return P.length===0?null:{contents:P,options:te()}},(C,P)=>{if(!Array.isArray(C?.contents)||C.contents.length!==P.contents.length||C.contents.some(g=>typeof g!="string"))throw Error("Invalid preview response");let B=P.contents,M=new Map;for(let g=0;g<B.length&&g<C.contents.length;g++)M.set(B[g],C.contents[g]);Re.clear(),M.forEach((g,S)=>Re.set(S,g)),He()});e.anonymizeGistContent=function(C){return C&&(Re.has(C)?Re.get(C):"")},e.previewGistFiles=[];function He(){let C=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=C.map(P=>({filename:e.anonymizeGistContent(P.filename),content:e.anonymizeGistContent(P.content),language:P.language}))}e.watch("details.gist.files",He,!0),e.watch("terms",He);function Ee(){let C=e.conference;if(e.conference_data=null,!C)return;let P=e.isUpdate&&C===e._originalConference;t.get("/api/conferences/"+C).then(B=>{e.conference===C&&(e.conference_data=B.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!P&&(e.options.expirationDate=new Date(e.conference_data.endDate),e.options.expirationMode="remove",e.options.update=e.conference_data.options.update,e.options.image=e.confe
Line truncated
|
||||
`).filter(S=>S),connection:e.githubConnection,fullName:`${B.owner}/${B.repo}`,repository:e.sourceUrl,reconnectRepositoryId:e.reconnectRepositoryId,options:e.options,source:e.source,conference:e.conference};e.details&&(M.options.pageSource=e.details.pageSource),F();let g=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(g,M,{headers:{"Content-Type":"application/json"}}).then(()=>{D.clear(),window.location.href="/status/"+e.repoId},S=>{S.data&&(i("ERRORS."+S.data.error).then(V=>{e.error=V},console.error),Q(S.data.error))}).finally(()=>{P.disabled=!1})},e.anonymizeGist=C=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(oe())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let P=C.currentTarget||C.target.closest?.("button")||C.target;P.disabled=!0;let B=parseGithubUrl(e.sourceUrl),M={gistId:e.gistId,terms:e.terms.trim().split(`
|
||||
`).filter(S=>S),source:{gistId:B.gistId},options:e.options,conference:e.conference};F();let g=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(g,M,{headers:{"Content-Type":"application/json"}}).then(()=>{D.clear(),window.location.href="/gist/"+e.gistId},S=>{S.data&&(i("ERRORS."+S.data.error).then(V=>{e.error=V},console.error),Q(S.data.error))}).finally(()=>{P.disabled=!1})},e.anonymizePullRequest=C=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(oe())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let P=C.currentTarget||C.target.closest?.("button")||C.target;P.disabled=!0;let B=parseGithubUrl(e.sourceUrl),M={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
|
||||
`).filter(S=>S),source:{repositoryFullName:`${B.owner}/${B.repo}`,pullRequestId:B.pullRequestId},options:e.options,conference:e.conference};F();let g=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(g,M,{headers:{"Content-Type":"application/json"}}).then(()=>{D.clear(),window.location.href="/pr/"+e.pullRequestId},S=>{S.data&&(i("ERRORS."+S.data.error).then(V=>{e.error=V},console.error),Q(S.data.error))}).finally(()=>{P.disabled=!1})},e.watch("conference",()=>{Ee()}),e.watch("terms",()=>{e.detectedType==="repo"&&ue(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Pe()}),e.watch("options.image",()=>{e.detectedType==="repo"&&ue(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Pe()}),e.watch("options.link",()=>{e.detectedType==="repo"&&ue(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Pe()}),e.watch("details",()=>{e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Pe()},!0)},nr=function(e,t,s,r,n,i){let a=ct();window.Prism?.hooks&&!window.Prism.redactionHook&&(window.Prism.hooks.add("complete",O=>Ha(O.element)),window.Prism.redactionHook=!0);let u=Jt();e.on("dark-mode",(O,Y)=>{e.aceOption&&(Y?e.aceOption.theme="nord_dark":e.aceOption.theme="chrome")});let f=0,v=!1;e.on("dispose",()=>{v=!0,f++,h&&h.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"keydown",function(O){if((O.metaKey||O.ctrlKey)&&O.key==="k"){O.preventDefault();var Y=document.querySelector(".tree-search-input");Y&&(Y.focus(),Y.select())}});var h=null;e.onFileSearchChange=function(){h&&(h.resolve(),h=null);let O=e.fileSearchQuery;if(!O||O.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let Y=i.defer();h=Y,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(O)}`,{timeout:Y.promise}).then(function(Z){if(!(v||h!==Y)){h=null,e.fileSearchLoading=!1;var te={};e.files.forEach(function(F){te[(F.path||"")+"/"+F.name]=!0});for(var J=[],se={},ue=0;ue<Z.data.length;ue++){var pe=Z.data[ue];if(pe.path)for(var le=pe.path.split("/"),Oe="",_e=0;_e<le.length;_e++){var Ue=Oe;Oe=Oe?Oe+"/"+le[_e]:le[_e];var Re=Ue+"/"+le[_e];!te[Re]&&!se[Re]&&(se[Re]=!0,J.push({name:le[_e],path:Ue}))}}J.sort(function(F,oe){return(F.path||"").split("/").length-(oe.path||"").split("/").length}),J.length>0&&e.files.push.apply(e.files,J);for(var $e=[],Pe=0;Pe<Z.data.length;Pe++){var He=Z.data[Pe],Ee=(He.path||"")+"/"+He.name;!te[Ee]&&He.size!=null&&($e.push(He),te[Ee]=!0)}$e.length>0&&e.files.push.apply(e.files,$e),e.fileSearchResults=Z.data}},function(){!v&&h===Y&&(h=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let k={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},m=["license","txt"],E=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],y=["wav","mp3","ogg","wma","flac","aac","m4a"],w=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(O,Y){if(e.repoId!=r.repoId)return _();if((r.path||"")!=e.filePath){e.filePath=r.path||"",e.paths=e.filePath.split("/").filter(Z=>Z&&Z.trim().length>0),ie();for(let Z=0;Z<e.paths.length-1;Z++){let te=Z>0?e.paths.slice(0,Z).join("/"):"";e.files.some(se=>se.path===te)||e.getFiles(te)}}});function D(){if(e.paths[0]!="")return;let O=["readme.md","readme.txt","readme.org","readme.1st","readme"],Y={};for(let te of e.files)te.name.toLowerCase().indexOf("readme")>-1&&(Y[te.name.toLowerCase()]=te.name);let Z=null;for(let te of O)if(Y[te]){Z=te;break}if(!Z&&Object.keys(Y).length>0&&(Z=Object.keys(Y)[0]),Z){let te=s.url();te[te.length-1]!="/"&&(te+="/"),s.url(te+encodePathForUrl(Y[Z]))}}e.fileCounts=null,e.getFiles=function(O){let Y=e.repoId;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(O)}&v=${e.options.lastUpdateDate}`).then(function(Z){if(v||Y!==e.repoId)return[];let te=O||"";return e.files=e.files.filter(J=>J.path!==te),e.files.push(...Z.data),Z.data},function(Z){if(v||Y!==e.repoId)return[];e.type="error",e.content=Z&&Z.data&&Z.data.error||"unknown_error",e.files=[]})};function R(){let O=e.repoId;t.get(`/api/repo/${e.repoId}/files/counts`).then(function(Y){v||O!==e.repoId||(e.fileCounts=Y.data)},function(){e.fileCounts={}})}function T(){return e.files.filter(O=>O.name==e.paths[e.paths.length-1]&&O.path==e.paths.slice(0,e.paths.length-1).join("/"))[0]}var I=null;e.on("dispose",function(){I&&a.interval.cancel(I)});function H(O){if(v)return;let Y=e.repoId;t.get(`/api/repo/${e.repoId}/options`).then(Z=>{if(!(v||Y!==e.repoId)){if(e.options=Z.data,e.options.url){window.location=e.options.url;return}O&&O(Z.data)}},Z=>{if(!(v||Y!==e.repoId)){var te=Z.data||{};if(te.error==="rate_limited"&&te.resetAt){let se=function(){var ue=Math.max(0,Math.ceil((e.rateLimitResetAt-Date.now())/1e3));if(ue<=0)e.rateLimitCountdown="",e.rateLimitResetAt=0,I&&(a.interval.cancel(I),I=null),H(O);else{var pe=Math.floor(ue/60),l
Line truncated
|
||||
`+w.join(`
|
||||
`)],{type:"text/csv"}),R=document.createElement("a");R.href=URL.createObjectURL(D),R.download=r.username+"-repositories.csv",R.click()},e.showStatusMessage=m=>{let E=m.statusMessage||"(no message)";window.prompt(`Status message for ${m.repoId} (${m.status}):`,E)},e.fetchGithubInfo=m=>{let E=window.open("","_blank");E&&E.document.write("<pre>Loading GitHub info for "+m.repoId+"...</pre>"),t.get("/api/admin/repos/"+m.repoId+"/github").then(y=>{E&&(E.document.open(),E.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(y.data,null,2).replace(/[<>]/g,w=>w==="<"?"<":">")+"</pre>"),E.document.close())},y=>{let w=y&&y.data?JSON.stringify(y.data,null,2):String(y);E&&(E.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+w+"</pre>")})};function f(m){t.get("/api/admin/users/"+m+"/repos",{}).then(E=>{e.repositories=E.data},E=>{console.error(E)})}function v(m){t.get("/api/admin/users/"+m,{}).then(E=>{e.userInfo=E.data},E=>{console.error(E)})}v(r.username),f(r.username),e.banUser=()=>{confirm(`Ban user ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/ban`).then(()=>v(r.username),m=>console.error(m))},e.activateUser=()=>{t.post(`/api/admin/users/${r.username}/activate`).then(()=>v(r.username),m=>console.error(m))},e.promoteUser=()=>{confirm(`Promote ${r.username} to admin?`)&&t.post(`/api/admin/users/${r.username}/promote`).then(()=>v(r.username),m=>console.error(m))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/demote`).then(()=>v(r.username),m=>console.error(m))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function h(){t.get("/api/admin/tokens").then(m=>{e.tokens=m.data||[]},m=>{m.status!==401&&m.status!==403&&console.error(m)})}h(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(m=>{e.tokenForm.plaintext=m.data.token,e.tokenForm.name="",h()},m=>console.error(m))},e.revokeToken=m=>{confirm(`Revoke token "${m.name}"?`)&&t.delete("/api/admin/tokens/"+m.id).then(()=>h(),E=>console.error(E))},e.removeCache=m=>{confirm("Remove cached files for "+m.repoId+"?")&&t.delete("/api/admin/repos/"+m.repoId).then(()=>f(r.username),E=>console.error(E))},e.removeRepository=m=>{confirm("Remove repository "+m.repoId+"?")&&t.delete("/api/repo/"+m.repoId+"/").then(()=>f(r.username),E=>console.error(E))},e.updateRepository=m=>{let E=ze({title:`Refreshing ${m.repoId}...`,date:new Date,body:`The repository ${m.repoId} is going to be refreshed.`});e.toasts.push(E),t.post(`/api/repo/${m.repoId}/refresh`).then(y=>{y.data.status=="ready"?E.title=`${m.repoId} is refreshed.`:E.title=`Refreshing of ${m.repoId}.`},y=>{E.title=`Error during the refresh of ${m.repoId}.`,E.body=y.body})},e.getGitHubRepositories=m=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(E=>{e.userInfo.repositories=E.data})};let k=null;e.watch("query",()=>{n.timeout.cancel(k),k=n.timeout(()=>{f(r.username)},500)},!0)},al=function(e,t,s){let r=ct(),n=Jt();e.Math=Math,e.watch("user.status",()=>{e.user==null&&s.url("/")}),e.user==null&&s.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let i=y=>{if(y.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){y.preventDefault();let w=document.querySelector('.admin-filter-toolbar input[type="search"]');w&&w.focus()}};n(document,"keydown",i),e.on("dispose",()=>document.removeEventListener("keydown",i)),e.clearFilter=y=>{y==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[y]="",e.query.page=1},e.chips=[];let a=()=>{let y=[];(e.query.dateFrom||e.query.dateTo)&&y.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=y};e.statusCountFor=y=>{let w=(e.statusCounts||[]).find(D=>D._id===y);return w?w.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=y=>{e.query.sort===y?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=y,e.query.direction="desc"),e.query.page=1},e.sortIcon=y=>e.query.sort===y?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",f={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!1,expired:!1,removed:!1,error:!0,preparing:!0},v=loadFilterPrefs(u)||{};e.query=Object.assign({},f,v,{page:1,search:""});let h=s.search();h.search&&(e.query.search=h.search);let k="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(k)||"[]"),e.savePreset=()=>{let y=window.prompt("Preset name:");if(!y)return;let w=Object.assign({},e.query);delete w.page,e.presets=(e.presets||[]).filter(D=>D.name!==y),e.presets.push({name:y,query:w}),localStorage.setItem(k,JSON.stringify(e.presets))},e.applyPreset=y=>{Object.assign(e.query,y.query,{page:1})},e.deletePreset=y=>{e.presets=(e.presets||[]).filter(w=>w.name!==y.name),localStorage.setItem(k,JS
Line truncated
|
||||
`).map((ue,pe)=>pe===0?ue:se+ue).join(`
|
||||
`)}else typeof _=="number"||typeof _=="boolean"?J=String(_):J=JSON.stringify(_);ae.push(`${Z}${J}${te}`)}),ae.push("}"),ae.join(`
|
||||
`)}function v(R){let T=[],I="",H=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,q=0,U;for(;U=H.exec(R);)I+=R.slice(q,U.index),q=H.lastIndex,T.push({key:U[1],op:U[2]||"=",val:U[3]});return I+=R.slice(q),{filters:T,free:I.trim().toLowerCase()}}function h(R,T){for(let I of T.filters){let H=(U,ae,ie)=>{let _=parseFloat(U),O=parseFloat(ae);return ie==="="?String(U)===String(ae):ie==="!="?String(U)!==String(ae):ie===">="?_>=O:ie==="<="?_<=O:ie===">"?_>O:ie==="<"?_<O:!0},q;if(I.key==="code")q=R.displayMessage;else if(I.key==="module")q=R.module;else if(I.key==="status")q=R._status;else if(I.key==="url")q=R._url;else if(I.key==="repo")q=R._repoId;else if(I.key==="level")q=R.level;else continue;if(q==null||!H(q,I.val,I.op))return!1}return!(T.free&&((R.displayMessage||"")+" "+(R.module||"")+" "+(R._url||"")+" "+JSON.stringify(R.raw||[])).toLowerCase().indexOf(T.free)===-1)}function k(){let R=v(e.query.search||"");e.parsedFilterCount=R.filters.length;let T=e.query.bucket,I=e.entries.filter(q=>T&&q._bucket!==T?!1:h(q,R)),H=e.query.group;if(H){let q=ie=>H==="module"?ie.module:ie.displayMessage||ie.message||"_",U=new Map;for(let ie of I){let _=q(ie);if(U.has(_)){let O=U.get(_);O.count++,O._related.push(ie),new Date(ie.ts)>new Date(O.ts)&&(O.ts=ie.ts,O._url=ie._url,O._status=ie._status),new Date(ie.ts)<new Date(O._firstSeen)&&(O._firstSeen=ie.ts)}else{let O=Object.assign({},ie);O._key=`${H}:${_}`,O._related=[ie],O._firstSeen=ie.ts,O._lastHourCount=0,O.count=1,U.set(_,O)}}let ae=Date.now()-3600*1e3;for(let ie of U.values())ie._lastHourCount=ie._related.filter(_=>new Date(_.ts).getTime()>=ae).length;I=Array.from(U.values())}else I=I.map((q,U)=>(q._key="row:"+U+":"+q.ts,q._related=[q],q._firstSeen=q.ts,q._lastHourCount=0,q.count=1,q));e.query.sort==="count"?I.sort((q,U)=>U.count-q.count||new Date(U.ts)-new Date(q.ts)):I.sort((q,U)=>new Date(U.ts)-new Date(q.ts)),e.visible=I}function m(R){let T=R?e.entries.length:0,I=R?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:T,limit:I}}).then(H=>{let q=(H.data.entries||[]).map(u);e.entries=R?e.entries.concat(q):q,e.available=!!H.data.available,e.cap=H.data.max||e.cap,e.total=H.data.total||e.entries.length,k()},H=>console.error(H))}e.loadMore=()=>m(!0),e.canLoadMore=()=>e.entries.length<e.total;function E(){t.get("/api/admin/errors/stats").then(R=>{let T=R.data||{},I=T.prev24h?Math.round((T.last24h-T.prev24h)/T.prev24h*100):0;e.stats={last24h:T.last24h||0,prev24h:T.prev24h||0,delta:I,severity:T.severity||{error:0,warn:0,info:0},unique:T.unique||{error:0,warn:0,info:0},buckets:T.buckets||[],dropped:T.dropped||0}},R=>console.error(R))}function y(){m(),E()}e.barPx=(R,T)=>{let I=e.stats.buckets||[],H=0;for(let ie of I)H=Math.max(H,(ie.error||0)+(ie.warn||0)+(ie.info||0));if(!H)return 0;let q=(R.error||0)+(R.warn||0)+(R.info||0);if(!q)return 0;let U=Math.round(q/H*60),ae=R[T]||0;return Math.round(ae/q*U)},e.bucketTitle=R=>`${new Date(R.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${R.error||0} err \xB7 ${R.warn||0} warn \xB7 ${R.info||0} info`,e.toggle=R=>{e.expanded[R._key]=!e.expanded[R._key]},e.setBucket=R=>{e.query.bucket=R},e.refreshNow=y,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(y,R=>console.error(R))},e.exportCsv=()=>{let R=["ts","level","module","displayMessage","_status","_url","_repoId"],T=[R.join(",")];for(let U of e.visible)T.push(R.map(ae=>{let ie=U[ae]==null?"":String(U[ae]);return/[",\n]/.test(ie)?`"${ie.replace(/"/g,'""')}"`:ie}).join(","));let I=new Blob([T.join(`
|
||||
`)],{type:"text/csv;charset=utf-8"}),H=URL.createObjectURL(I),q=document.createElement("a");q.href=H,q.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(q),q.click(),document.body.removeChild(q),URL.revokeObjectURL(H)};function w(R){e.copyHint=`${R} copied`,n.timeout(()=>{e.copyHint=""},1500)}e.copyJson=R=>{navigator.clipboard.writeText(R._detailJson).then(()=>w("JSON"))},e.copyCurl=R=>{if(!R._url)return;let I=`curl -X ${R._method||"GET"} '${window.location.origin}${R._url}'`;navigator.clipboard.writeText(I).then(()=>w("curl"))},y();let D=r(()=>{e.query.autoRefresh&&y()},15e3);e.on("dispose",()=>r.cancel(D)),e.watch("query.search",k),e.watch("query.bucket",k),e.watch("query.sort",k),e.watch("query.group",k)},ul=function(e,t,s,r){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&s.url("/")}),e.user==null){s.url("/");return}e.data=null,e.loading=!0,e.error=null,e.performance=null,e.performanceMinutes=15,e.performanceTab="routes",e.performanceError=null,e.formatMs=function(y){return y==null?"":y<1e3?y+" ms":+(y/1e3).toFixed(1)+" s"},e.latencyBound=function(y,w){return y.count?y[w]==null?"> 60 s":"\u2264 "+e.formatMs(y[w]):"No samples"},e.firstByteBound=function(y){return e.latencyBound({count:y.firstByteCount,p95UpperMs:y.firstByteP95UpperMs},"p95UpperMs")},e.sampleAge=function(y){return Math.max(0,Math.round((Date.now()-y.sampledAt)/1e3))},e.peakRss=function(y){return(e.performance.runtimeSeries||[]).filter(function(w){return w.instance===y.instance}).reduce(function(w,D){return Math.max(w,D.rss)},y.memory.rss)},e.performanceWarning=function(y){return e.sampleAge(y)>45||y.droppedBatches||y.droppedMetrics||y.sockets?.closeWait>0||y.eventLoop.p95Ms>100||y.memoryLimitBytes&&y.memory.rss>y.memoryLimitBytes*.8};var n=!1;e.loadPerformance=function(){var y=e.performanceMinutes;e.performance?.windowMinutes&&e.performance.windowMinutes!==y&&(e.performance=null),!n&&(n=!0,t.get("/api/admin/performance",{params:{minutes:y}}).then(function(w){y===e.performanceMinutes&&(e.performance=w.data,e.performanceError=w.data.available?null:"Performance monitoring is unavailable. Request history may be incomplete.")},function(){y===e.performanceMinutes&&(e.performanceError="Could not refresh performance monitoring. Displayed samples may be stale.")}).finally(function(){n=!1,y!==e.performanceMinutes&&e.loadPerformance()}))};function i(y){if(y==null)return"\u2014";for(var w=["B","KB","MB","GB","TB"],D=0,R=y;R>=1024&&D<w.length-1;)R/=1024,D++;return R.toFixed(D>0?1:0)+" "+w[D]}e.humanBytes=i;function a(y){if(!y)return"\u2014";var w=Math.floor(y/86400),D=Math.floor(y%86400/3600),R=Math.floor(y%3600/60);return w>0?w+"d "+(D<10?"0":"")+D+"h":D>0?D+"h "+(R<10?"0":"")+R+"m":R+"m"}e.humanDuration=a;function u(y){return y==null?"\u2014":y>=1e6?(y/1e6).toFixed(1)+"M":y>=1e3?(y/1e3).toFixed(1)+"K":String(y)}e.humanNum=u,e.queueTotal=function(y){return y?(y.waiting||0)+(y.active||0)+(y.delayed||0)+(y.failed||0):0},e.statusCount=function(y){if(!e.data||!e.data.repos)return 0;for(var w=e.data.repos.statusBreakdown||[],D=0;D<w.length;D++)if(w[D]._id===y)return w[D].count;return 0},e.barPct=function(y){var w=e.data&&e.data.repos?e.data.repos.total:0;if(!w)return 0;var D=[y];y==="expired"&&D.push("expiring"),y==="removed"&&D.push("removing"),y==="preparing"&&D.push("download");var R=0;return D.forEach(function(T){R+=e.statusCount(T)}),Math.max(.4,R/w*100)},e.errPct=function(y){if(!e.data||!e.data.errors)return 0;var w=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.errors.severity.info,1);return e.data.errors.severity[y]/w*100};function f(y){var w=y||[];return w.map(function(D,R){var T=w[R-1]||{},I=Object.assign({},D);return I.dailyRepositories=R?Math.max(0,(D.nbRepositories||0)-(T.nbRepositories||0)):0,I.dailyUsers=R?Math.max(0,(D.nbUsers||0)-(T.nbUsers||0)):0,I.dailyPageViews=R?Math.max(0,(D.nbPageViews||0)-(T.nbPageViews||0)):0,I})}function v(y){var w=y&&y.length?y[y.length-1]:{};return{repositories:w.dailyRepositories||0,users:w.dailyUsers||0,pageViews:w.dailyPageViews||0}}var h={},k={};e.historyBarH=function(y,w){return!y||!h[w]?0:Math.max(1,Math.round(y[w]/h[w]*140))},e.historyRangeH=function(y,w){if(!y||h[w]==null)return 0;var D=h[w]-k[w];return D?Math.round(8+(y[w]-k[w])/D*132):70},e.historyRange=function(y){return{min:k[y],max:h[y]}},e.historyLabel=function(y){if(!y||!y.date)return"";var w=new Date(y.date);return w.getUTCMonth()+1+"/"+w.getUTCDate()};function m(){e.loadPerformance(),t.get("/api/admin/overview").then(function(y){y.data.history=f(y.data.history),y.data.daily={today:v(y.data.history)},e.data=y.data,e.loading=!1,e.error=null,h={},k={},(y.data.history||[]).forEach(function(w){["dailyPageViews","dailyRepositories","dailyUsers","nbUsers"].forEach(function(D){(!h[D]||w[D]>h[D])&&(h[D]=w[D]),(k[D]==null||w[D]<k[D])&&(k[D]=w[D])})})},function(y){e.loading=!1,e.error=y.data&&y.data.error||"Failed to load overview"})}m();var E=r(m,3e4);e.on("dispose",function(){r.can
Line truncated
|
||||
anonymous_github`)])])])])]),p(" Limitations "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#ui-icon-warning"})]),b("Limitations ")]),o("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[o("h3",{class:"panel-title"},[o("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? ")])]),o("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[o("div",{class:"panel-body p-3"},[o("ul",null,[o("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),o("li",null," Files larger than 8 MB are not supported. "),o("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),o("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),p(" Privacy & Security "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#ui-icon-shield"})]),b("Privacy & Security ")]),o("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[o("h3",{class:"panel-title"},[o("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? ")])]),o("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[o("div",{class:"panel-body p-3"},[o("p",null," Data stored on Anonymous GitHub is never shared or used for any purpose beyond providing the anonymization service. When a repository is removed or expires, only its configuration is retained \u2014 this makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[o("h3",{class:"panel-title"},[o("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? ")])]),o("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[o("div",{class:"panel-body p-3"},[o("p",null,[b("The GitHub App has read-only access to the repositories you choose. You can change repository access on your "),o("a",{href:"/connections"},"GitHub connections"),b(" page.")]),o("p",null,"Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection."),o("p",null,"Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.")])])]),o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[o("h3",{class:"panel-title"},[o("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? ")])]),o("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[o("div",{class:"panel-body p-3"},[o("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),p(" Self-Hosting "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#project-icon-storage"})]),b("Self-Hosting ")]),o("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[o("h3",{class:"panel-title"},[o("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? ")])]),o("div",{id:"deploy",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingDeploy"},[o("div",{class:"panel-body p-3"},[o("p",null,[b(" Yes. Anonymous Git
Line truncated
|
||||
anonymized repository often think it is about the paper, so the
|
||||
label names the service and the tooltip says who to contact. `),t[12]||(t[12]=s("li",{class:"nav-item d-none d-lg-block"},[s("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"},[s("i",{class:"fas fa-bug d-lg-none mr-1","aria-hidden":"true"}),y("Report a bug ")])],-1)),e.isDarkMode?f("v-if",!0):(l(),d("li",XO,[s("a",{class:"nav-link nav-icon",href:"#",title:"Switch to dark mode","aria-label":"Switch to dark mode",role:"button",onClick:t[0]||(t[0]=we(o=>{e.darkMode(!0)},["prevent"]))},[...t[7]||(t[7]=[s("i",{class:"fas fa-moon","aria-hidden":"true"},null,-1)])])])),e.isDarkMode?(l(),d("li",ZO,[s("a",{class:"nav-link nav-icon",href:"#",title:"Switch to light mode","aria-label":"Switch to light mode",role:"button",onClick:t[1]||(t[1]=we(o=>{e.darkMode(!1)},["prevent"]))},[...t[8]||(t[8]=[s("i",{class:"fas fa-sun","aria-hidden":"true"},null,-1)])])])):f("v-if",!0),e.user?f("v-if",!0):(l(),d("li",xO,[...t[9]||(t[9]=[s("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[s("i",{class:"fab fa-github mr-1"}),y(" Sign in ")],-1)])])),e.user?(l(),d("li",eA,[s("a",tA,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,sA)):f("v-if",!0),s("span",nA,c(e.user?.username),1)]),s("div",oA,[s("h6",rA,"Signed in as @"+c(e.user?.username),1),t[10]||(t[10]=ge('<a class="dropdown-item" href="/profile"><i class="fas fa-sliders-h" aria-hidden="true"></i><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><i class="fas fa-link" aria-hidden="true"></i><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><i class="fas fa-bug" aria-hidden="true"></i><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="/api/user/logout" target="_self"><i class="fas fa-sign-out-alt" aria-hidden="true"></i><span>Sign out</span></a>',8))])])):f("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,iA)):f("v-if",!0)],64)}var aA={class:"paper-landing"},lA={class:"paper-hero"},dA={class:"paper-hero-copy"},uA={class:"paper-cta-row"},cA={key:0,href:"/signin",target:"_self",class:"btn-hero"},pA={key:1,href:"/anonymize",class:"btn-hero"},fA={class:"paper-stats paper-wrap",id:"metrics","aria-label":"Usage over the last 60 days"},mA={class:"paper-stats-grid"},hA={class:"paper-stat-value"},vA={class:"paper-stat-label"},yA=["viewBox"],gA=["x","y","width","height"],bA={key:1,class:"paper-stat-delta"},wA={class:"paper-features paper-wrap","aria-label":"Product features"},kA={class:"paper-features-list",role:"tablist","aria-label":"Features","aria-orientation":"vertical"},_A=["onClick","onKeydown","id","aria-controls","aria-selected","tabindex"],EA={class:"paper-eyebrow"},CA={class:"paper-feature-title"},NA={class:"paper-feature-more"},SA={class:"paper-feature-title paper-feature-title-m","aria-hidden":"true"},DA=["href","target"],RA={class:"paper-features-stage"},TA=["id","aria-labelledby"],OA={class:"paper-frame"},AA={class:"paper-frame-bar","aria-hidden":"true"},IA={class:"url"},VA=["src","alt"],PA={class:"paper-footer"},qA={class:"paper-footer-inner"},MA={class:"paper-footer-col"},$A={key:0,href:"/dashboard"},LA={key:1,href:"/conferences"},UA={class:"paper-footer-foot"};function pl(e,t){return l(),d("div",aA,[f(" Hero: copy on the left, the product on the right "),s("section",lA,[s("div",dA,[t[2]||(t[2]=s("div",{class:"paper-eyebrow"},"For double-blind peer review",-1)),t[3]||(t[3]=s("h1",{class:"hero-title"},[y(" Share the code,"),s("br"),s("span",{class:"accent"},"not the author.")],-1)),t[4]||(t[4]=s("p",{class:"hero-subtitle"}," Anonymous GitHub makes a read-only mirror of your repository, pull request, or gist with every trace of identity removed, so you can attach a link to your submission without breaking anonymity. ",-1)),s("div",uA,[e.user?f("v-if",!0):(l(),d("a",cA,[...t[0]||(t[0]=[s("i",{class:"fab fa-github mr-2","aria-hidden":"true"},null,-1),y("Sign in with GitHub ",-1)])])),e.user?(l(),d("a",pA," Anonymize a repository \
Line truncated
|
||||
label names the service and the tooltip says who to contact. `),t[12]||(t[12]=o("li",{class:"nav-item d-none d-lg-block"},[o("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"},[o("svg",{class:"ui-icon d-lg-none mr-1","aria-hidden":"true"},[o("use",{href:"#ui-icon-bug"})]),b("Report a bug ")])],-1)),e.isDarkMode?p("v-if",!0):(l(),d("li",s3,[o("a",{class:"nav-link nav-icon",href:"#",title:"Switch to dark mode","aria-label":"Switch to dark mode",role:"button",onClick:t[0]||(t[0]=me(s=>{e.darkMode(!0)},["prevent"]))},[...t[8]||(t[8]=[o("svg",{class:"ui-icon","aria-hidden":"true"},[o("use",{href:"#ui-icon-moon"})],-1)])])])),e.isDarkMode?(l(),d("li",n3,[o("a",{class:"nav-link nav-icon",href:"#",title:"Switch to light mode","aria-label":"Switch to light mode",role:"button",onClick:t[1]||(t[1]=me(s=>{e.darkMode(!1)},["prevent"]))},[...t[9]||(t[9]=[o("svg",{class:"ui-icon","aria-hidden":"true"},[o("use",{href:"#ui-icon-sun"})],-1)])])])):p("v-if",!0),!e.user&&!e.isReviewerRoute?(l(),d("li",r3,[...t[10]||(t[10]=[o("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[o("i",{class:"fab fa-github mr-1"}),b(" Sign in ")],-1)])])):p("v-if",!0),e.user?(l(),d("li",i3,[o("a",a3,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,l3)):p("v-if",!0),o("span",d3,c(e.user?.username),1)]),o("div",u3,[o("h6",c3,"Signed in as @"+c(e.user?.username),1),t[11]||(t[11]=ce('<a class="dropdown-item" href="/profile"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a><a class="dropdown-item" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg><span>Support the project</span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="/api/user/logout" target="_self"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-logout"></use></svg><span>Sign out</span></a>',9))])])):p("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,p3)):p("v-if",!0)],64)}var f3={class:"paper-landing"},m3={class:"paper-hero"},h3={class:"paper-hero-copy"},v3={class:"paper-cta-row"},g3={key:0,href:"/signin",target:"_self",class:"btn-hero"},y3={key:1,href:"/anonymize",class:"btn-hero"},b3={class:"paper-stats paper-wrap",id:"metrics","aria-label":"Usage over the last 60 days"},w3={class:"paper-stats-grid"},k3={class:"paper-stat-value"},E3={class:"paper-stat-label"},C3=["viewBox"],_3=["x","y","width","height"],S3={key:1,class:"paper-stat-delta"},N3={class:"paper-features paper-wrap","aria-label":"Product features"},D3={class:"paper-features-list",role:"tablist","aria-label":"Features","aria-orientation":"vertical"},R3=["onClick","onKeydown","id","aria-controls","aria-selected","tabindex"],T3={class:"paper-eyebrow"},A3={class:"paper-feature-title"},O3={class:"paper-feature-more"},I3={class:"paper-feature-title paper-feature-title-m","aria-hidden":"true"},P3=["href","target"],V3={class:"paper-features-stage"},q3=["id","aria-labelledby"],M3={class:"paper-frame"},$3={class:"paper-frame-bar","aria-hidden":"true"},L3={class:"url"},U3=["src","alt"],F3={class:"paper-footer"},z3={class:"paper-footer-inner"},H3={class:"paper-footer-col"},B3={key:0,href:"/dashboard"},j3={key:1,href:"/conferences"},G3={class:"paper-footer-foot"};function Il(e,t){return l(),d("div",f3,[p(" Hero: copy on the left, the product on the right "),o("section",m3,[o("div",h3,[t[2]||(t[2]=o("div",{class:"paper-eyebrow"},"For double-blind peer review",-1)),t[3]||(t[3]=o("h1",{class:"hero-title"},[b(" Share the code,"),o("br"),o("span",{class:"accent"},"not the author.")],-1)),t[4]||(t[4]=o("p
Line truncated
|
||||
MIT CSAIL
|
||||
jane.smith@example.org`},UP={id:"termsHelp",class:"form-text text-muted"},FP={class:"invalid-feedback"},zP={id:"settings-display",class:"paper-settings-section"},HP={class:"form-group paper-check-list"},BP={class:"form-check"},GP={class:"form-check-input",type:"checkbox",id:"link",name:"link"},jP={class:"form-check"},WP={class:"form-check-input",type:"checkbox",id:"image",name:"image"},KP={class:"form-check"},YP={class:"form-check-input",type:"checkbox",id:"pdf",name:"pdf"},JP={class:"form-check"},QP={class:"form-check-input",type:"checkbox",id:"notebook",name:"notebook"},XP={id:"settings-features",class:"paper-settings-section"},ZP={class:"form-group paper-check-list"},xP={class:"form-check"},e3={class:"form-check-input",type:"checkbox",id:"page",name:"page"},t3={class:"form-check"},s3={class:"form-check-input",type:"checkbox",id:"loc",name:"loc"},n3={class:"form-check"},o3={class:"form-check-input",type:"checkbox",id:"update",name:"update"},r3={class:"form-group"},i3={class:"form-control",id:"mode",name:"mode"},a3={class:"form-text text-muted"},l3={id:"settings-expiration",class:"paper-settings-section"},d3={class:"form-group"},u3={class:"form-control",id:"expiration",name:"expiration"},c3={class:"paper-settings-footer"},p3={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},f3=["textContent"],m3={class:"paper-settings-actions"},h3=["disabled"],v3={key:0,class:"fas fa-check mr-1","aria-hidden":"true"},y3={id:"settings-account",class:"paper-settings-section"},g3={class:"paper-account-card"},b3=["src"],w3={class:"paper-account-identity"},k3={class:"paper-account-name"},_3={class:"paper-danger-zone"},E3={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},C3=["textContent"],N3=["disabled"];function yl(e,t){let o=be("paper-scrollspy"),r=be("field"),n=be("form");return l(),d("div",gP,[t[48]||(t[48]=ge('<div class="paper-crumbs"><a href="/dashboard">My work</a> \xA0/\xA0 <span class="here">Settings</span></div><h1 class="paper-page-title">Your <em>settings</em></h1><p class="paper-page-lede">Defaults applied to every new anonymization, your quotas, and your account.</p>',3)),s("div",bP,[E((l(),d("aside",wP,[...t[2]||(t[2]=[ge('<div class="paper-settings-toc-head">On this page</div><nav><a href="#settings-quota">Quota</a><a href="#settings-terms">Terms to redact</a><a href="#settings-display">Display</a><a href="#settings-features">Features</a><a href="#settings-expiration">Expiration</a><a href="#settings-account">Account</a></nav>',2)])])),[[o]]),s("div",kP,[f(" Quota: same markup as the dashboard, driven by quotaService "),s("section",_P,[t[4]||(t[4]=s("div",{class:"paper-section-eyebrow"},"Quota",-1)),t[5]||(t[5]=s("p",{class:"paper-section-copy"},"What you are using across all your anonymizations. Conferences can lift these limits during a review window.",-1)),e.quota?(l(),d("div",EP,[(l(),d(J,null,ne([{key:"repository",label:"Repositories",kind:"count"},{key:"storage",label:"Storage",kind:"bytes"},{key:"file",label:"Files",kind:"count"}],(i,a)=>s("div",CP,[s("div",NP,[s("span",SP,c(i?.label),1),i?.kind==="count"?(l(),d("span",DP,[y(c(e.fmt?.number(e.quota[i?.key].used)),1),e.quota[i?.key].unlimited?f("v-if",!0):(l(),d("span",RP," / "+c(e.fmt?.number(e.quota[i?.key].total)),1)),e.quota[i?.key].unlimited?(l(),d("span",TP,"Unlimited")):f("v-if",!0)])):f("v-if",!0),i?.kind==="bytes"?(l(),d("span",OP,[y(c(e.fmt?.humanFileSize(e.quota[i?.key].used)),1),e.quota[i?.key].unlimited?f("v-if",!0):(l(),d("span",AP," / "+c(e.fmt?.humanFileSize(e.quota[i?.key].total)),1)),e.quota[i?.key].unlimited?(l(),d("span",IP,"Unlimited")):f("v-if",!0)])):f("v-if",!0)]),s("div",{class:O(["quota-track","quota-"+e.quota[i?.key].level]),role:"progressbar","aria-valuemin":"0","aria-label":i?.label+" quota","aria-valuenow":e.quota[i?.key].used,"aria-valuemax":e.quota[i?.key].unlimited?e.quota[i?.key].used:e.quota[i?.key].total,"aria-valuetext":e.quota[i?.key].unlimited?"unlimited":e.fmt.number(e.quota[i?.key].percent,0)+"% used"},[e.quota[i?.key].unlimited?f("v-if",!0):(l(),d("div",{key:0,class:"quota-fill",style:Pe({width:e.quota[i?.key].percent+"%"})},null,4))],10,VP)])),64))])):f("v-if",!0),e.quota?f("v-if",!0):(l(),d("div",PP,[(l(),d(J,null,ne([1,2,3],(i,a)=>s("div",qP,[...t[3]||(t[3]=[s("div",{class:"quota-header"},[s("span",{class:"skeleton skeleton-line",style:{width:"40%"}}),s("span",{class:"skeleton skeleton-line",style:{width:"25%"}})],-1),s("div",{class:"quota-track"},null,-1)])])),64))]))]),E((l(),d("form",{class:"form needs-validation",name:"defaultsForm",novalidate:"",onSubmit:t[0]||(t[0]=we(i=>e.submitForm(i,()=>{e.saveDefault(i)}),["prevent"]))},[s("section",MP,[t[11]||(t[11]=s("div",{class:"paper-section-eyebrow"},"Terms to redact",-1)),t[12]||(t[12]=s("p",{class:"paper-section-copy"},"Pre-filled in every new anonymization. You can still edit the list per anonymization.",-1)),s("div",$P,[t[10]||(t[10]=s("label",{class:"paper-field-label",for:"terms"},"Default terms",-1)),E(s("tex
Line truncated
|
||||
`);for(let k=0;k<v.length;k++){let p=v[k];if(p.startsWith("diff --git")){h(),m();continue}if(p.startsWith("--- ")){i&&i.lines.length&&h(),m().oldPath=p.replace(/^--- (a\/)?/,"").trim();continue}if(p.startsWith("+++ ")){m().newPath=p.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(p.startsWith("index ")||p.startsWith("similarity index")||p.startsWith("rename ")||p.startsWith("new file mode")||p.startsWith("deleted file mode")||p.startsWith("Binary files"))){if(p.startsWith("@@")){let g=p.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);g&&(a=parseInt(g[1],10),u=parseInt(g[2],10)),m().lines.push({kind:"hunk",oldNo:"",newNo:"",text:p});continue}i&&(p.startsWith("+")?(i.lines.push({kind:"add",oldNo:"",newNo:u,text:p.slice(1)}),u++):p.startsWith("-")?(i.lines.push({kind:"remove",oldNo:a,newNo:"",text:p.slice(1)}),a++):(i.lines.push({kind:"ctx",oldNo:a,newNo:u,text:p.startsWith(" ")?p.slice(1):p}),a++,u++))}}return o(n,i),n.join("")}})({trustAsHtml:e=>e});function Mq(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function $q(e){return String(e??"").toUpperCase()}function Lq(e,t,o=0){return(e||[]).slice(o,o+t)}function Uq(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function Fq(e,t){if(!t)return e||[];let o=Array.isArray(t)?t:[t];return[...e||[]].sort((r,n)=>{for(let i of o){let a=i[0]==="-"?-1:1;i=i.replace(/^[+-]/,"");let u=i.split(".").reduce((h,v)=>h?.[v],r),m=i.split(".").reduce((h,v)=>h?.[v],n);if(typeof u=="string"&&(u=u.toLowerCase()),typeof m=="string"&&(m=m.toLowerCase()),u!==m)return(u==null?-1:m==null?1:u<m?-1:1)*a}return 0})}function zq(e,t){if(!e)return"";let o=new Date(e);if(isNaN(o))return String(e);if(!t||t==="mediumDate")return o.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let r={yyyy:o.getFullYear(),MMM:o.toLocaleDateString(void 0,{month:"short"}),MM:String(o.getMonth()+1).padStart(2,"0"),dd:String(o.getDate()).padStart(2,"0"),d:o.getDate(),HH:String(o.getHours()).padStart(2,"0"),mm:String(o.getMinutes()).padStart(2,"0"),ss:String(o.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,n=>r[n])}function Hq(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}var Cl={ERRORS:{repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"Unknown error, contact the admin.",unreachable:"Anonymous GitHub is unreachable, contact the admin.",request_error:"Unable to download the file, check your connection or contact the admin.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"The repository was not found on GitHub. Check the URL and spelling, make sure you are signed in to the account that can see it, and confirm the repo isn't hidden under an org that restricts third-party app access.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name is invalid; expected the form 'owner/name'.",repoId_not_defined:"A repository ID must be provided.",repoUrl_not_defined:"The repository URL needs to be defined.",source_not_provided:"A repository source must be provided.",github_rate_limit_exceeded:"GitHub
Line truncated
|
||||
jane.smith@example.org`},M5={id:"termsHelp",class:"form-text text-muted"},$5={class:"invalid-feedback"},L5={id:"settings-display",class:"paper-settings-section"},U5={class:"form-group paper-check-list"},F5={class:"form-check"},z5={class:"form-check-input",type:"checkbox",id:"link",name:"link"},H5={class:"form-check"},B5={class:"form-check-input",type:"checkbox",id:"image",name:"image"},j5={class:"form-check"},G5={class:"form-check-input",type:"checkbox",id:"pdf",name:"pdf"},W5={class:"form-check"},K5={class:"form-check-input",type:"checkbox",id:"notebook",name:"notebook"},Y5={id:"settings-features",class:"paper-settings-section"},J5={class:"form-group paper-check-list"},Q5={class:"form-check"},X5={class:"form-check-input",type:"checkbox",id:"page",name:"page"},Z5={class:"form-check"},x5={class:"form-check-input",type:"checkbox",id:"loc",name:"loc"},eq={class:"form-check"},tq={class:"form-check-input",type:"checkbox",id:"update",name:"update"},oq={class:"form-group"},sq={class:"form-control",id:"mode",name:"mode"},nq={class:"form-text text-muted"},rq={id:"settings-expiration",class:"paper-settings-section"},iq={class:"form-group"},aq={class:"form-control",id:"expiration",name:"expiration"},lq={key:0,class:"draft-status",role:"status"},dq={key:1,class:"draft-status"},uq={class:"paper-settings-footer"},cq={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},pq=["textContent"],fq={class:"paper-settings-actions"},mq=["disabled"],hq={key:0,class:"ui-icon mr-1","aria-hidden":"true"},vq={id:"settings-account",class:"paper-settings-section"},gq={class:"paper-account-card"},yq=["src"],bq={class:"paper-account-identity"},wq={class:"paper-account-name"},kq={class:"paper-danger-zone"},Eq={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},Cq=["textContent"],_q=["disabled"];function $l(e,t){let s=We("quota-usage"),r=be("paper-scrollspy"),n=be("field"),i=be("form");return l(),d("div",N5,[t[52]||(t[52]=ce('<div class="paper-crumbs"><a href="/dashboard">My work</a> \xA0/\xA0 <span class="here">Settings</span></div><h1 class="paper-page-title">Your <em>settings</em></h1><p class="paper-page-lede">Defaults applied to every new anonymization, your quotas, and your account.</p>',3)),o("div",D5,[N((l(),d("aside",R5,[...t[4]||(t[4]=[ce('<div class="paper-settings-toc-head">On this page</div><nav><a href="#settings-quota">Quota</a><a href="#settings-terms">Terms to redact</a><a href="#settings-display">Display</a><a href="#settings-features">Features</a><a href="#settings-expiration">Expiration</a><a href="#settings-account">Account</a></nav>',2)])])),[[r]]),o("div",T5,[p(" Account usage shares the dashboard component. "),o("section",A5,[t[6]||(t[6]=o("div",{class:"paper-section-eyebrow"},"Quota",-1)),t[7]||(t[7]=o("p",{class:"paper-section-copy"},"What you are using across all your anonymizations. Conferences can lift these limits during a review window.",-1)),e.quota?(l(),Mt(s,{key:0,quota:e.quota,fmt:e.fmt},null,8,["quota","fmt"])):p("v-if",!0),e.quota?p("v-if",!0):(l(),d("div",O5,[(l(),d(X,null,re([1,2,3],(a,u)=>o("div",I5,[...t[5]||(t[5]=[o("div",{class:"quota-header"},[o("span",{class:"skeleton skeleton-line",style:{width:"40%"}}),o("span",{class:"skeleton skeleton-line",style:{width:"25%"}})],-1),o("div",{class:"quota-track"},null,-1)])])),64))]))]),N((l(),d("form",{class:"form needs-validation",name:"defaultsForm",novalidate:"",onSubmit:t[2]||(t[2]=me(a=>e.submitForm(a,()=>{e.saveDefault(a)}),["prevent"]))},[o("section",P5,[t[13]||(t[13]=o("div",{class:"paper-section-eyebrow"},"Terms to redact",-1)),t[14]||(t[14]=o("p",{class:"paper-section-copy"},"Pre-filled in every new anonymization. You can still edit the list per anonymization.",-1)),o("div",V5,[t[12]||(t[12]=o("label",{class:"paper-field-label",for:"terms"},"Default terms",-1)),N(o("textarea",q5,null,512),[[n,{state:e.viewState,set:a=>{e.terms=a},value:e.terms,form:"defaultsForm",options:{debounce:250}}]]),o("small",M5,[t[8]||(t[8]=b(" One term per line (regex allowed). Each match is replaced by ",-1)),o("code",null,c(e.site_options?.ANONYMIZATION_MASK||"XXX")+"-[N]",1),t[9]||(t[9]=b(", or use ",-1)),t[10]||(t[10]=o("code",null,"term=>replacement",-1)),t[11]||(t[11]=b(" to pick your own. ",-1))]),N(o("div",$5," Terms are in an invalid format ",512),[[W,e.defaultsForm?.terms?.errors?.format]])])]),o("section",L5,[t[23]||(t[23]=o("div",{class:"paper-section-eyebrow"},"Display",-1)),t[24]||(t[24]=o("p",{class:"paper-section-copy"},"What reviewers can see inside an anonymized repository.",-1)),o("div",U5,[o("div",F5,[N(o("input",z5,null,512),[[n,{state:e.viewState,set:a=>{e.options.link=a},value:e.options?.link,form:"defaultsForm",options:{}}]]),t[15]||(t[15]=o("label",{class:"form-check-label",for:"link"},"Keep links",-1)),t[16]||(t[16]=o("small",{class:"form-text text-muted"},"Unchecked, every link in text files is removed.",-1))]),o("div",H5,[N(o("input",B5,null,512),[[n,{state:e.viewState,set:a=>{e.options.image=a},value:e.op
Line truncated
|
||||
`);for(let k=0;k<h.length;k++){let m=h[k];if(m.startsWith("diff --git")){v(),f();continue}if(m.startsWith("--- ")){i&&i.lines.length&&v(),f().oldPath=m.replace(/^--- (a\/)?/,"").trim();continue}if(m.startsWith("+++ ")){f().newPath=m.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(m.startsWith("index ")||m.startsWith("similarity index")||m.startsWith("rename ")||m.startsWith("new file mode")||m.startsWith("deleted file mode")||m.startsWith("Binary files"))){if(m.startsWith("@@")){let E=m.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);E&&(a=parseInt(E[1],10),u=parseInt(E[2],10)),f().lines.push({kind:"hunk",oldNo:"",newNo:"",text:m});continue}i&&(m.startsWith("+")?(i.lines.push({kind:"add",oldNo:"",newNo:u,text:m.slice(1)}),u++):m.startsWith("-")?(i.lines.push({kind:"remove",oldNo:a,newNo:"",text:m.slice(1)}),a++):(i.lines.push({kind:"ctx",oldNo:a,newNo:u,text:m.startsWith(" ")?m.slice(1):m}),a++,u++))}}return s(n,i),n.join("")}})({trustAsHtml:e=>e});function zM(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function HM(e){return String(e??"").toUpperCase()}function BM(e,t,s=0){return(e||[]).slice(s,s+t)}function jM(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function GM(e,t){if(!t)return e||[];let s=Array.isArray(t)?t:[t];return[...e||[]].sort((r,n)=>{for(let i of s){let a=i[0]==="-"?-1:1;i=i.replace(/^[+-]/,"");let u=i.split(".").reduce((v,h)=>v?.[h],r),f=i.split(".").reduce((v,h)=>v?.[h],n);if(typeof u=="string"&&(u=u.toLowerCase()),typeof f=="string"&&(f=f.toLowerCase()),u!==f)return(u==null?-1:f==null?1:u<f?-1:1)*a}return 0})}function WM(e,t){if(!e)return"";let s=new Date(e);if(isNaN(s))return String(e);if(!t||t==="mediumDate")return s.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let r={yyyy:s.getFullYear(),MMM:s.toLocaleDateString(void 0,{month:"short"}),MM:String(s.getMonth()+1).padStart(2,"0"),dd:String(s.getDate()).padStart(2,"0"),d:s.getDate(),HH:String(s.getHours()).padStart(2,"0"),mm:String(s.getMinutes()).padStart(2,"0"),ss:String(s.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,n=>r[n])}function KM(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}var ar={ERRORS:{cache_busy:"Downloads are busy. Please try again shortly.",invalid_sort:"The selected sort order is invalid.",invalid_cursor:"This project page is no longer valid. Reload the list.",repository_changed:"The repository changed while processing. Please retry.",repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"Unknown error, contact the admin.",unreachable:"Anonymous GitHub is unreachable, contact the admin.",request_error:"Unable to download the file, check your connection or contact the admin.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"The repository was not found on GitHub. Check the URL and spelling, make sure you are signed in to the account that can see it, and confirm the repo isn't hidden under an org that restricts third-party app access.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name
Line truncated
|
||||
/*! Bundled license information:
|
||||
|
||||
@vue/shared/dist/shared.esm-bundler.js:
|
||||
|
||||