Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F9340241
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
59 KB
Subscribers
None
View Options
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 += `&timestamp=${encodeURIComponent(row.visit_date)}`;
}
html += `<a href="${browseOriginUrl}">${sanitizedURL}</a>`;
} else {
html += sanitizedURL;
}
html += ` <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 += `&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 += ` <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">×</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
Details
Attached
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
Attached To
rDWAPPS Web applications
Event Timeline
Log In to Comment