Changeset View
Changeset View
Standalone View
Standalone View
assets/src/bundles/forge_add/index.js
- This file was added.
/** | |||||
* Copyright (C) 2021 The Software Heritage developers | |||||
* See the AUTHORS file at the top-level directory of this distribution | |||||
* License: GNU Affero General Public License version 3, or any later version | |||||
* See top-level LICENSE file for more information | |||||
*/ | |||||
import {csrfPost, handleFetchError, htmlAlert, removeUrlFragment} from 'utils/functions'; | |||||
import {swhSpinnerSrc} from 'utils/constants'; | |||||
let addForgeRequestsTable; | |||||
async function forgeAddRequest( | |||||
forgeType, forgeUrl, acceptedCallback, pendingCallback, errorCallback | |||||
) { | |||||
// Actually trigger the forge add request | |||||
const addForgeUrlRequestUrl = Urls.api_1_add_forge(forgeType, forgeUrl); | |||||
// show computation spinner | |||||
$('.swh-processing-forge-add-request').css('display', 'block'); | |||||
const headers = {}; | |||||
const body = null; | |||||
try { | |||||
const response = await csrfPost(addForgeUrlRequestUrl, headers, body); | |||||
handleFetchError(response); | |||||
const data = await response.json(); | |||||
// hide the computation spinner | |||||
$('.swh-processing-forge-add-request').css('display', 'none'); | |||||
if (data.add_forge_request_status === 'accepted') { | |||||
acceptedCallback(); | |||||
} else { | |||||
pendingCallback(); | |||||
} | |||||
} catch (response) { | |||||
$('.swh-processing-forge-add-request').css('display', 'none'); | |||||
const errorData = await response.json(); | |||||
errorCallback(response.status, errorData); | |||||
}; | |||||
} | |||||
const userRequestsFilterCheckbox = ` | |||||
<div class="custom-control custom-checkbox swhid-option"> | |||||
<input class="custom-control-input" value="option-user-requests-filter" type="checkbox" | |||||
id="swh-add-forge-requests-user-filter"> | |||||
<label class="custom-control-label font-weight-normal" for="swh-add-forge-requests-user-filter"> | |||||
show only your own requests | |||||
</label> | |||||
</div> | |||||
`; | |||||
export function initForgeAdd() { | |||||
$(document).ready(() => { | |||||
$.fn.dataTable.ext.errMode = 'none'; | |||||
// set git as the default value as before | |||||
$('#swh-input-forge-type').val('gitlab'); | |||||
addForgeRequestsTable = $('#swh-forge-add-requests') | |||||
.on('error.dt', (e, settings, techNote, message) => { | |||||
$('#swh-forge-add-request-list-error').text( | |||||
'An error occurred while retrieving the add forge requests list' | |||||
); | |||||
console.log(message); | |||||
}) | |||||
.DataTable({ | |||||
serverSide: true, | |||||
processing: true, | |||||
language: { | |||||
processing: `<img src="${swhSpinnerSrc}"></img>` | |||||
}, | |||||
ajax: { | |||||
url: Urls.forge_add_requests_list('all'), | |||||
data: (d) => { | |||||
if (swh.webapp.isUserLoggedIn() && $('#swh-add-forge-requests-user-filter').prop('checked')) { | |||||
d.user_requests_only = '1'; | |||||
} | |||||
} | |||||
}, | |||||
searchDelay: 1000, | |||||
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-add-forge-requests-user-filter').on('change', () => { | |||||
addForgeRequestsTable.draw(); | |||||
}); | |||||
} | |||||
}, | |||||
columns: [ | |||||
{ | |||||
data: 'request_date', | |||||
name: 'request_date', | |||||
render: (data, type, row) => { | |||||
if (type === 'display') { | |||||
const date = new Date(data); | |||||
return date.toLocaleString(); | |||||
} | |||||
return data; | |||||
} | |||||
}, | |||||
{ | |||||
data: 'forge_type', | |||||
name: 'forge_type' | |||||
}, | |||||
{ | |||||
data: 'forge_url', | |||||
name: 'forge_url' | |||||
}, | |||||
{ | |||||
data: 'request_status', | |||||
name: 'request_status' | |||||
}, | |||||
{ | |||||
data: 'task_status', | |||||
name: 'task_status' | |||||
}, | |||||
{ | |||||
name: 'info', | |||||
render: (data, type, row) => { | |||||
if (row.task_status === 'succeeded' || row.task_status === 'failed' || | |||||
row.note != null) { | |||||
return `<i class="mdi mdi-information-outline swh-add-forge-request-info" | |||||
aria-hidden="true" style="cursor: pointer" | |||||
onclick="swh.forge_add.displayForgeAddRequestInfo(event, ${row.id})"></i>`; | |||||
} else { | |||||
return ''; | |||||
} | |||||
} | |||||
} | |||||
], | |||||
scrollY: '50vh', | |||||
scrollCollapse: true, | |||||
order: [[0, 'desc']], | |||||
responsive: { | |||||
details: { | |||||
type: 'none' | |||||
} | |||||
} | |||||
}); | |||||
swh.webapp.addJumpToPagePopoverToDataTable(addForgeRequestsTable); | |||||
$('#swh-forge-add-requests-list').on('shown.bs.tab', () => { | |||||
addForgeRequestsTable.draw(); | |||||
window.location.hash = '#forge-requests'; | |||||
}); | |||||
$('#swh-forge-add-request-help').on('shown.bs.tab', () => { | |||||
removeUrlFragment(); | |||||
$('.swh-add-forge-request-info').popover('dispose'); | |||||
}); | |||||
const addForgeRequestAcceptedAlert = htmlAlert( | |||||
'success', | |||||
'The "add forge now" request has been accepted and will be processed as soon as possible.', | |||||
true | |||||
); | |||||
const addForgeRequestPendingAlert = htmlAlert( | |||||
'warning', | |||||
'The "add forge now" request has been put in pending state and may be accepted for processing after manual review.', | |||||
true | |||||
); | |||||
const addForgeRequestRateLimitedAlert = htmlAlert( | |||||
'danger', | |||||
'The rate limit for "add forge now" requests has been reached. Please try again later.', | |||||
true | |||||
); | |||||
const addForgeRequestUnknownErrorAlert = htmlAlert( | |||||
'danger', | |||||
'An unexpected error happened when submitting the "add forge now request".', | |||||
true | |||||
); | |||||
$('#swh-add-forge-forge-form').submit(async event => { | |||||
event.preventDefault(); | |||||
event.stopPropagation(); | |||||
$('.alert').alert('close'); | |||||
if (event.target.checkValidity()) { | |||||
$(event.target).removeClass('was-validated'); | |||||
const forgeType = $('#swh-input-visit-type').val(); | |||||
const forgeUrl = $('#swh-input-forge-url').val(); | |||||
forgeAddRequest( | |||||
forgeType, | |||||
forgeUrl, | |||||
() => $('#swh-forge-add-request-status').html(addForgeRequestAcceptedAlert), | |||||
() => $('#swh-forge-add-request-status').html(addForgeRequestPendingAlert), | |||||
(statusCode, errorData) => { | |||||
$('#swh-forge-add-request-status').css('color', 'red'); | |||||
if (statusCode === 403) { | |||||
const errorAlert = htmlAlert('danger', `Error: ${errorData['reason']}`); | |||||
$('#swh-forge-add-request-status').html(errorAlert); | |||||
} else if (statusCode === 429) { | |||||
$('#swh-forge-add-request-status').html(addForgeRequestRateLimitedAlert); | |||||
} else if (statusCode === 400) { | |||||
const errorAlert = htmlAlert('danger', errorData['reason']); | |||||
$('#swh-forge-add-request-status').html(errorAlert); | |||||
} else { | |||||
$('#swh-forge-add-request-status').html(addForgeRequestUnknownErrorAlert); | |||||
} | |||||
}); | |||||
} else { | |||||
$(event.target).addClass('was-validated'); | |||||
} | |||||
}); | |||||
$('#swh-show-forge-add-requests-list').on('click', (event) => { | |||||
event.preventDefault(); | |||||
$('.nav-tabs a[href="#swh-forge-add-requests-list"]').tab('show'); | |||||
}); | |||||
$('#swh-input-forge-url').on('input', function(event) { | |||||
const forgeUrl = $(this).val().trim(); | |||||
$(this).val(forgeUrl); | |||||
$('#swh-input-visit-type option').each(function() { | |||||
const val = $(this).val(); | |||||
if (val && forgeUrl.includes(val)) { | |||||
$(this).prop('selected', true); | |||||
} | |||||
}); | |||||
}); | |||||
if (window.location.hash === '#forge-requests') { | |||||
$('.nav-tabs a[href="#swh-forge-add-requests-list"]').tab('show'); | |||||
} | |||||
}); | |||||
} | |||||
export function validateAddForgeUrl(input) { | |||||
let forgeUrl = null; | |||||
let validUrl = true; | |||||
try { | |||||
forgeUrl = new URL(input.value.trim()); | |||||
} catch (TypeError) { | |||||
validUrl = false; | |||||
} | |||||
if (validUrl) { | |||||
const allowedProtocols = ['http:', 'https:']; | |||||
validUrl = ( | |||||
allowedProtocols.find(protocol => protocol === forgeUrl.protocol) !== undefined | |||||
); | |||||
} | |||||
if (validUrl) { | |||||
input.setCustomValidity(''); | |||||
} else { | |||||
input.setCustomValidity('The forge 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 "add forge now request".', | |||||
true | |||||
); | |||||
$(document).ready(() => { | |||||
$('#swh-take-new-snapshot-form').submit(event => { | |||||
event.preventDefault(); | |||||
event.stopPropagation(); | |||||
const forgeType = $('#swh-input-visit-type').val(); | |||||
const forgeUrl = $('#swh-input-forge-url').val(); | |||||
forgeAddRequest(forgeType, forgeUrl, | |||||
() => $('#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 displayForgeAddRequestInfo(event, addForgeRequestId) { | |||||
event.stopPropagation(); | |||||
const addForgeRequestTaskInfoUrl = Urls.forge_add_task_info(addForgeRequestId); | |||||
// close popover when clicking again on the info icon | |||||
if ($(event.target).data('bs.popover')) { | |||||
$(event.target).popover('dispose'); | |||||
return; | |||||
} | |||||
$('.swh-add-forge-request-info').popover('dispose'); | |||||
$(event.target).popover({ | |||||
animation: false, | |||||
boundary: 'viewport', | |||||
container: 'body', | |||||
title: 'Add request task information ' + | |||||
'<i style="cursor: pointer; position: absolute; right: 1rem;" ' + | |||||
`class="mdi mdi-close swh-add-forge-request-info-close"></i>`, | |||||
content: `<div class="swh-popover swh-add-forge-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(addForgeRequestTaskInfoUrl); | |||||
const addForgeRequestTaskInfo = await response.json(); | |||||
let content; | |||||
if ($.isEmptyObject(addForgeRequestTaskInfo)) { | |||||
content = 'Not available'; | |||||
} else if (addForgeRequestTaskInfo.note != null) { | |||||
content = addForgeRequestTaskInfo.note; | |||||
} else { | |||||
const addForgeRequestInfo = []; | |||||
const taskData = { | |||||
'Type': ['raw', 'forge_type'], | |||||
'Arguments': ['json', 'arguments'], | |||||
'Id': ['raw', '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 (addForgeRequestTaskInfo.hasOwnProperty(property)) { | |||||
addForgeRequestInfo.push({ | |||||
key: title, | |||||
value: formatValuePerType(type, addForgeRequestTaskInfo[property]) | |||||
}); | |||||
} | |||||
} | |||||
content = '<table class="table"><tbody>'; | |||||
for (const info of addForgeRequestInfo) { | |||||
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 fillForgeAddRequestFormAndScroll(visitType, forgeUrl) { | |||||
$('#swh-input-forge-url').val(forgeUrl); | |||||
let forgeTypeFound = false; | |||||
$('#swh-input-visit-type option').each(function() { | |||||
const val = $(this).val(); | |||||
if (val && forgeUrl.includes(val)) { | |||||
$(this).prop('selected', true); | |||||
forgeTypeFound = true; | |||||
} | |||||
}); | |||||
if (!forgeTypeFound) { | |||||
$('#swh-input-visit-type option').each(function() { | |||||
const val = $(this).val(); | |||||
if (val === visitType) { | |||||
$(this).prop('selected', true); | |||||
} | |||||
}); | |||||
} | |||||
window.scrollTo(0, 0); | |||||
} |