diff --git a/assets/src/bundles/save/index.js b/assets/src/bundles/save/index.js
index d3e48af0..3bdd2963 100644
--- a/assets/src/bundles/save/index.js
+++ b/assets/src/bundles/save/index.js
@@ -1,553 +1,561 @@
/**
* Copyright (C) 2018-2021 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 {csrfPost, handleFetchError, isGitRepoUrl, htmlAlert, removeUrlFragment,
getCanonicalOriginURL} from 'utils/functions';
import {swhSpinnerSrc} from 'utils/constants';
import artifactFormRowTemplate from './artifact-form-row.ejs';
let saveRequestsTable;
async function originSaveRequest(
originType, originUrl, extraData,
acceptedCallback, pendingCallback, errorCallback
) {
// Actually trigger the origin save request
const addSaveOriginRequestUrl = Urls.api_1_save_origin(originType, originUrl);
$('.swh-processing-save-request').css('display', 'block');
let headers = {};
let body = null;
if (extraData !== {}) {
body = JSON.stringify(extraData);
headers = {
'Content-Type': 'application/json'
};
};
try {
const response = await csrfPost(addSaveOriginRequestUrl, headers, body);
handleFetchError(response);
const data = await response.json();
$('.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');
const errorData = await response.json();
errorCallback(response.status, errorData);
};
}
function addArtifactVersionAutofillHandler(formId) {
// autofill artifact version input with the filename from
// the artifact url without extensions
$(`#swh-input-artifact-url-${formId}`).on('input', function(event) {
const artifactUrl = $(this).val().trim();
let filename = artifactUrl.split('/').slice(-1)[0];
if (filename !== artifactUrl) {
filename = filename.replace(/tar.*$/, 'tar');
const filenameNoExt = filename.split('.').slice(0, -1).join('.');
const artifactVersion = $(`#swh-input-artifact-version-${formId}`);
if (filenameNoExt !== filename) {
artifactVersion.val(filenameNoExt);
}
}
});
}
export function maybeRequireExtraInputs() {
// Read the actual selected value and depending on the origin type, display some extra
// inputs or hide them. This makes the extra inputs disabled when not displayed.
const originType = $('#swh-input-visit-type').val();
let display = 'none';
let disabled = true;
if (originType === 'archives') {
display = 'flex';
disabled = false;
}
$('.swh-save-origin-archives-form').css('display', display);
if (!disabled) {
// help paragraph must have block display for proper rendering
$('#swh-save-origin-archives-help').css('display', 'block');
}
$('.swh-save-origin-archives-form .form-control').prop('disabled', disabled);
if (originType === 'archives' && $('.swh-save-origin-archives-form').length === 1) {
// insert first artifact row when the archives visit type is selected for the first time
$('.swh-save-origin-archives-form').last().after(
artifactFormRowTemplate({deletableRow: false, formId: 0}));
addArtifactVersionAutofillHandler(0);
}
}
export function addArtifactFormRow() {
const formId = $('.swh-save-origin-artifact-form').length;
$('.swh-save-origin-artifact-form').last().after(
artifactFormRowTemplate({
deletableRow: true,
formId: formId
})
);
addArtifactVersionAutofillHandler(formId);
}
export function deleteArtifactFormRow(event) {
$(event.target).closest('.swh-save-origin-artifact-form').remove();
}
const userRequestsFilterCheckbox = `
`;
export function initOriginSave() {
$(document).ready(() => {
$.fn.dataTable.ext.errMode = 'none';
// set git as the default value as before
$('#swh-input-visit-type').val('git');
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,
processing: true,
language: {
processing: ``
},
ajax: {
url: Urls.origin_save_requests_list('all'),
data: (d) => {
if (swh.webapp.isUserLoggedIn() && $('#swh-save-requests-user-filter').prop('checked')) {
d.user_requests_only = '1';
}
}
},
searchDelay: 1000,
// see https://datatables.net/examples/advanced_init/dom_toolbar.html and the comments section
// this option customizes datatables UI components by adding an extra checkbox above the table
// while keeping bootstrap layout
dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' +
'<"row"<"col-sm-12"tr>>' +
'<"row"<"col-sm-5"i><"col-sm-7"p>>',
fnInitComplete: function() {
if (swh.webapp.isUserLoggedIn()) {
$('div.user-requests-filter').html(userRequestsFilterCheckbox);
$('#swh-save-requests-user-filter').on('change', () => {
saveRequestsTable.draw();
});
}
},
columns: [
{
data: 'save_request_date',
name: 'request_date',
render: (data, type, row) => {
if (type === 'display') {
const date = new Date(data);
return date.toLocaleString();
}
return data;
}
},
{
data: 'visit_type',
name: 'visit_type'
},
{
data: 'origin_url',
name: 'origin_url',
render: (data, type, row) => {
if (type === 'display') {
let html = '';
const sanitizedURL = $.fn.dataTable.render.text().display(data);
if (row.save_task_status === 'succeeded') {
if (row.visit_status === 'full' || row.visit_status === 'partial') {
let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(sanitizedURL)}`;
if (row.visit_date) {
browseOriginUrl += `×tamp=${encodeURIComponent(row.visit_date)}`;
}
html += `${sanitizedURL}`;
} else {
const tooltip = 'origin was successfully loaded, waiting for data to be available in database';
html += `${sanitizedURL}`;
}
} else {
html += sanitizedURL;
}
html += ` ` +
'';
return html;
}
return data;
}
},
{
data: 'save_request_status',
name: 'status'
},
{
data: 'save_task_status',
name: 'loading_task_status'
},
{
name: 'info',
render: (data, type, row) => {
if (row.save_task_status === 'succeeded' || row.save_task_status === 'failed' ||
row.note != null) {
return ``;
} else {
return '';
}
}
},
{
render: (data, type, row) => {
if (row.save_request_status === 'accepted') {
const saveAgainButton =
'';
return saveAgainButton;
} else {
return '';
}
}
}
],
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-help-tab').on('shown.bs.tab', () => {
removeUrlFragment();
$('.swh-save-request-info').popover('dispose');
});
const saveRequestAcceptedAlert = htmlAlert(
'success',
'The "save code now" request has been accepted and will be processed as soon as possible.',
true
);
const saveRequestPendingAlert = htmlAlert(
'warning',
'The "save code now" request has been put in pending state and may be accepted for processing after manual review.',
true
);
const saveRequestRateLimitedAlert = htmlAlert(
'danger',
'The rate limit for "save code now" requests has been reached. Please try again later.',
true
);
const saveRequestUnknownErrorAlert = htmlAlert(
'danger',
'An unexpected error happened when submitting the "save code now request".',
true
);
$('#swh-save-origin-form').submit(async event => {
event.preventDefault();
event.stopPropagation();
$('.alert').alert('close');
if (event.target.checkValidity()) {
$(event.target).removeClass('was-validated');
const originType = $('#swh-input-visit-type').val();
let originUrl = $('#swh-input-origin-url').val();
originUrl = await getCanonicalOriginURL(originUrl);
// read the extra inputs for the 'archives' type
const extraData = {};
if (originType === 'archives') {
extraData['archives_data'] = [];
for (let i = 0; i < $('.swh-save-origin-artifact-form').length; ++i) {
extraData['archives_data'].push({
'artifact_url': $(`#swh-input-artifact-url-${i}`).val(),
'artifact_version': $(`#swh-input-artifact-version-${i}`).val()
});
}
}
originSaveRequest(originType, originUrl, extraData,
() => $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert),
() => $('#swh-origin-save-request-status').html(saveRequestPendingAlert),
(statusCode, errorData) => {
$('#swh-origin-save-request-status').css('color', 'red');
if (statusCode === 403) {
const errorAlert = htmlAlert('danger', `Error: ${errorData['reason']}`);
$('#swh-origin-save-request-status').html(errorAlert);
} else if (statusCode === 429) {
$('#swh-origin-save-request-status').html(saveRequestRateLimitedAlert);
} else if (statusCode === 400) {
const errorAlert = htmlAlert('danger', errorData['reason']);
$('#swh-origin-save-request-status').html(errorAlert);
} 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) {
const originUrl = $(this).val().trim();
$(this).val(originUrl);
$('#swh-input-visit-type option').each(function() {
const val = $(this).val();
if (val && originUrl.includes(val)) {
$(this).prop('selected', true);
// origin URL input need to be validated once new visit type set
validateSaveOriginUrl($('#swh-input-origin-url')[0]);
}
});
});
if (window.location.hash === '#requests') {
$('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
}
+ $(window).on('hashchange', () => {
+ if (window.location.hash === '#requests') {
+ $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
+ } else {
+ $('.nav-tabs a[href="#swh-origin-save-requests-create"]').tab('show');
+ }
+ });
+
});
}
export function validateSaveOriginUrl(input) {
const originType = $('#swh-input-visit-type').val();
let originUrl = null;
let validUrl = true;
try {
originUrl = new URL(input.value.trim());
} catch (TypeError) {
validUrl = false;
}
if (validUrl) {
const allowedProtocols = ['http:', 'https:', 'svn:', 'git:', 'rsync:', 'pserver:', 'ssh:', 'bzr:'];
validUrl = (
allowedProtocols.find(protocol => protocol === originUrl.protocol) !== undefined
);
}
if (validUrl && originType === 'git') {
validUrl = isGitRepoUrl(originUrl);
}
if (validUrl) {
input.setCustomValidity('');
} else {
input.setCustomValidity('The origin url is not valid or does not reference a code repository');
}
}
export function initTakeNewSnapshot() {
const newSnapshotRequestAcceptedAlert = htmlAlert(
'success',
'The "take new snapshot" request has been accepted and will be processed as soon as possible.',
true
);
const newSnapshotRequestPendingAlert = htmlAlert(
'warning',
'The "take new snapshot" request has been put in pending state and may be accepted for processing after manual review.',
true
);
const newSnapshotRequestRateLimitAlert = htmlAlert(
'danger',
'The rate limit for "take new snapshot" requests has been reached. Please try again later.',
true
);
const newSnapshotRequestUnknownErrorAlert = htmlAlert(
'danger',
'An unexpected error happened when submitting the "save code now request".',
true
);
$(document).ready(() => {
$('#swh-take-new-snapshot-form').submit(event => {
event.preventDefault();
event.stopPropagation();
const originType = $('#swh-input-visit-type').val();
const originUrl = $('#swh-input-origin-url').val();
const extraData = {};
originSaveRequest(originType, originUrl, extraData,
() => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestAcceptedAlert),
() => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestPendingAlert),
(statusCode, errorData) => {
$('#swh-take-new-snapshot-request-status').css('color', 'red');
if (statusCode === 403) {
const errorAlert = htmlAlert('danger', `Error: ${errorData['detail']}`, true);
$('#swh-take-new-snapshot-request-status').html(errorAlert);
} else if (statusCode === 429) {
$('#swh-take-new-snapshot-request-status').html(newSnapshotRequestRateLimitAlert);
} else {
$('#swh-take-new-snapshot-request-status').html(newSnapshotRequestUnknownErrorAlert);
}
});
});
});
}
export function formatValuePerType(type, value) {
// Given some typed value, format and return accordingly formatted value
const mapFormatPerTypeFn = {
'json': (v) => JSON.stringify(v, null, 2),
'date': (v) => new Date(v).toLocaleString(),
'raw': (v) => v,
'duration': (v) => v + ' seconds'
};
return value === null ? null : mapFormatPerTypeFn[type](value);
}
export async function displaySaveRequestInfo(event, saveRequestId) {
event.stopPropagation();
const saveRequestTaskInfoUrl = Urls.origin_save_task_info(saveRequestId);
// close popover when clicking again on the info icon
if ($(event.target).data('bs.popover')) {
$(event.target).popover('dispose');
return;
}
$('.swh-save-request-info').popover('dispose');
$(event.target).popover({
animation: false,
boundary: 'viewport',
container: 'body',
title: 'Save request task information ' +
'`,
content: `