Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/assets/src/bundles/add_forge/add-request-history-item.ejs b/assets/src/bundles/add_forge/add-request-history-item.ejs
index 710c3144..16de3716 100644
--- a/assets/src/bundles/add_forge/add-request-history-item.ejs
+++ b/assets/src/bundles/add_forge/add-request-history-item.ejs
@@ -1,31 +1,31 @@
<%#
Copyright (C) 2022 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
%>
<div class="history-item">
<div class="card border-dark">
<div class="card-header" id="heading<%= index %>}">
<h2 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#collapse<%= index %>"
aria-expanded="true" aria-controls="collapse$<%= index %>">
- From <%= event.actor %> (<%= event.actor_role %>) on <%= event.date.slice(0, 16) %>
+ From <%= event.actor %> (<%= event.actor_role %>) on <%= getHumanReadableDate(event.date) %>
<%if (event.new_status !== null) { %>
<span style="padding-left: 10px;">New status:</span> <%= swh.add_forge.formatRequestStatusName(event.new_status) %>
<% } %>
</button>
</h2>
</div>
<div id="collapse<%= index %>" class="collapse" aria-labelledby="headingOne" data-parent="#requestHistory">
<div class="card-body">
<p><%= event.text %></p>
<%if (event.new_status !== null) { %>
<p>
<span>Status changed to:</span> <strong><%= swh.add_forge.formatRequestStatusName(event.new_status) %></strong>
</p>
<% } %>
</div>
</div>
</div>
-</div>
\ No newline at end of file
+</div>
diff --git a/assets/src/bundles/add_forge/create-request.js b/assets/src/bundles/add_forge/create-request.js
index cae10aea..78f0ec98 100644
--- a/assets/src/bundles/add_forge/create-request.js
+++ b/assets/src/bundles/add_forge/create-request.js
@@ -1,116 +1,118 @@
/**
* Copyright (C) 2022 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, removeUrlFragment, csrfPost} from 'utils/functions';
+import {handleFetchError, removeUrlFragment, csrfPost,
+ getHumanReadableDate} from 'utils/functions';
let requestBrowseTable;
export function onCreateRequestPageLoad() {
$(document).ready(() => {
$('#requestCreateForm').submit(async function(event) {
event.preventDefault();
try {
const response = await csrfPost($(this).attr('action'),
{'Content-Type': 'application/x-www-form-urlencoded'},
$(this).serialize());
handleFetchError(response);
$('#userMessageDetail').empty();
$('#userMessage').text('Your request has been submitted');
$('#userMessage').removeClass('badge-danger');
$('#userMessage').addClass('badge-success');
requestBrowseTable.draw(); // redraw the table to update the list
} catch (errorResponse) {
$('#userMessageDetail').empty();
let errorMessage;
let errorMessageDetail = '';
const errorData = await errorResponse.json();
// if (errorResponse.content_type === 'text/plain') { // does not work?
if (errorResponse.status === 409) {
errorMessage = errorData;
} else { // assuming json response
// const exception = errorData['exception'];
errorMessage = 'An unknown error occurred during the request creation';
try {
const reason = JSON.parse(errorData['reason']);
Object.entries(reason).forEach((keys, _) => {
const key = keys[0];
const message = keys[1][0]; // take only the first issue
errorMessageDetail += `\n${key}: ${message}`;
});
} catch (_) {
errorMessageDetail = errorData['reason']; // can't parse it, leave it raw
}
}
$('#userMessage').text(
errorMessageDetail ? `Error: ${errorMessageDetail}` : errorMessage
);
$('#userMessage').removeClass('badge-success');
$('#userMessage').addClass('badge-danger');
}
});
$(window).on('hashchange', () => {
if (window.location.hash === '#browse-requests') {
$('.nav-tabs a[href="#swh-add-forge-requests-list"]').tab('show');
} else {
$('.nav-tabs a[href="#swh-add-forge-submit-request"]').tab('show');
}
});
$('#swh-add-forge-requests-list-tab').on('shown.bs.tab', () => {
window.location.hash = '#browse-requests';
});
$('#swh-add-forge-tab').on('shown.bs.tab', () => {
removeUrlFragment();
});
populateRequestBrowseList(); // Load existing requests
});
}
export function populateRequestBrowseList() {
requestBrowseTable = $('#add-forge-request-browse')
.on('error.dt', (e, settings, techNote, message) => {
$('#add-forge-browse-request-error').text(message);
})
.DataTable({
serverSide: true,
processing: true,
retrieve: true,
searching: true,
info: false,
dom: '<<"d-flex justify-content-between align-items-center"f' +
'<"#list-exclude">l>rt<"bottom"ip>>',
ajax: {
'url': Urls.add_forge_request_list_datatables()
},
columns: [
{
data: 'submission_date',
- name: 'submission_date'
+ name: 'submission_date',
+ render: getHumanReadableDate
},
{
data: 'forge_type',
name: 'forge_type'
},
{
data: 'forge_url',
name: 'forge_url'
},
{
data: 'status',
name: 'status',
render: function(data, type, row, meta) {
return swh.add_forge.formatRequestStatusName(data);
}
}
]
});
requestBrowseTable.draw();
}
diff --git a/assets/src/bundles/add_forge/moderation-dashboard.js b/assets/src/bundles/add_forge/moderation-dashboard.js
index 2956c6bf..eaaee4bd 100644
--- a/assets/src/bundles/add_forge/moderation-dashboard.js
+++ b/assets/src/bundles/add_forge/moderation-dashboard.js
@@ -1,60 +1,62 @@
/**
* Copyright (C) 2022 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 {getHumanReadableDate} from 'utils/functions';
+
export function onModerationPageLoad() {
populateModerationList();
}
export async function populateModerationList() {
$('#swh-add-forge-now-moderation-list')
.on('error.dt', (e, settings, techNote, message) => {
$('#swh-add-forge-now-moderation-list-error').text(message);
})
.DataTable({
serverSide: true,
processing: true,
searching: true,
info: false,
dom: '<<"d-flex justify-content-between align-items-center"f' +
'<"#list-exclude">l>rt<"bottom"ip>>',
ajax: {
'url': Urls.add_forge_request_list_datatables()
},
columns: [
{
data: 'id',
name: 'id',
render: function(data, type, row, meta) {
const dashboardUrl = Urls.add_forge_now_request_dashboard(data);
return `<a href=${dashboardUrl}>${data}</a>`;
}
},
{
data: 'submission_date',
name: 'submission_date',
- render: $.fn.dataTable.render.text()
+ render: getHumanReadableDate
},
{
data: 'forge_type',
name: 'forge_type',
render: $.fn.dataTable.render.text()
},
{
data: 'forge_url',
name: 'forge_url',
render: $.fn.dataTable.render.text()
},
{
data: 'status',
name: 'status',
render: function(data, type, row, meta) {
return swh.add_forge.formatRequestStatusName(data);
}
}
]
});
}
diff --git a/assets/src/bundles/add_forge/request-dashboard.js b/assets/src/bundles/add_forge/request-dashboard.js
index 056d1691..7924c3e6 100644
--- a/assets/src/bundles/add_forge/request-dashboard.js
+++ b/assets/src/bundles/add_forge/request-dashboard.js
@@ -1,127 +1,131 @@
/**
* Copyright (C) 2022 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} from 'utils/functions';
+import {handleFetchError, csrfPost, getHumanReadableDate} from 'utils/functions';
import emailTempate from './forge-admin-email.ejs';
import requestHistoryItem from './add-request-history-item.ejs';
let forgeRequest;
export function onRequestDashboardLoad(requestId) {
$(document).ready(() => {
populateRequestDetails(requestId);
$('#contactForgeAdmin').click((event) => {
contactForgeAdmin(event);
});
$('#updateRequestForm').submit(async function(event) {
event.preventDefault();
try {
const response = await csrfPost($(this).attr('action'),
{'Content-Type': 'application/x-www-form-urlencoded'},
$(this).serialize());
handleFetchError(response);
$('#userMessage').text('The request status has been updated ');
$('#userMessage').removeClass('badge-danger');
$('#userMessage').addClass('badge-success');
populateRequestDetails(requestId);
} catch (response) {
$('#userMessage').text('Sorry; Updating the request failed');
$('#userMessage').removeClass('badge-success');
$('#userMessage').addClass('badge-danger');
}
});
});
}
async function populateRequestDetails(requestId) {
try {
const response = await fetch(Urls.api_1_add_forge_request_get(requestId));
handleFetchError(response);
const data = await response.json();
forgeRequest = data.request;
$('#requestStatus').text(swh.add_forge.formatRequestStatusName(forgeRequest.status));
$('#requestType').text(forgeRequest.forge_type);
$('#requestURL').text(forgeRequest.forge_url);
$('#requestContactName').text(forgeRequest.forge_contact_name);
$('#requestContactConsent').text(forgeRequest.submitter_forward_username);
$('#requestContactEmail').text(forgeRequest.forge_contact_email);
$('#submitterMessage').text(forgeRequest.forge_contact_comment);
$('#updateComment').val('');
// Setting data for the email, now adding static data
$('#contactForgeAdmin').attr('emailTo', forgeRequest.forge_contact_email);
$('#contactForgeAdmin').attr('emailSubject', `[swh-add_forge_now] Request ${forgeRequest.id}`);
populateRequestHistory(data.history);
populateDecisionSelectOption(forgeRequest.status);
} catch (response) {
// The error message
$('#fetchError').removeClass('d-none');
$('#requestDetails').addClass('d-none');
}
}
function populateRequestHistory(history) {
$('#requestHistory').children().remove();
history.forEach((event, index) => {
- const historyEvent = requestHistoryItem({'event': event, 'index': index});
+ const historyEvent = requestHistoryItem({
+ 'event': event,
+ 'index': index,
+ 'getHumanReadableDate': getHumanReadableDate
+ });
$('#requestHistory').append(historyEvent);
});
}
export function populateDecisionSelectOption(currentStatus) {
const nextStatusesFor = {
'PENDING': ['WAITING_FOR_FEEDBACK', 'REJECTED', 'SUSPENDED'],
'WAITING_FOR_FEEDBACK': ['FEEDBACK_TO_HANDLE'],
'FEEDBACK_TO_HANDLE': [
'WAITING_FOR_FEEDBACK',
'ACCEPTED',
'REJECTED',
'SUSPENDED'
],
'ACCEPTED': ['SCHEDULED'],
'SCHEDULED': [
'FIRST_LISTING_DONE',
'FIRST_ORIGIN_LOADED'
],
'FIRST_LISTING_DONE': ['FIRST_ORIGIN_LOADED'],
'FIRST_ORIGIN_LOADED': [],
'REJECTED': [],
'SUSPENDED': ['PENDING'],
'DENIED': []
};
// Determine the possible next status out of the current one
const nextStatuses = nextStatusesFor[currentStatus];
function addStatusOption(status, index) {
// Push the next possible status options
const label = swh.add_forge.formatRequestStatusName(status);
$('#decisionOptions').append(
`<option value="${status}">${label}</option>`
);
}
// Remove all the options and add new ones
$('#decisionOptions').children().remove();
nextStatuses.forEach(addStatusOption);
$('#decisionOptions').append(
'<option hidden disabled selected value> -- Add a comment -- </option>'
);
}
function contactForgeAdmin(event) {
// Open the mailclient with pre-filled text
const mailTo = $('#contactForgeAdmin').attr('emailTo');
const subject = $('#contactForgeAdmin').attr('emailSubject');
const emailText = emailTempate({'forgeUrl': forgeRequest.forge_url}).trim().replace(/\n/g, '%0D%0A');
const w = window.open('', '_blank', '', true);
w.location.href = `mailto: ${mailTo}?subject=${subject}&body=${emailText}`;
w.focus();
}
diff --git a/assets/src/bundles/admin/deposit.js b/assets/src/bundles/admin/deposit.js
index 7d7f98d4..6f109f6a 100644
--- a/assets/src/bundles/admin/deposit.js
+++ b/assets/src/bundles/admin/deposit.js
@@ -1,163 +1,159 @@
/**
* Copyright (C) 2018-2022 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 {getHumanReadableDate} from 'utils/functions';
+
function genSwhLink(data, type) {
if (type === 'display' && data && data.startsWith('swh')) {
const browseUrl = Urls.browse_swhid(data);
const formattedSWHID = data.replace(/;/g, ';<br/>');
return `<a href="${browseUrl}">${formattedSWHID}</a>`;
}
return data;
}
function genLink(data, type) {
if (type === 'display' && data) {
const sData = encodeURI(data);
return `<a href="${sData}">${sData}</a>`;
}
return data;
}
export function initDepositAdmin(username, isStaff) {
let depositsTable;
$(document).ready(() => {
$.fn.dataTable.ext.errMode = 'none';
depositsTable = $('#swh-admin-deposit-list')
.on('error.dt', (e, settings, techNote, message) => {
$('#swh-admin-deposit-list-error').text(message);
})
.DataTable({
serverSide: true,
processing: true,
// let's define the order of table options display
// f: (f)ilter
// l: (l)ength changing
// r: p(r)ocessing
// t: (t)able
// i: (i)nfo
// p: (p)agination
// see https://datatables.net/examples/basic_init/dom.html
dom: '<<"d-flex justify-content-between align-items-center"f' +
'<"#list-exclude">l>rt<"bottom"ip>>',
// div#list-exclude is a custom filter added next to dataTable
// initialization below through js dom manipulation, see
// https://datatables.net/examples/advanced_init/dom_toolbar.html
ajax: {
url: Urls.admin_deposit_list(),
data: d => {
d.excludePattern = $('#swh-admin-deposit-list-exclude-filter').val();
if (!isStaff) {
d.username = username;
}
}
},
columns: [
{
data: 'id',
name: 'id'
},
{
data: 'type',
name: 'type'
},
{
data: 'uri',
name: 'uri',
render: (data, type, row) => {
return genLink(data, type);
}
},
{
data: 'reception_date',
name: 'reception_date',
- render: (data, type, row) => {
- if (type === 'display') {
- const date = new Date(data);
- return date.toLocaleString();
- }
- return data;
- }
+ render: getHumanReadableDate
},
{
data: 'status',
name: 'status'
},
{
data: 'status_detail',
name: 'status_detail',
render: (data, type, row) => {
if (type === 'display' && data) {
let text = data;
if (typeof data === 'object') {
text = JSON.stringify(data, null, 4);
}
return `<div style="width: 200px; white-space: pre; overflow-x: auto;">${text}</div>`;
}
return data;
},
orderable: false,
visible: false
},
{
data: 'swhid',
name: 'swhid',
render: (data, type, row) => {
return genSwhLink(data, type);
},
orderable: false,
visible: false
},
{
data: 'swhid_context',
name: 'swhid_context',
render: (data, type, row) => {
return genSwhLink(data, type);
},
orderable: false,
visible: false
}
],
scrollX: true,
scrollY: '50vh',
scrollCollapse: true,
order: [[0, 'desc']]
});
// Some more customization is needed on the table
$('div#list-exclude').html(`<div id="swh-admin-deposit-list-exclude-wrapper">
<div id="swh-admin-deposit-list-exclude-div-wrapper" class="dataTables_filter">
<label>
Exclude:<input id="swh-admin-deposit-list-exclude-filter"
type="search"
value="check-deposit"
class="form-control form-control-sm"
placeholder="exclude-pattern" aria-controls="swh-admin-deposit-list">
</input>
</label>
</div>
</div>
`);
// Adding exclusion pattern update behavior, when typing, update search
$('#swh-admin-deposit-list-exclude-filter').keyup(function() {
depositsTable.draw();
});
// at last draw the table
depositsTable.draw();
});
$('a.toggle-col').on('click', function(e) {
e.preventDefault();
var column = depositsTable.column($(this).attr('data-column'));
column.visible(!column.visible());
if (column.visible()) {
$(this).removeClass('col-hidden');
} else {
$(this).addClass('col-hidden');
}
});
}
diff --git a/assets/src/bundles/admin/origin-save.js b/assets/src/bundles/admin/origin-save.js
index f7d08220..f711c99a 100644
--- a/assets/src/bundles/admin/origin-save.js
+++ b/assets/src/bundles/admin/origin-save.js
@@ -1,414 +1,409 @@
/**
- * Copyright (C) 2018-2021 The Software Heritage developers
+ * Copyright (C) 2018-2022 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, htmlAlert} from 'utils/functions';
+import {handleFetchError, csrfPost, htmlAlert,
+ getHumanReadableDate} from 'utils/functions';
import {swhSpinnerSrc} from 'utils/constants';
let authorizedOriginTable;
let unauthorizedOriginTable;
let pendingSaveRequestsTable;
let acceptedSaveRequestsTable;
let rejectedSaveRequestsTable;
function enableRowSelection(tableSel) {
$(`${tableSel} tbody`).on('click', 'tr', function() {
if ($(this).hasClass('selected')) {
$(this).removeClass('selected');
$(tableSel).closest('.tab-pane').find('.swh-action-need-selection').prop('disabled', true);
} else {
$(`${tableSel} tr.selected`).removeClass('selected');
$(this).addClass('selected');
$(tableSel).closest('.tab-pane').find('.swh-action-need-selection').prop('disabled', false);
}
});
}
export function initOriginSaveAdmin() {
$(document).ready(() => {
$.fn.dataTable.ext.errMode = 'throw';
authorizedOriginTable = $('#swh-authorized-origin-urls').DataTable({
serverSide: true,
ajax: Urls.admin_origin_save_authorized_urls_list(),
columns: [{data: 'url', name: 'url'}],
scrollY: '50vh',
scrollCollapse: true,
info: false
});
enableRowSelection('#swh-authorized-origin-urls');
swh.webapp.addJumpToPagePopoverToDataTable(authorizedOriginTable);
unauthorizedOriginTable = $('#swh-unauthorized-origin-urls').DataTable({
serverSide: true,
ajax: Urls.admin_origin_save_unauthorized_urls_list(),
columns: [{data: 'url', name: 'url'}],
scrollY: '50vh',
scrollCollapse: true,
info: false
});
enableRowSelection('#swh-unauthorized-origin-urls');
swh.webapp.addJumpToPagePopoverToDataTable(unauthorizedOriginTable);
const columnsData = [
{
data: 'id',
name: 'id',
visible: false,
searchable: false
},
{
data: 'save_request_date',
name: 'request_date',
- render: (data, type, row) => {
- if (type === 'display') {
- const date = new Date(data);
- return date.toLocaleString();
- }
- return data;
- }
+ render: getHumanReadableDate
},
{
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') {
let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(sanitizedURL)}`;
if (row.visit_date) {
browseOriginUrl += `&amp;timestamp=${encodeURIComponent(row.visit_date)}`;
}
html += `<a href="${browseOriginUrl}">${sanitizedURL}</a>`;
} else {
html += sanitizedURL;
}
html += `&nbsp;<a href="${sanitizedURL}" target="_blank" rel="noopener noreferrer">` +
'<i class="mdi mdi-open-in-new" aria-hidden="true"></i></a>';
return html;
}
return data;
}
}
];
pendingSaveRequestsTable = $('#swh-origin-save-pending-requests').DataTable({
serverSide: true,
processing: true,
language: {
processing: `<img src="${swhSpinnerSrc}"></img>`
},
ajax: Urls.origin_save_requests_list('pending'),
searchDelay: 1000,
columns: columnsData,
scrollY: '50vh',
scrollCollapse: true,
order: [[0, 'desc']],
responsive: {
details: {
type: 'none'
}
}
});
enableRowSelection('#swh-origin-save-pending-requests');
swh.webapp.addJumpToPagePopoverToDataTable(pendingSaveRequestsTable);
columnsData.push({
name: 'info',
render: (data, type, row) => {
if (row.save_task_status === 'succeeded' || row.save_task_status === 'failed' ||
row.note != null) {
return `<i class="mdi mdi-information-outline swh-save-request-info" aria-hidden="true"
style="cursor: pointer"
onclick="swh.save.displaySaveRequestInfo(event, ${row.id})"></i>`;
} else {
return '';
}
}
});
rejectedSaveRequestsTable = $('#swh-origin-save-rejected-requests').DataTable({
serverSide: true,
processing: true,
language: {
processing: `<img src="${swhSpinnerSrc}"></img>`
},
ajax: Urls.origin_save_requests_list('rejected'),
searchDelay: 1000,
columns: columnsData,
scrollY: '50vh',
scrollCollapse: true,
order: [[0, 'desc']],
responsive: {
details: {
type: 'none'
}
}
});
enableRowSelection('#swh-origin-save-rejected-requests');
swh.webapp.addJumpToPagePopoverToDataTable(rejectedSaveRequestsTable);
columnsData.splice(columnsData.length - 1, 0, {
data: 'save_task_status',
name: 'save_task_status'
});
acceptedSaveRequestsTable = $('#swh-origin-save-accepted-requests').DataTable({
serverSide: true,
processing: true,
language: {
processing: `<img src="${swhSpinnerSrc}"></img>`
},
ajax: Urls.origin_save_requests_list('accepted'),
searchDelay: 1000,
columns: columnsData,
scrollY: '50vh',
scrollCollapse: true,
order: [[0, 'desc']],
responsive: {
details: {
type: 'none'
}
}
});
enableRowSelection('#swh-origin-save-accepted-requests');
swh.webapp.addJumpToPagePopoverToDataTable(acceptedSaveRequestsTable);
$('#swh-origin-save-requests-nav-item').on('shown.bs.tab', () => {
pendingSaveRequestsTable.draw();
});
$('#swh-origin-save-url-filters-nav-item').on('shown.bs.tab', () => {
authorizedOriginTable.draw();
});
$('#swh-authorized-origins-tab').on('shown.bs.tab', () => {
authorizedOriginTable.draw();
});
$('#swh-unauthorized-origins-tab').on('shown.bs.tab', () => {
unauthorizedOriginTable.draw();
});
$('#swh-save-requests-pending-tab').on('shown.bs.tab', () => {
pendingSaveRequestsTable.draw();
});
$('#swh-save-requests-accepted-tab').on('shown.bs.tab', () => {
acceptedSaveRequestsTable.draw();
});
$('#swh-save-requests-rejected-tab').on('shown.bs.tab', () => {
rejectedSaveRequestsTable.draw();
});
$('#swh-save-requests-pending-tab').click(() => {
pendingSaveRequestsTable.ajax.reload(null, false);
});
$('#swh-save-requests-accepted-tab').click(() => {
acceptedSaveRequestsTable.ajax.reload(null, false);
});
$('#swh-save-requests-rejected-tab').click(() => {
rejectedSaveRequestsTable.ajax.reload(null, false);
});
$('body').on('click', e => {
if ($(e.target).parents('.popover').length > 0) {
e.stopPropagation();
} else if ($(e.target).parents('.swh-save-request-info').length === 0) {
$('.swh-save-request-info').popover('dispose');
}
});
});
}
export async function addAuthorizedOriginUrl() {
const originUrl = $('#swh-authorized-url-prefix').val();
const addOriginUrl = Urls.admin_origin_save_add_authorized_url(originUrl);
try {
const response = await csrfPost(addOriginUrl);
handleFetchError(response);
authorizedOriginTable.row.add({'url': originUrl}).draw();
$('.swh-add-authorized-origin-status').html(
htmlAlert('success', 'The origin url prefix has been successfully added in the authorized list.', true)
);
} catch (_) {
$('.swh-add-authorized-origin-status').html(
htmlAlert('warning', 'The provided origin url prefix is already registered in the authorized list.', true)
);
}
}
export async function removeAuthorizedOriginUrl() {
const originUrl = $('#swh-authorized-origin-urls tr.selected').text();
if (originUrl) {
const removeOriginUrl = Urls.admin_origin_save_remove_authorized_url(originUrl);
try {
const response = await csrfPost(removeOriginUrl);
handleFetchError(response);
authorizedOriginTable.row('.selected').remove().draw();
} catch (_) {}
}
}
export async function addUnauthorizedOriginUrl() {
const originUrl = $('#swh-unauthorized-url-prefix').val();
const addOriginUrl = Urls.admin_origin_save_add_unauthorized_url(originUrl);
try {
const response = await csrfPost(addOriginUrl);
handleFetchError(response);
unauthorizedOriginTable.row.add({'url': originUrl}).draw();
$('.swh-add-unauthorized-origin-status').html(
htmlAlert('success', 'The origin url prefix has been successfully added in the unauthorized list.', true)
);
} catch (_) {
$('.swh-add-unauthorized-origin-status').html(
htmlAlert('warning', 'The provided origin url prefix is already registered in the unauthorized list.', true)
);
}
}
export async function removeUnauthorizedOriginUrl() {
const originUrl = $('#swh-unauthorized-origin-urls tr.selected').text();
if (originUrl) {
const removeOriginUrl = Urls.admin_origin_save_remove_unauthorized_url(originUrl);
try {
const response = await csrfPost(removeOriginUrl);
handleFetchError(response);
unauthorizedOriginTable.row('.selected').remove().draw();
} catch (_) {};
}
}
export function acceptOriginSaveRequest() {
const selectedRow = pendingSaveRequestsTable.row('.selected');
if (selectedRow.length) {
const acceptOriginSaveRequestCallback = async() => {
const rowData = selectedRow.data();
const acceptSaveRequestUrl = Urls.admin_origin_save_request_accept(rowData['visit_type'], rowData['origin_url']);
await csrfPost(acceptSaveRequestUrl);
pendingSaveRequestsTable.ajax.reload(null, false);
};
swh.webapp.showModalConfirm(
'Accept origin save request ?',
'Are you sure to accept this origin save request ?',
acceptOriginSaveRequestCallback);
}
}
const rejectModalHtml = `
<form id="swh-rejection-form">
<div class="form-group row">
<label for="swh-rejection-reason" class="col-4 col-form-label">
Rejection reason:
</label>
<div class="col-8">
<select class="custom-select" id="swh-rejection-reason">
<option value="custom" selected>Custom</option>
<option value="invalid-origin">Invalid origin</option>
<option value="invalid-origin-type">Invalid origin type</option>
<option value="origin-not-found">Origin not found</option>
</select>
</div>
</div>
<div class="form-group row">
<textarea class="form-control" id="swh-rejection-text"></textarea>
</div>
<button type="submit" class="btn btn-default float-right" id="swh-rejection-submit">
Reject
</button>
</form>
`;
export function rejectOriginSaveRequest() {
const selectedRow = pendingSaveRequestsTable.row('.selected');
const rowData = selectedRow.data();
if (selectedRow.length) {
const rejectOriginSaveRequestCallback = async() => {
$('#swh-web-modal-html').modal('hide');
const rejectSaveRequestUrl = Urls.admin_origin_save_request_reject(
rowData['visit_type'], rowData['origin_url']);
await csrfPost(rejectSaveRequestUrl, {},
JSON.stringify({note: $('#swh-rejection-text').val()}));
pendingSaveRequestsTable.ajax.reload(null, false);
};
let currentRejectionReason = 'custom';
const rejectionTexts = {};
swh.webapp.showModalHtml('Reject origin save request ?', rejectModalHtml);
$('#swh-rejection-reason').on('change', (event) => {
// backup current textarea value
rejectionTexts[currentRejectionReason] = $('#swh-rejection-text').val();
currentRejectionReason = event.target.value;
let newRejectionText = '';
if (rejectionTexts.hasOwnProperty(currentRejectionReason)) {
// restore previous textarea value
newRejectionText = rejectionTexts[currentRejectionReason];
} else {
// fill textarea with default text according to rejection type
if (currentRejectionReason === 'invalid-origin') {
newRejectionText = `The origin with URL ${rowData['origin_url']} is not ` +
`a link to a ${rowData['visit_type']} repository.`;
} else if (currentRejectionReason === 'invalid-origin-type') {
newRejectionText = `The origin with URL ${rowData['origin_url']} is not ` +
`of type ${rowData['visit_type']}.`;
} else if (currentRejectionReason === 'origin-not-found') {
newRejectionText = `The origin with URL ${rowData['origin_url']} cannot be found.`;
}
}
$('#swh-rejection-text').val(newRejectionText);
});
$('#swh-rejection-form').on('submit', (event) => {
event.preventDefault();
event.stopPropagation();
// ensure confirmation modal will be displayed above the html modal
$('#swh-web-modal-html').css('z-index', 4000);
swh.webapp.showModalConfirm(
'Reject origin save request ?',
'Are you sure to reject this origin save request ?',
rejectOriginSaveRequestCallback);
});
}
}
function removeOriginSaveRequest(requestTable) {
const selectedRow = requestTable.row('.selected');
if (selectedRow.length) {
const requestId = selectedRow.data()['id'];
const removeOriginSaveRequestCallback = async() => {
const removeSaveRequestUrl = Urls.admin_origin_save_request_remove(requestId);
await csrfPost(removeSaveRequestUrl);
requestTable.ajax.reload(null, false);
};
swh.webapp.showModalConfirm(
'Remove origin save request ?',
'Are you sure to remove this origin save request ?',
removeOriginSaveRequestCallback);
}
}
export function removePendingOriginSaveRequest() {
removeOriginSaveRequest(pendingSaveRequestsTable);
}
export function removeAcceptedOriginSaveRequest() {
removeOriginSaveRequest(acceptedSaveRequestsTable);
}
export function removeRejectedOriginSaveRequest() {
removeOriginSaveRequest(rejectedSaveRequestsTable);
}
diff --git a/assets/src/bundles/save/index.js b/assets/src/bundles/save/index.js
index 3bdd2963..b2214071 100644
--- a/assets/src/bundles/save/index.js
+++ b/assets/src/bundles/save/index.js
@@ -1,561 +1,555 @@
/**
- * Copyright (C) 2018-2021 The Software Heritage developers
+ * Copyright (C) 2018-2022 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';
+ getCanonicalOriginURL, getHumanReadableDate} 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 = `
<div class="custom-control custom-checkbox swhid-option">
<input class="custom-control-input" value="option-user-requests-filter" type="checkbox"
id="swh-save-requests-user-filter">
<label class="custom-control-label font-weight-normal" for="swh-save-requests-user-filter">
show only your own requests
</label>
</div>
`;
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: `<img src="${swhSpinnerSrc}"></img>`
},
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;
- }
+ render: getHumanReadableDate
},
{
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 += `&amp;timestamp=${encodeURIComponent(row.visit_date)}`;
}
html += `<a href="${browseOriginUrl}">${sanitizedURL}</a>`;
} else {
const tooltip = 'origin was successfully loaded, waiting for data to be available in database';
html += `<span title="${tooltip}">${sanitizedURL}</span>`;
}
} else {
html += sanitizedURL;
}
html += `&nbsp;<a href="${sanitizedURL}" target="_blank" rel="noopener noreferrer">` +
'<i class="mdi mdi-open-in-new" aria-hidden="true"></i></a>';
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 `<i class="mdi mdi-information-outline swh-save-request-info"
aria-hidden="true" style="cursor: pointer"
onclick="swh.save.displaySaveRequestInfo(event, ${row.id})"></i>`;
} else {
return '';
}
}
},
{
render: (data, type, row) => {
if (row.save_request_status === 'accepted') {
const saveAgainButton =
'<button class="btn btn-default btn-sm swh-save-origin-again" type="button" ' +
`onclick="swh.save.fillSaveRequestFormAndScroll(` +
`'${row.visit_type}', '${row.origin_url}');">` +
'<i class="mdi mdi-camera mdi-fw" aria-hidden="true"></i>' +
'Save again</button>';
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 ' +
'<i style="cursor: pointer; position: absolute; right: 1rem;" ' +
`class="mdi mdi-close swh-save-request-info-close"></i>`,
content: `<div class="swh-popover swh-save-request-info-popover">
<div class="text-center">
<img src=${swhSpinnerSrc}></img>
<p>Fetching task information ...</p>
</div>
</div>`,
html: true,
placement: 'left',
sanitizeFn: swh.webapp.filterXSS
});
$(event.target).on('shown.bs.popover', function() {
const popoverId = $(this).attr('aria-describedby');
$(`#${popoverId} .mdi-close`).click(() => {
$(this).popover('dispose');
});
});
$(event.target).popover('show');
const response = await fetch(saveRequestTaskInfoUrl);
const saveRequestTaskInfo = await response.json();
let content;
if ($.isEmptyObject(saveRequestTaskInfo)) {
content = 'Not available';
} else if (saveRequestTaskInfo.note != null) {
content = saveRequestTaskInfo.note;
} else {
const saveRequestInfo = [];
const taskData = {
'Type': ['raw', 'type'],
'Visit status': ['raw', 'visit_status'],
'Arguments': ['json', 'arguments'],
'Id': ['raw', 'id'],
'Backend id': ['raw', 'backend_id'],
'Scheduling date': ['date', 'scheduled'],
'Start date': ['date', 'started'],
'Completion date': ['date', 'ended'],
'Duration': ['duration', 'duration'],
'Runner': ['raw', 'worker'],
'Log': ['raw', 'message']
};
for (const [title, [type, property]] of Object.entries(taskData)) {
if (saveRequestTaskInfo.hasOwnProperty(property)) {
saveRequestInfo.push({
key: title,
value: formatValuePerType(type, saveRequestTaskInfo[property])
});
}
}
content = '<table class="table"><tbody>';
for (const info of saveRequestInfo) {
content +=
`<tr>
<th class="swh-metadata-table-row swh-metadata-table-key">${info.key}</th>
<td class="swh-metadata-table-row swh-metadata-table-value">
<pre>${info.value}</pre>
</td>
</tr>`;
}
content += '</tbody></table>';
}
$('.swh-popover').html(content);
$(event.target).popover('update');
}
export function fillSaveRequestFormAndScroll(visitType, originUrl) {
$('#swh-input-origin-url').val(originUrl);
let originTypeFound = false;
$('#swh-input-visit-type option').each(function() {
const val = $(this).val();
if (val && originUrl.includes(val)) {
$(this).prop('selected', true);
originTypeFound = true;
}
});
if (!originTypeFound) {
$('#swh-input-visit-type option').each(function() {
const val = $(this).val();
if (val === visitType) {
$(this).prop('selected', true);
}
});
}
window.scrollTo(0, 0);
}
diff --git a/assets/src/utils/functions.js b/assets/src/utils/functions.js
index 0a7e8579..ae42d335 100644
--- a/assets/src/utils/functions.js
+++ b/assets/src/utils/functions.js
@@ -1,145 +1,153 @@
/**
* Copyright (C) 2018-2020 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
*/
// utility functions
import Cookies from 'js-cookie';
export function handleFetchError(response) {
if (!response.ok) {
throw response;
}
return response;
}
export function handleFetchErrors(responses) {
for (let i = 0; i < responses.length; ++i) {
if (!responses[i].ok) {
throw responses[i];
}
}
return responses;
}
export function staticAsset(asset) {
return `${__STATIC__}${asset}`;
}
export function csrfPost(url, headers = {}, body = null) {
headers['X-CSRFToken'] = Cookies.get('csrftoken');
return fetch(url, {
credentials: 'include',
headers: headers,
method: 'POST',
body: body
});
}
export function isGitRepoUrl(url, pathPrefix = '/') {
const allowedProtocols = ['http:', 'https:', 'git:'];
if (allowedProtocols.find(protocol => protocol === url.protocol) === undefined) {
return false;
}
if (!url.pathname.startsWith(pathPrefix)) {
return false;
}
const re = new RegExp('[\\w\\.-]+\\/?(?!=.git)(?:\\.git\\/?)?$');
return re.test(url.pathname.slice(pathPrefix.length));
};
export function removeUrlFragment() {
history.replaceState('', document.title, window.location.pathname + window.location.search);
}
export function selectText(startNode, endNode) {
const selection = window.getSelection();
selection.removeAllRanges();
const range = document.createRange();
range.setStart(startNode, 0);
if (endNode.nodeName !== '#text') {
range.setEnd(endNode, endNode.childNodes.length);
} else {
range.setEnd(endNode, endNode.textContent.length);
}
selection.addRange(range);
}
export function htmlAlert(type, message, closable = false) {
let closeButton = '';
let extraClasses = '';
if (closable) {
closeButton =
`<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>`;
extraClasses = 'alert-dismissible';
}
return `<div class="alert alert-${type} ${extraClasses}" role="alert">${message}${closeButton}</div>`;
}
export function isValidURL(string) {
try {
new URL(string);
} catch (_) {
return false;
}
return true;
}
export async function isArchivedOrigin(originPath) {
if (!isValidURL(originPath)) {
// Not a valid URL, return immediately
return false;
} else {
const response = await fetch(Urls.api_1_origin(originPath));
return response.ok && response.status === 200; // Success response represents an archived origin
}
}
async function getCanonicalGithubOriginURL(ownerRepo) {
const ghApiResponse = await fetch(`https://api.github.com/repos/${ownerRepo}`);
if (ghApiResponse.ok && ghApiResponse.status === 200) {
const ghApiResponseData = await ghApiResponse.json();
return ghApiResponseData.html_url;
}
}
export async function getCanonicalOriginURL(originUrl) {
let originUrlLower = originUrl.toLowerCase();
// github.com URL processing
const ghUrlRegex = /^http[s]*:\/\/github.com\//;
if (originUrlLower.match(ghUrlRegex)) {
// remove trailing .git
if (originUrlLower.endsWith('.git')) {
originUrlLower = originUrlLower.slice(0, -4);
}
// remove trailing slash
if (originUrlLower.endsWith('/')) {
originUrlLower = originUrlLower.slice(0, -1);
}
// extract {owner}/{repo}
const ownerRepo = originUrlLower.replace(ghUrlRegex, '');
// fetch canonical URL from github Web API
const url = getCanonicalGithubOriginURL(ownerRepo);
if (url) {
return url;
}
}
const ghpagesUrlRegex = /^http[s]*:\/\/(?<owner>[^/]+).github.io\/(?<repo>[^/]+)\/?.*/;
const parsedUrl = originUrlLower.match(ghpagesUrlRegex);
if (parsedUrl) {
const ownerRepo = `${parsedUrl.groups.owner}/${parsedUrl.groups.repo}`;
// fetch canonical URL from github Web API
const url = getCanonicalGithubOriginURL(ownerRepo);
if (url) {
return url;
}
}
return originUrl;
}
+
+export function getHumanReadableDate(data) {
+ // Display iso format date string into a human readable date
+ // This is expected to be used by date field in datatable listing views
+ // Example: 3/24/2022, 10:31:08 AM
+ const date = new Date(data);
+ return date.toLocaleString();
+}

File Metadata

Mime Type
text/x-diff
Expires
Jul 4 2025, 10:27 AM (4 w, 6 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3273151

Event Timeline