diff --git a/swh/web/assets/src/bundles/save/index.js b/swh/web/assets/src/bundles/save/index.js
index 196bdbeb..85b97b09 100644
--- a/swh/web/assets/src/bundles/save/index.js
+++ b/swh/web/assets/src/bundles/save/index.js
@@ -1,299 +1,301 @@
/**
* Copyright (C) 2018-2019 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
import {handleFetchError, csrfPost, isGitRepoUrl, removeUrlFragment} from 'utils/functions';
import {validate} from 'validate.js';
let saveRequestsTable;
function originSaveRequest(originType, originUrl,
acceptedCallback, pendingCallback, errorCallback) {
let addSaveOriginRequestUrl = Urls.origin_save_request(originType, originUrl);
let headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
$('.swh-processing-save-request').css('display', 'block');
csrfPost(addSaveOriginRequestUrl, headers)
.then(handleFetchError)
.then(response => response.json())
.then(data => {
$('.swh-processing-save-request').css('display', 'none');
if (data.save_request_status === 'accepted') {
acceptedCallback();
} else {
pendingCallback();
}
})
.catch(response => {
$('.swh-processing-save-request').css('display', 'none');
errorCallback(response.status);
});
}
function htmlAlert(type, message) {
return `
${message}
`;
}
export function initOriginSave() {
$(document).ready(() => {
$.fn.dataTable.ext.errMode = 'none';
fetch(Urls.origin_save_types_list())
.then(response => response.json())
.then(data => {
for (let originType of data) {
$('#swh-input-origin-type').append(``);
}
});
saveRequestsTable = $('#swh-origin-save-requests')
.on('error.dt', (e, settings, techNote, message) => {
$('#swh-origin-save-request-list-error').text('An error occurred while retrieving the save requests list');
console.log(message);
})
.DataTable({
serverSide: true,
ajax: Urls.origin_save_requests_list('all'),
searchDelay: 1000,
columns: [
{
data: 'save_request_date',
name: 'request_date',
render: (data, type, row) => {
if (type === 'display') {
let date = new Date(data);
return date.toLocaleString();
}
return data;
}
},
{
data: 'origin_type',
name: 'origin_type'
},
{
data: 'origin_url',
name: 'origin_url',
render: (data, type, row) => {
if (type === 'display') {
const sanitizedURL = $.fn.dataTable.render.text().display(data);
return `${sanitizedURL}`;
}
return data;
}
},
{
data: 'save_request_status',
name: 'status'
},
{
data: 'save_task_status',
name: 'loading_task_status',
render: (data, type, row) => {
if (data === 'succeed') {
let browseOriginUrl = Urls.browse_origin(row.origin_url);
if (row.visit_date) {
browseOriginUrl += `visit/${row.visit_date}/`;
}
return `${data}`;
}
return data;
}
}
],
scrollY: '50vh',
scrollCollapse: true,
order: [[0, 'desc']],
responsive: {
details: {
type: 'none'
}
}
});
+ swh.webapp.addJumpToPagePopoverToDataTable(saveRequestsTable);
+
$('#swh-origin-save-requests-list-tab').on('shown.bs.tab', () => {
saveRequestsTable.draw();
window.location.hash = '#requests';
});
$('#swh-origin-save-request-create-tab').on('shown.bs.tab', () => {
removeUrlFragment();
});
let saveRequestAcceptedAlert = htmlAlert(
'success',
'The "save code now" request has been accepted and will be processed as soon as possible.'
);
let saveRequestPendingAlert = htmlAlert(
'warning',
'The "save code now" request has been put in pending state and may be accepted for processing after manual review.'
);
let saveRequestRejectedAlert = htmlAlert(
'danger',
'The "save code now" request has been rejected because the provided origin url is blacklisted.'
);
let saveRequestRateLimitedAlert = htmlAlert(
'danger',
'The rate limit for "save code now" requests has been reached. Please try again later.'
);
let saveRequestUnknownErrorAlert = htmlAlert(
'danger',
'An unexpected error happened when submitting the "save code now request'
);
$('#swh-save-origin-form').submit(event => {
event.preventDefault();
event.stopPropagation();
$('.alert').alert('close');
if (event.target.checkValidity()) {
$(event.target).removeClass('was-validated');
let originType = $('#swh-input-origin-type').val();
let originUrl = $('#swh-input-origin-url').val();
originSaveRequest(originType, originUrl,
() => $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert),
() => $('#swh-origin-save-request-status').html(saveRequestPendingAlert),
(statusCode) => {
$('#swh-origin-save-request-status').css('color', 'red');
if (statusCode === 403) {
$('#swh-origin-save-request-status').html(saveRequestRejectedAlert);
} else if (statusCode === 429) {
$('#swh-origin-save-request-status').html(saveRequestRateLimitedAlert);
} else {
$('#swh-origin-save-request-status').html(saveRequestUnknownErrorAlert);
}
});
} else {
$(event.target).addClass('was-validated');
}
});
$('#swh-show-origin-save-requests-list').on('click', (event) => {
event.preventDefault();
$('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
});
$('#swh-input-origin-url').on('input', function(event) {
let originUrl = $(this).val().trim();
$(this).val(originUrl);
$('#swh-input-origin-type option').each(function() {
let val = $(this).val();
if (val && originUrl.includes(val)) {
$(this).prop('selected', true);
}
});
});
if (window.location.hash === '#requests') {
$('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
}
});
}
export function validateSaveOriginUrl(input) {
let originUrl = input.value.trim();
let validUrl = validate({website: originUrl}, {
website: {
url: {
schemes: ['http', 'https', 'svn', 'git']
}
}
}) === undefined;
let originType = $('#swh-input-origin-type').val();
if (originType === 'git' && validUrl) {
// additional checks for well known code hosting providers
let githubIdx = originUrl.indexOf('://github.com');
let gitlabIdx = originUrl.indexOf('://gitlab.');
let gitSfIdx = originUrl.indexOf('://git.code.sf.net');
let bitbucketIdx = originUrl.indexOf('://bitbucket.org');
if (githubIdx !== -1 && githubIdx <= 5) {
validUrl = isGitRepoUrl(originUrl, 'github.com');
} else if (gitlabIdx !== -1 && gitlabIdx <= 5) {
let startIdx = gitlabIdx + 3;
let idx = originUrl.indexOf('/', startIdx);
if (idx !== -1) {
let gitlabDomain = originUrl.substr(startIdx, idx - startIdx);
// GitLab repo url needs to be suffixed by '.git' in order to be successfully loaded
// This is due to a bug in dulwich < 0.19.11.
// TODO: remove this check once dulwich >= 0.19.11 is used in production
validUrl = isGitRepoUrl(originUrl, gitlabDomain) && originUrl.endsWith('.git');
} else {
validUrl = false;
}
} else if (gitSfIdx !== -1 && gitSfIdx <= 5) {
validUrl = isGitRepoUrl(originUrl, 'git.code.sf.net/p');
} else if (bitbucketIdx !== -1 && bitbucketIdx <= 5) {
validUrl = isGitRepoUrl(originUrl, 'bitbucket.org');
}
}
if (validUrl) {
input.setCustomValidity('');
} else {
input.setCustomValidity('The origin url is not valid or does not reference a code repository');
}
}
export function initTakeNewSnapshot() {
let newSnapshotRequestAcceptedAlert = htmlAlert(
'success',
'The "take new snapshot" request has been accepted and will be processed as soon as possible.'
);
let newSnapshotRequestPendingAlert = htmlAlert(
'warning',
'The "take new snapshot" request has been put in pending state and may be accepted for processing after manual review.'
);
let newSnapshotRequestRejectedAlert = htmlAlert(
'danger',
'The "take new snapshot" request has been rejected.'
);
let newSnapshotRequestRateLimitAlert = htmlAlert(
'danger',
'The rate limit for "take new snapshot" requests has been reached. Please try again later.'
);
let newSnapshotRequestUnknownErrorAlert = htmlAlert(
'danger',
'An unexpected error happened when submitting the "save code now request".'
);
$(document).ready(() => {
$('#swh-take-new-snapshot-form').submit(event => {
event.preventDefault();
event.stopPropagation();
let originType = $('#swh-input-origin-type').val();
let originUrl = $('#swh-input-origin-url').val();
originSaveRequest(originType, originUrl,
() => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestAcceptedAlert),
() => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestPendingAlert),
(statusCode) => {
$('#swh-take-new-snapshot-request-status').css('color', 'red');
if (statusCode === 403) {
$('#swh-take-new-snapshot-request-status').html(newSnapshotRequestRejectedAlert);
} else if (statusCode === 429) {
$('#swh-take-new-snapshot-request-status').html(newSnapshotRequestRateLimitAlert);
} else {
$('#swh-take-new-snapshot-request-status').html(newSnapshotRequestUnknownErrorAlert);
}
});
});
});
}
diff --git a/swh/web/assets/src/bundles/webapp/webapp-utils.js b/swh/web/assets/src/bundles/webapp/webapp-utils.js
index 8fc5ab9d..79bda034 100644
--- a/swh/web/assets/src/bundles/webapp/webapp-utils.js
+++ b/swh/web/assets/src/bundles/webapp/webapp-utils.js
@@ -1,210 +1,245 @@
/**
* Copyright (C) 2018-2019 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
import objectFitImages from 'object-fit-images';
import {Layout} from 'admin-lte';
import {selectText} from 'utils/functions';
import {BREAKPOINT_MD} from 'utils/constants';
let collapseSidebar = false;
let previousSidebarState = localStorage.getItem('swh-sidebar-collapsed');
if (previousSidebarState !== undefined) {
collapseSidebar = JSON.parse(previousSidebarState);
}
// adapt implementation of fixLayoutHeight from admin-lte
Layout.prototype.fixLayoutHeight = () => {
let heights = {
window: $(window).height(),
header: $('.main-header').outerHeight(),
footer: $('.footer').outerHeight(),
sidebar: $('.main-sidebar').height(),
topbar: $('.swh-top-bar').height()
};
let offset = 10;
$('.content-wrapper').css('min-height', heights.window - heights.topbar - heights.header - heights.footer - offset);
$('.main-sidebar').css('min-height', heights.window - heights.topbar - heights.header - heights.footer - offset);
};
$(document).on('DOMContentLoaded', () => {
// set state to collapsed on smaller devices
if ($(window).width() < BREAKPOINT_MD) {
collapseSidebar = true;
}
// restore previous sidebar state (collapsed/expanded)
if (collapseSidebar) {
// hack to avoid animated transition for collapsing sidebar
// when loading a page
let sidebarTransition = $('.main-sidebar, .main-sidebar:before').css('transition');
let sidebarEltsTransition = $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition');
$('.main-sidebar, .main-sidebar:before').css('transition', 'none');
$('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', 'none');
$('body').addClass('sidebar-collapse');
$('.swh-words-logo-swh').css('visibility', 'visible');
// restore transitions for user navigation
setTimeout(() => {
$('.main-sidebar, .main-sidebar:before').css('transition', sidebarTransition);
$('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', sidebarEltsTransition);
});
}
});
$(document).on('collapsed.lte.pushmenu', event => {
if ($('body').width() >= BREAKPOINT_MD) {
$('.swh-words-logo-swh').css('visibility', 'visible');
}
});
$(document).on('shown.lte.pushmenu', event => {
$('.swh-words-logo-swh').css('visibility', 'hidden');
});
function ensureNoFooterOverflow() {
$('body').css('padding-bottom', $('footer').outerHeight() + 'px');
}
$(document).ready(() => {
// redirect to last browse page if any when clicking on the 'Browse' entry
// in the sidebar
$(`.swh-browse-link`).click(event => {
let lastBrowsePage = sessionStorage.getItem('last-browse-page');
if (lastBrowsePage) {
event.preventDefault();
window.location = lastBrowsePage;
}
});
// ensure footer do not overflow main content for mobile devices
// or after resizing the browser window
ensureNoFooterOverflow();
$(window).resize(function() {
ensureNoFooterOverflow();
if ($('body').hasClass('sidebar-collapse') && $('body').width() >= BREAKPOINT_MD) {
$('.swh-words-logo-swh').css('visibility', 'visible');
}
});
// activate css polyfill 'object-fit: contain' in old browsers
objectFitImages();
// reparent the modals to the top navigation div in order to be able
// to display them
$('.swh-browse-top-navigation').append($('.modal'));
let selectedCode = null;
function getCodeOrPreEltUnderPointer(e) {
let elts = document.elementsFromPoint(e.clientX, e.clientY);
for (let elt of elts) {
if (elt.nodeName === 'CODE' || elt.nodeName === 'PRE') {
return elt;
}
}
return null;
}
// click handler to set focus on code block for copy
$(document).click(e => {
selectedCode = getCodeOrPreEltUnderPointer(e);
});
function selectCode(event, selectedCode) {
if (selectedCode) {
let hljsLnCodeElts = $(selectedCode).find('.hljs-ln-code');
if (hljsLnCodeElts.length) {
selectText(hljsLnCodeElts[0], hljsLnCodeElts[hljsLnCodeElts.length - 1]);
} else {
selectText(selectedCode.firstChild, selectedCode.lastChild);
}
event.preventDefault();
}
}
// select the whole text of focused code block when user
// double clicks or hits Ctrl+A
$(document).dblclick(e => {
if ((e.ctrlKey || e.metaKey)) {
selectCode(e, getCodeOrPreEltUnderPointer(e));
}
});
$(document).keydown(e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
selectCode(e, selectedCode);
}
});
// show/hide back-to-top button
let scrollThreshold = 0;
scrollThreshold += $('.swh-top-bar').height() || 0;
scrollThreshold += $('.navbar').height() || 0;
$(window).scroll(() => {
if ($(window).scrollTop() > scrollThreshold) {
$('#back-to-top').css('display', 'block');
} else {
$('#back-to-top').css('display', 'none');
}
});
});
export function initPage(page) {
$(document).ready(() => {
// set relevant sidebar link to page active
$(`.swh-${page}-item`).addClass('active');
$(`.swh-${page}-link`).addClass('active');
// triggered when unloading the current page
$(window).on('unload', () => {
// backup sidebar state (collapsed/expanded)
let sidebarCollapsed = $('body').hasClass('sidebar-collapse');
localStorage.setItem('swh-sidebar-collapsed', JSON.stringify(sidebarCollapsed));
// backup current browse page
if (page === 'browse') {
sessionStorage.setItem('last-browse-page', window.location);
}
});
});
}
export function showModalMessage(title, message) {
$('#swh-web-modal-message .modal-title').text(title);
$('#swh-web-modal-message .modal-content p').text(message);
$('#swh-web-modal-message').modal('show');
}
export function showModalConfirm(title, message, callback) {
$('#swh-web-modal-confirm .modal-title').text(title);
$('#swh-web-modal-confirm .modal-content p').text(message);
$('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').bind('click', () => {
callback();
$('#swh-web-modal-confirm').modal('hide');
$('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').unbind('click');
});
$('#swh-web-modal-confirm').modal('show');
}
+export function addJumpToPagePopoverToDataTable(dataTableElt) {
+ dataTableElt.on('draw.dt', function() {
+ $('.paginate_button.disabled').css('cursor', 'pointer');
+ $('.paginate_button.disabled').on('click', event => {
+ const pageInfo = dataTableElt.page.info();
+ let content = ' / ${pageInfo.pages}`;
+ $(event.target).popover({
+ 'title': 'Jump to page',
+ 'content': content,
+ 'html': true,
+ 'placement': 'top',
+ 'sanitizeFn': swh.webapp.filterXSS
+ });
+ $(event.target).popover('show');
+ $('.jump-to-page').on('change', function() {
+ $('.paginate_button.disabled').popover('hide');
+ const pageNumber = parseInt($(this).val()) - 1;
+ dataTableElt.page(pageNumber).draw('page');
+ });
+ });
+ });
+
+ dataTableElt.on('preXhr.dt', () => {
+ $('.paginate_button.disabled').popover('hide');
+ });
+}
+
let swhObjectIcons;
export function setSwhObjectIcons(icons) {
swhObjectIcons = icons;
}
export function getSwhObjectIcon(swhObjectType) {
return swhObjectIcons[swhObjectType];
}
let browsedSwhObjectMetadata = {};
export function setBrowsedSwhObjectMetadata(metadata) {
browsedSwhObjectMetadata = metadata;
}
export function getBrowsedSwhObjectMetadata() {
return browsedSwhObjectMetadata;
}