New download page, better JSON/module docs

This commit is contained in:
Matthew Holt 2021-05-25 17:26:35 -06:00
parent 09c3a73299
commit 5c3a67692e
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
8 changed files with 277 additions and 217 deletions

View file

@ -51,12 +51,18 @@
<option value="windows-amd64">Windows amd64</option> <option value="windows-amd64">Windows amd64</option>
<option value="windows-arm-6">Windows arm 6</option> <option value="windows-arm-6">Windows arm 6</option>
<option value="windows-arm-7">Windows arm 7</option> <option value="windows-arm-7">Windows arm 7</option>
<!-- <option value="windows-arm64">Windows arm64</option> TODO: Go 1.17 - https://github.com/golang/go/issues/36439 -->
</select> </select>
</div> </div>
</div> </div>
<div> <div>
<div> <div>
<b>Additional packages:</b> <span id="package-count">0</span> <b>Standard features:</b> &#9745;&#65039;
</div>
</div>
<div>
<div>
<b>Extra features:</b> <span id="package-count">0</span>
</div> </div>
</div> </div>
<div> <div>
@ -64,24 +70,17 @@
</div> </div>
</div> </div>
<div class="packages-explanation"> <input type="search" id="filter" placeholder="Filter packages and modules...">
Always comes with all standard <a href="/docs/modules">Caddy modules</a>.
<br><br> <div class="text-center">
Optionally select additional packages to include in your build: <span class="warning">⚠️ Only choose plugins you need and trust</span> <span class="warning">⚠️ Only choose plugins you need and trust</span>
</div> </div>
<div class="table-container"> <div id="optional-packages">
<table id="optional-packages"> <!-- Populated by JavaScript -->
<tr>
<th>Package</th>
<th class="text-center">Version</th>
<th>Modules</th>
</tr>
<!-- Populated by JS -->
</table>
</div> </div>
</div> </div>
{{include "/includes/footer.html"}} {{include "/includes/footer.html"}}
</body> </body>
</html> </html>

View file

@ -720,7 +720,7 @@ td code {
#module-list td:first-child { #module-list td:first-child {
word-wrap: break-word; word-wrap: break-word;
max-width: 300px; max-width: 400px;
} }
#module-list .module-link { #module-list .module-link {

View file

@ -28,27 +28,43 @@ body {
#download { #download {
margin: 0; margin: 0;
font-size: 16px; padding-left: 30px;
padding-right: 30px;
font-weight: bold; font-weight: bold;
} }
.packages-explanation { #filter {
margin: 20px auto; margin-top: 1em;
width: 100%;
padding: 15px;
font-size: 24px;
text-align: center;
border: 0;
border-bottom: 1px solid #ccc;
outline: none;
}
#filter.found {
color: green;
}
#filter.not-found {
color: #cc0000;
} }
.warning { .warning {
margin-left: 1em; margin-top: 20px;
font-size: 12px; display: inline-block;
font-size: 14px;
font-weight: bold; font-weight: bold;
padding: 2px 15px; padding: 5px 15px;
border-radius: 1em; border-radius: 1em;
border: 2px solid rgb(255, 201, 0); color: rgb(255, 208, 0);
color: rgb(206, 151, 0); background: #333;
} }
input:disabled, input:disabled,
select:disabled, select:disabled,
#optional-packages.disabled .optpkg label { #optional-packages.disabled {
cursor: not-allowed !important; cursor: not-allowed !important;
} }
@ -129,133 +145,93 @@ select:disabled,
.table-container {
overflow-x: auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
#optional-packages { #optional-packages {
width: 100%; margin-top: 2em;
border-spacing: 0;
border-collapse: collapse;
} }
/* #optional-packages tr { .package {
height: 1px;
} */
#optional-packages th,
#optional-packages td {
border-bottom: 1px solid #dfeaf0;
}
#optional-packages th:first-child { border-top-left-radius: 8px; }
#optional-packages th:last-child { border-top-right-radius: 8px; }
#optional-packages th {
background: #dfe8ec;
text-align: left;
text-transform: uppercase;
letter-spacing: 2px;
color: #54676f;
font-weight: bold;
font-size: 12px;
margin-bottom: 1em;
border-color: #90a2ac;
}
#optional-packages th:first-child {
padding-left: 4.25em;
}
#optional-packages td {
background: #fff;
height: 1px; /* TODO: works on Chrome, but not Firefox */
/* height: 100%; TODO: works on Firefox, but not Chrome */
/* TODO:
see https://stackoverflow.com/a/34781198/1048862
(could also do tr with height: 1px which gets ignored,
then td with height: inherit; but this effectively the
same as just doing a td with height: 1px.)
I don't like how either Firefox or Chrome handle this
styling (setting the height of the parent shouldn't be
required at all, the browser is rendering a height
regardless!) but I think I lean toward Firefox's as
being more correct; the hack in Firefox is setting a
flexible 100% height on the parent, rather than the hack
in Chrome which is setting a height that is too small
and stretched or ignored anyway.
*/
}
#optional-packages th,
#optional-packages .optpkg td:first-child label,
#optional-packages td:not(:first-child) {
padding: 15px;
}
#optional-packages .optpkg-name {
font-weight: bold;
}
#optional-packages .optpkg input[type=checkbox] {
transform: scale(1.5);
cursor: pointer;
margin-right: 2em;
}
#optional-packages .optpkg label {
display: flex; display: flex;
align-items: center; padding: 2em;
height: 100%; background: rgba(255, 255, 255, .4);
line-height: 1em; border: 10px solid transparent;
box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
cursor: pointer; cursor: pointer;
transition: all 200ms ease-out;
} }
#optional-packages:not(.disabled) .optpkg label:hover { .package:hover {
background: linear-gradient(90deg, rgba(242,248,253,1) 75%, rgba(242,248,253,0) 100%); transform: scale(1.02);
background: #fff;
box-shadow: 0 2px 20px -2px rgba(0, 0, 0, .05);
} }
#optional-packages .optpkg.selected td { .package.selected {
background-color: #f2f8fd; border-color: rgb(25, 97, 192);
background: #fff;
} }
#optional-packages .optpkg input[type=text] { .package-icon {
font-size: 12px; font-size: 48px;
padding: 6px; margin-right: 20px;
outline: none; }
.package-data {
flex-grow: 1;
}
.package-meta {
float: right;
font-size: 14px;
}
.package-downloads {
margin-right: 1em;
}
.package-version-input {
text-align: center; text-align: center;
border: 1px solid #ccc; max-width: 75px;
border-radius: 4px; padding: 4px;
margin-left: 5px;
border-radius: 5px;
border: 1px solid #aaa;
outline: none;
} }
#optional-packages .optpkg input[type=text]::placeholder { .package-link {
font-style: italic; font-size: 20px;
font-weight: bold;
color: inherit;
word-break: break-all;
} }
#optional-packages .optpkg-no-modules { .package-host {
font-size: 12px; font-size: 14px;
}
.package-modules {
margin-top: 1em;
}
.package-no-modules {
font-style: italic; font-style: italic;
color: #555; color: #555;
font-size: 14px;
} }
#optional-packages .optpkg-module { .module {
margin-top: .5em; margin: 10px 0;
margin-bottom: .5em; word-wrap: break-word;
} }
#optional-packages .optpkg-module .module-name { .module-link {
font-weight: bold; font-weight: bold;
font-family: 'PT Mono', monospace; font-family: 'PT Mono', monospace;
} }
#optional-packages .optpkg-module .module-description { .module-desc {
color: #444;
margin-left: 1em;
font-size: 14px; font-size: 14px;
margin-left: 1em;
} }
@ -264,6 +240,9 @@ select:disabled,
.swal-custom-content { .swal-custom-content {
text-align: left; text-align: left;
} }

View file

@ -14,20 +14,23 @@ function isStandard(packagePath) {
return packagePath.startsWith(caddyImportPath); return packagePath.startsWith(caddyImportPath);
} }
function substrBeforeLastDot(s) { function truncate(str, maxLen) {
return s.substr(0, s.lastIndexOf('.'))
}
function substrAfterLastDot(s) {
return s.substr(s.lastIndexOf('.'))
}
function truncate(str, len) {
if (!str) return ""; if (!str) return "";
var startLen = str.length; str = str.trim();
str = str.substring(0, len); let firstPeriod = str.match(/\.(\s|$)/); // first dot not in the middle of a word, or at end of string
if (startLen > len) { let terminate = firstPeriod ? firstPeriod.index+1 : str.length;
str += "..."; str = str.substring(0, terminate);
if (str.length <= maxLen) {
return str;
} }
return str; return str+"...";
}
function moduleDocsPreview(mod, maxLen) {
if (!mod || !mod.docs) return "";
let short = truncate(mod.docs, maxLen);
if (short.indexOf(mod.name) === 0) {
short = short.substr(mod.name.length).trim();
}
return short;
} }

View file

@ -155,29 +155,23 @@ $(function() {
}); });
function beginRendering(json) { function beginRendering(json, moduleID) {
pageData = json; pageData = json;
console.log("DATA:", pageData); console.log("PAGE DATA:", pageData);
// show notice if module is non-standard // show notice if module is non-standard
if (pageData.structure.type_name && !isStandard(pageData.structure.type_name)) { if (pageData.repo && !isStandard(pageData.structure.type_name)) {
var projectHref = 'https://'+pageData.structure.type_name; $('.nonstandard-project-link').attr('href', pageData.repo).text(pageData.repo);
projectHref = substrBeforeLastDot(projectHref);
$('.nonstandard-project-link').attr('href', projectHref).text(projectHref);
$('.nonstandard-notice').prepend(nonStandardFlag).show(); $('.nonstandard-notice').prepend(nonStandardFlag).show();
} }
if (pageData.structure.doc) { // for most types, just render their docs; but for maps or arrays, fall through to underlying type for docs
// for most types, just render their docs let rawDocs = pageData.structure.doc ?? pageData.structure.elems;
$('#top-doc').html(markdown(pageData.structure.doc));
} else if (pageData.structure.elems) { $('#top-doc').html(markdown(replaceGoTypeNameWithCaddyModuleName(rawDocs, moduleID)));
// for maps or arrays, fall through to the underlying type
$('#top-doc').html(markdown(pageData.structure.elems.doc));
}
$('#top-doc').append(makeSubmoduleList("", pageData.structure)); $('#top-doc').append(makeSubmoduleList("", pageData.structure));
renderData(pageData.structure, 0, "", $('<div class="group"/>')); renderData(pageData.structure, 0, "", $('<div class="group"/>'));
console.log("DOCS:", pageDocs);
if ($('#field-list-contents').text().trim()) { if ($('#field-list-contents').text().trim()) {
$('#field-list-header').show(); $('#field-list-header').show();
@ -359,6 +353,23 @@ function canTraverse() {
return pageData.breadcrumb != null; return pageData.breadcrumb != null;
} }
function replaceGoTypeNameWithCaddyModuleName(docs, moduleID) {
if (!docs || !moduleID) return docs;
// fully qualified type name
let fqtn = pageData.structure.type_name;
// extract just the local type name
let typeName = fqtn.substr(fqtn.lastIndexOf('.')+1)
// replace the type name with the Caddy module ID if it starts the docs.
if (docs.indexOf(typeName) === 0) {
docs = moduleID + docs.substr(typeName.length);
}
return docs;
}
function markdown(input) { function markdown(input) {
if (!input) { if (!input) {
return ""; return "";

View file

@ -1,72 +1,68 @@
// download package list as soon as possible // download package list as soon as possible
$.get("/api/packages").done(function(json) { $.get("/api/packages").done(function(json) {
// sort package list by most popular, seems to make sense for convenience
var packageList = json.result; var packageList = json.result;
const preselectedPackage = new URL(window.location.href).searchParams.getAll("package") ; packageList.sort((a, b) => {
return b.downloads > a.downloads ? 1 : -1;
});
const preselectedPackage = new URL(window.location.href).searchParams.getAll("package");
// wait until the DOM has finished loading before rendering the results // wait until the DOM has finished loading before rendering the results
$(function() { $(function() {
const optpkgTemplate = const packageTemplate =
'<tr class="optpkg">'+ '<div class="package">\n'+
' <td><label><input type="checkbox" class="optpkg-check"><span class="optpkg-name"></span></label></td>'+ ' <div class="package-icon">&#128230;</div>\n'+
' <td class="text-center"><input type="text" name="version" placeholder="latest" title="Package version" size="5"></td>'+ ' <div class="package-data">\n'+
' <td class="optpkg-modules"></td>'+ ' <div class="package-meta">\n'+
'</tr>'; ' <b>downloads:</b> <span class="package-downloads"></span>\n'+
' <b>version:</b> <input type="text" class="package-version-input" name="version" placeholder="latest" title="Any version string recognized by `go get` can be used">\n'+
' </div>\n'+
' <a target="_blank" title="View package repo" class="package-link"></a>\n'+
' <div class="package-modules"></div>\n'+
' </div>\n'+
'</div>\n'
const moduleTemplate =
'<div class="module">\n'+
' &#128268;<a target="_blank" title="View module docs" class="module-link"></a>\n'+
' <span class="module-desc"></span>\n'+
'</div>\n';
const optpkgModuleTemplate =
'<div class="optpkg-module">'+
' <a class="module-name" title="View docs"></a>'+
' <span class="module-description"></span>'+
'</div>';
for (var i = 0; i < packageList.length; i++) { for (var i = 0; i < packageList.length; i++) {
var pkg = packageList[i]; var pkg = packageList[i];
if (isStandard(pkg.path)) {
// not necessary to show, since these packages
// come with standard distribution
continue;
}
var $optpkg = $(optpkgTemplate); var $pkg = $(packageTemplate);
$('.optpkg-name', $optpkg).text(pkg.path);
let { provider, path } = splitVCSProvider(pkg.path);
if (provider) {
var $pkgHost = $('<span class="package-host"/>').text(provider);
$('.package-link', $pkg).html($pkgHost).append('<br/>');
}
$pkgName = $('<span class="package-name"/>').text(path);
$('.package-link', $pkg).append($pkgName);
$('.package-link', $pkg).prop('href', pkg.repo);
$('.package-downloads', $pkg).text(pkg.downloads);
if (preselectedPackage.includes(pkg.path)) { if (preselectedPackage.includes(pkg.path)) {
$('.optpkg-check', $optpkg).prop("checked", true); $($pkg).addClass("selected");
$('.optpkg-check', $optpkg).closest('.optpkg').toggleClass("selected");
} }
$('.optpkg-check', $optpkg).change({pkg: pkg}, (event) => {
const element = $(event.currentTarget);
let newUrl = new URL(window.location.href);
let currentSelected = newUrl.searchParams.getAll("package") ;
newUrl.searchParams.delete("package");
const pkgPath = event.data.pkg.path;
if (element.is(':checked')) {
if (!currentSelected.includes(pkgPath)) {
currentSelected = [...currentSelected, pkgPath];
}
} else {
const position = currentSelected.indexOf(pkgPath);
if (position >= 0) {
currentSelected.splice(position, 1);
}
}
currentSelected.forEach( (selected) => newUrl.searchParams.append("package", selected));
history.replaceState({}, "Download Caddy", newUrl.toString());
});
if (pkg.modules && pkg.modules.length > 0) { if (pkg.modules && pkg.modules.length > 0) {
for (var j = 0; j < pkg.modules.length; j++) { for (var j = 0; j < pkg.modules.length; j++) {
var mod = pkg.modules[j]; var mod = pkg.modules[j];
var $mod = $(optpkgModuleTemplate); var $mod = $(moduleTemplate);
$('.module-name', $mod).attr('href', '/docs/modules/'+mod.name).text(mod.name); $('.module-link', $mod).attr('href', '/docs/modules/'+mod.name).text(mod.name).attr('title', "View module details");
$('.module-description', $mod).text(truncate(mod.docs, 120)); $('.module-desc', $mod).text(moduleDocsPreview(mod, 120));
$('.optpkg-modules', $optpkg).append($mod); $('.package-modules', $pkg).append($mod);
} }
} else { } else {
$('.optpkg-modules', $optpkg) $('.package-modules', $pkg)
.addClass("optpkg-no-modules") .addClass("package-no-modules")
.text('This package does not add any modules. Either it is another kind of plugin (such as a config adapter) or this listing is in error.'); .text('This package does not add any modules to the JSON config structure. Either it is another kind of plugin (such as a config adapter) or this listing is in error.');
} }
$('#optional-packages').append($optpkg); $('#optional-packages').append($pkg);
} }
updatePage(); updatePage();
}); });
@ -86,19 +82,71 @@ $(function() {
downloadButtonHtml = $('#download').html(); downloadButtonHtml = $('#download').html();
// update the page, including the download link, when form fields change $('#filter').on('search keyup', function(event) {
$('#optional-packages').on('change', 'input[type=checkbox]', function() { var count = 0;
$(this).closest('.optpkg').toggleClass('selected'); var q = $(this).val().trim().toLowerCase();
updatePage();
}); $('.package').each(function() {
$('#optional-packages').on('change keyup', 'input[name=version]', function() { if (!q) {
updatePage(); // filter is cleared; show all
this.style.display = '';
return;
}
var corpus = $(this).find('.package-link, .module-link, .module-desc').text().trim().toLowerCase();
if (corpus.indexOf(q) === -1) {
this.style.display = 'none';
return;
}
this.style.display = '';
count++;
});
// update color of search input based on results
if (q) {
if (count > 0) {
$('#filter').addClass('found').removeClass('not-found');
} else {
$('#filter').addClass('not-found').removeClass('found');
}
} else {
$('#filter').removeClass('found not-found');
}
}); });
$('#platform').change(function() { $('#platform').change(function() {
updatePage(); updatePage();
}); });
$('#download').click(function(event) { $('#optional-packages').on('click', '.package', function() {
$(this).toggleClass('selected');
updatePage();
let newUrl = new URL(window.location.href);
let currentSelected = newUrl.searchParams.getAll("package") ;
newUrl.searchParams.delete("package");
const pkgPath = $('.package-link', $(this)).text().trim();
if ($(this).hasClass('selected')) {
if (!currentSelected.includes(pkgPath)) {
currentSelected = [...currentSelected, pkgPath];
}
} else {
const position = currentSelected.indexOf(pkgPath);
if (position >= 0) {
currentSelected.splice(position, 1);
}
}
currentSelected.forEach( (selected) => newUrl.searchParams.append("package", selected));
history.replaceState({}, document.title, newUrl.toString());
});
// when a link within a package listing is clicked, only operate the link (don't select the package)
$('#optional-packages').on('click', '.package-link, .module-link, .package-version-input', function(event) {
event.stopPropagation();
});
$('#download').click(function() {
if ($(this).hasClass('disabled')) { if ($(this).hasClass('disabled')) {
return false; return false;
} }
@ -180,7 +228,7 @@ function getDownloadLink() {
// get plugins and their versions // get plugins and their versions
$('#optional-packages .selected').each(function() { $('#optional-packages .selected').each(function() {
// get package path // get package path
var p = $('.optpkg-name', this).text().trim(); var p = $('.package-link', this).text().trim();
// get package version, if user specified one // get package version, if user specified one
var ver = $('input[name=version]', this).val().trim(); var ver = $('input[name=version]', this).val().trim();
@ -216,7 +264,7 @@ function handleBuildError(jqxhr, status, error) {
} }
function updatePage() { function updatePage() {
$('#package-count').text($('.optpkg.selected').length); $('#package-count').text($('.package.selected').length);
$('#download').attr('href', getDownloadLink()); $('#download').attr('href', getDownloadLink());
} }
@ -244,4 +292,17 @@ function enableFields() {
window.onbeforeunload = null; window.onbeforeunload = null;
} }
function splitVCSProvider(pkgPath) {
var providers = ["github.com/", "bitbucket.org/"];
for (var i = 0; i < providers.length; i++) {
if (pkgPath.toLowerCase().indexOf(providers[i]) == 0) {
return {
provider: providers[i],
path: pkgPath.slice(providers[i].length)
};
}
}
return {provider: "", path: pkgPath};
}
var downloadButtonHtml; // to restore button to its original contents var downloadButtonHtml; // to restore button to its original contents

View file

@ -9,7 +9,7 @@ if (moduleID) {
$(function() { $(function() {
$('#module-docs-container').show(); $('#module-docs-container').show();
$('h1').text("Module "+moduleID); $('h1').text("Module "+moduleID);
beginRendering(json.result); beginRendering(json.result, moduleID);
}); });
}); });
} else { } else {
@ -23,10 +23,17 @@ if (moduleID) {
$table = $('#module-list'); $table = $('#module-list');
for (modID in moduleList) { for (modID in moduleList) {
var val = moduleList[modID]; var val = moduleList[modID];
// refine a short preview of the module's docs
let shortDoc = truncate(val.doc, 200);
if (shortDoc && shortDoc.indexOf(modID) === 0) {
shortDoc = shortDoc.substr(modID.length).trim();
}
var standard = isStandard(val.type_name); var standard = isStandard(val.type_name);
var $tr = $('<tr/>'); var $tr = $('<tr/>');
$tr.append('<td><a href="./'+modID+'" class="module-link">'+modID+'</a>'+(standard ? '' : ' '+nonStandardFlag)+'</td>'); $tr.append('<td><a href="./'+modID+'" class="module-link">'+modID+'</a>'+(standard ? '' : ' '+nonStandardFlag)+'</td>');
$tr.append('<td>'+markdown(truncate(val.doc, 200))+'</td>'); $tr.append($('<td/>').text(shortDoc));
$table.append($tr); $table.append($tr);
} }
}); });